Skip to main content

GeoIP in Nginx: Access and Blocking Requests by Country

Nginx · 18.10.2026 · 6 min read
Illustration for “GeoIP in Nginx: Access and Blocking Requests by Country”

GeoIP lets nginx make a decision based on the visitor's country — show a localized version of the site, close access for regions where there are no legitimate clients, or just collect traffic statistics by geography. The module does not replace a firewall and does not protect against a distributed attack: it works at the HTTP level after the connection has already reached nginx, and suits targeted rules rather than protection from a flood of requests.

Two GeoIP modules and why geoip2 is preferable

Nginx has two modules for looking up a country by IP: the legacy ngx_http_geoip_module, which works with legacy-format binary GeoIP databases, and ngx_http_geoip2_module, which reads modern .mmdb format databases — GeoLite2 or the paid GeoIP2 from MaxMind. MaxMind has not updated the legacy GeoIP databases for a long time, so there is no point in the old module for new servers.

The geoip2 module is not part of the base nginx build — it is a dynamic module installed as a separate package from the official nginx repository, or built from source with the --add-dynamic-module option. The base setup of the nginx instance this module is added to is covered in the article on configuring Nginx for a site on a VDS.

Installing the module and the GeoLite2 database

After installing the package, the module is loaded with the load_module directive at the start of the main configuration file, before the events block. The GeoLite2-Country or GeoLite2-City database is downloaded from the MaxMind site — the free databases require registration and a license key, and updating them is usually automated with a separate cron job running geoipupdate.

The geoip2 directive is declared once in the http block and links the database file path to variables that can be used further in the configuration — the country code, the continent, or the city name if a City database is used.

load_module modules/ngx_http_geoip2_module.so;

http {
    geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
        auto_reload 60m;
        $geoip2_data_country_code country iso_code;
    }

    map $geoip2_data_country_code $country_blocked {
        default 0;
        RU 1;
        BY 1;
    }
}

Blocking countries with map and return 403

The $geoip2_data_country_code variable is fed into a map that turns the country code into a simple 0 or 1 flag. Inside server or location it remains to check this flag with an if directive and return 403 without reaching the backend — a request from a blocked country never reaches PHP-FPM or the proxied application.

ApproachLevelWhen it fits
geoip2 in nginxHTTP, on the server itselftargeted rules for individual locations or domains
Cloudflare firewall rulesin front of the server, on the CDNbulk blocking of countries before the request even reaches the VDS
limit_req / limit_connHTTP, on the server itselfprotection from a flood of requests regardless of geography

These three approaches do not exclude each other: geoip2 decides where the request is from, Cloudflare filters traffic at the edge of the network, and limit_req and limit_conn limit the request rate regardless of country.

Where this fits, and where Cloudflare is the better place to block

A rule at the nginx level still accepts the TCP connection and spends process resources on parsing the request headers before returning 403. For a single VDS without a CDN this is not critical, but with a serious volume of traffic from blocked countries it makes more sense not to let it reach the server at all.

If Cloudflare is already in front of the site, blocking by country is more logically set up there — it is covered in the article on Cloudflare firewall rules. For sites without a CDN, geoip2 in nginx remains a working option without extra services.

Checklist before enabling it

Before turning on country blocking in production, it is worth going through the following points — a mistake in the rule cuts off not abuse, but part of the real visitors.

  • Checked that the IPs of real site users, including customers traveling or on a VPN, do not end up in the blocked country list by mistake.
  • The GeoLite2 database is updated regularly — a stale database misidentifies the country for new subnets.
  • The rule is tested with curl using a header that simulates an IP from the target country before it is turned on for real traffic.
  • An exception is made for API calls and webhooks from payment systems if their servers are physically located in a blocked region.
  • Nginx logs record the country code in a separate variable, so it is possible to later check how many requests the rule actually cut off.

Frequently asked questions

Can a single region or city be blocked instead of a whole country?

Yes, if the GeoLite2-City database is used instead of GeoLite2-Country — it provides variables with the city and region name, but the accuracy of city detection is noticeably lower than country detection, especially for mobile and corporate subnets.

What to do if a legitimate client reports a 403 block?

Check their real IP and country code against the GeoLite2 database by hand — sometimes a provider routes traffic through a node in another country, and the block triggers by mistake. In that situation it is simpler to add their IP to a separate allowlist than to remove the rule entirely.

Does nginx need to be restarted after the GeoLite2 database is updated?

No, if auto_reload with an interval is set in the geoip2 block — nginx picks up the new database file on schedule by itself. Without this directive, changes only apply after the configuration is reloaded.

Was this article helpful?
← Back to Knowledge Base Ask Support