How to Implement GeoIP Blocking in Nginx

How to Implement GeoIP Blocking in Nginx

How to Implement GeoIP Blocking in Nginx

There are plenty of legitimate reasons to restrict access to your site or application based on where a visitor is connecting from — licensing restrictions that only permit content in certain countries, compliance requirements, cutting down on abuse traffic that overwhelmingly originates from a handful of regions, or simply reducing noise from bots hammering an admin panel from countries none of your actual users live in. Nginx can do this natively using GeoIP databases, without needing an external service or a third-party firewall product sitting in front of your server.

This guide covers setting up GeoIP-based blocking (and allow-listing) in Nginx using MaxMind’s GeoLite2 database and the modern ngx_http_geoip2_module, since the legacy geoip module and its associated database format have been deprecated by MaxMind and shouldn’t be used for new setups.

How GeoIP Blocking Works in Nginx

Nginx doesn’t have geographic awareness built in — it relies on an external IP-to-country (or IP-to-city/region) database, most commonly MaxMind’s GeoLite2. The ngx_http_geoip2_module reads that database and exposes variables like $geoip2_data_country_code that you can then use in your configuration to allow or deny requests, redirect visitors, or serve different content entirely.

The general flow:

  1. Download and maintain a MaxMind GeoLite2 database (free with a registered account, or a paid GeoIP2 database for more accuracy).
  2. Install the ngx_http_geoip2_module (a dynamic module in most modern Nginx packages).
  3. Configure Nginx to load the database and expose country/region variables.
  4. Use those variables in map blocks or if conditions to allow or deny requests.

Requirements

Step 1: Sign Up for a MaxMind Account and Get a License Key

Go to MaxMind’s GeoLite2 sign-up page, create a free account, and generate a license key from the account portal. You’ll need this key to download the database via their API — direct download links without an account no longer work.

Step 2: Install the GeoIP2 Module and Database Tools

On Ubuntu/Debian, the module is often available as a package:

sudo apt update
sudo apt install nginx-module-geoip2 libmaxminddb0 libmaxminddb-dev mmdb-bin -y

If your Nginx package doesn’t provide nginx-module-geoip2, you may need to install geoipupdate (MaxMind’s official updater tool) instead:

sudo apt install geoipupdate -y

On CentOS/RHEL:

sudo dnf install epel-release -y
sudo dnf install libmaxminddb libmaxminddb-devel geoipupdate -y

Step 3: Configure geoipupdate and Download the Database

Edit /etc/GeoIP.conf (created by the geoipupdate package):

AccountID YOUR_ACCOUNT_ID
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-Country GeoLite2-City

Then run the updater:

sudo geoipupdate

This downloads the .mmdb database files, typically to /usr/share/GeoIP/ or /var/lib/GeoIP/ depending on distro defaults. Confirm they’re there:

ls -lh /usr/share/GeoIP/

You should see GeoLite2-Country.mmdb (and GeoLite2-City.mmdb if you requested it).

Step 4: Load the GeoIP2 Module in Nginx

If installed as a dynamic module, add this near the top of /etc/nginx/nginx.conf, before the events block:

load_module modules/ngx_http_geoip2_module.so;

Then inside the http block, point Nginx at your database and define which fields to expose as variables:

http {
    geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
        $geoip2_country_code country iso_code;
        $geoip2_country_name country names en;
    }

    ...
}

If you downloaded the City database and want more granularity (region, city):

    geoip2 /usr/share/GeoIP/GeoLite2-City.mmdb {
        $geoip2_city_name city names en;
        $geoip2_region_code subdivisions 0 iso_code;
    }

Step 5: Block or Allow by Country

The cleanest way to do this is with a map block, which avoids the performance and readability problems of stacking if statements. Define a map that flags whether a country is blocked:

http {
    map $geoip2_country_code $blocked_country {
        default 0;
        RU 1;
        CN 1;
        KP 1;
    }

    server {
        listen 443 ssl http2;
        server_name example.com;

        if ($blocked_country) {
            return 403;
        }

        location / {
            root /var/www/example.com;
            index index.html;
        }
    }
}

This blocks visitors from Russia, China, and North Korea (as an example — adjust country codes to whatever list your actual policy requires) with a 403 Forbidden, while everyone else passes through normally.

Allow-Listing Instead of Blocking

If your use case is the opposite — only permit specific countries — invert the map default:

map $geoip2_country_code $allowed_country {
    default 0;
    US 1;
    CA 1;
    GB 1;
}

server {
    if ($allowed_country = 0) {
        return 403;
    }
    ...
}

Redirecting Instead of Blocking Outright

Sometimes a redirect to a localized page or a “not available in your region” notice is friendlier than a bare 403:

if ($blocked_country) {
    return 302 https://example.com/not-available;
}

Step 6: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Step 7: Verify It’s Actually Working

Testing GeoIP blocking from your own machine is tricky since you’re presumably not physically located in the country you’re testing. A few practical approaches:

Use a VPN endpoint in a blocked country and confirm you get a 403.

Query the database directly to confirm a known IP resolves to the expected country:

mmdblookup --file /usr/share/GeoIP/GeoLite2-Country.mmdb --ip 1.2.3.4 country iso_code

(Swap 1.2.3.4 for an IP you know belongs to a specific country — many public IP-to-country lookup tools online can give you a sample IP for testing.)

Add a debug header temporarily to confirm the variable is populating correctly in production:

add_header X-Debug-Country $geoip2_country_code always;

Then check with curl:

curl -I https://example.com

Remove the debug header once confirmed working — you don’t want to leak this information to real visitors indefinitely.

Troubleshooting Common Issues

$geoip2_country_code is always empty. Usually means the geoip2 directive isn’t loaded correctly, or the module failed to load — check sudo nginx -t output carefully and confirm load_module is present and points to the correct .so path (varies by distro: /usr/lib/nginx/modules/ is common on Debian-based systems).

Everyone gets blocked, including allowed regions. Double check your map default value — a default 1; with map‘s logic inverted from what you intended is an easy mistake.

Database seems outdated or wrong for known IPs. GeoLite2 databases update periodically; MaxMind recommends updating at least monthly. Set up a cron job:

echo "0 3 * * 3 /usr/bin/geoipupdate" | sudo tee -a /etc/crontab

Nginx won’t start after adding load_module. Confirm the module .so file actually exists at the path specified — package names and install paths vary between Debian, Ubuntu, and RHEL-based systems.

Security Considerations

if ($blocked_country) {
    return 403;
}
access_log /var/log/nginx/geo_blocked.log combined if=$blocked_country;

Performance Tips

Real-World Use Cases

Best Practices

Wrapping Up

GeoIP blocking in Nginx is quick to set up and genuinely useful for cutting down unwanted traffic patterns or meeting basic regional content restrictions — but it’s worth going in with realistic expectations about its limits. It’s a blunt instrument, easily bypassed by anyone using a VPN, and dependent on a database that’s only ever approximately accurate. Used as one layer among several (rate limiting, WAF rules, proper authentication) rather than a standalone security measure, it does exactly what it’s good at: quietly filtering out the geography-based noise you don’t want reaching your application.

Exit mobile version