Nginx IP Geolocation Module for IPGeolocation.io


Overview

Add country, city, ASN, abuse contact, company, and VPN, proxy and Tor detection data to Nginx as native variables, powered by IPGeolocation.io MMDB databases. This Nginx IP geolocation module reads any IPGeolocation.io DB (.mmdb) file directly from disk and exposes the data as $ip_* variables you can use anywhere in your nginx.conf, with no API calls, no network latency, and no per-request cost.

Use it to block anonymous traffic, redirect visitors by country, enforce geo-blocking and compliance rules, personalize content, and enrich your access logs, all at the edge before requests ever reach your application.

# Block Tor exit nodes and known attackers, redirect EU visitors, all at the edge
if ($ip_is_tor) { return 403; }
if ($ip_is_known_attacker) { return 403; }
if ($ip_country_code = DE) { return 302 https://de.example.com$request_uri; }

Why use this module

Most teams reach for IP intelligence in one of two ways: calling a remote API on every request, or wiring a geolocation library into their application code. Both add latency, cost, and complexity. This module takes a third path. It loads IPGeolocation.io databases into Nginx memory once at startup and answers every lookup locally in microseconds.

  • No API calls and no per-request billing. Lookups happen in process against a memory-mapped file. There is no outbound request, so there is nothing to rate limit and nothing to pay per lookup.
  • Decisions at the edge. Block, redirect, or route traffic in nginx.conf before it reaches your backend. Your application never has to know the visitor's country or risk level unless you choose to forward it as request headers.
  • Load only what you need. Point the module at a single database or at several. If you load only a Security database, only the security variables resolve. Load a full bundle and everything resolves. You pay in memory only for the data you actually use.
  • One module, many databases. IP Location, IP Security, IP Company, IP to ASN, and IP Abuse Contact databases all work through the same directive and the same variable names.
  • Schema resilient. Each variable knows several candidate field paths. The current nested IPGeolocation.io schema is tried first, the older flat schema second, so the same configuration keeps working across database versions.

How it works

You load one or more .mmdb files with the ipgeolocation_db directive. The module opens each file at startup and keeps it memory-mapped for the life of the worker process.

When Nginx evaluates an $ip_* variable during a request, the module:

  1. Resolves the client IP address (see how the client IP is selected).
  2. Looks that IP up in each loaded database, in the order you declared them.
  3. Returns the first database that contains the requested field. This is why loading order is also priority order.
  4. If no loaded database has the field, the variable resolves to an empty value.

Because the first match wins, you can layer databases. For example, load a Geolocation database and a Security database together, and $ip_country_name resolves from the first while $ip_is_vpn resolves from the second, automatically.


Requirements

  • Nginx source that matches the binary you intend to run. This module is compiled into Nginx, so you build Nginx with the module attached.
  • libmaxminddb development headers and library. This is the official MMDB reader that the module links against.
  • A C compiler toolchain (gcc or clang, make).
  • At least one IPGeolocation.io .mmdb database file. See which databases provide which variables.

Install libmaxminddb on common systems:

# Debian and Ubuntu
sudo apt-get update
sudo apt-get install -y libmaxminddb0 libmaxminddb-dev mmdb-bin

# RHEL, CentOS, Rocky, Alma (EPEL)
sudo yum install -y libmaxminddb libmaxminddb-devel

# macOS (Homebrew)
brew install libmaxminddb

The mmdbio package provided by IPGeolocation.io is handy for inspecting a database and confirming which fields it contains.


Installation (build from source)


1. Get the module source and config file

git clone https://github.com/IPGeolocation/ngx_http_ipgeolocation_module.git

The module directory must contain the C source and a standard Nginx config file. If your copy does not already include the config file, create one next to the .c source with this content:

ngx_addon_name=ngx_http_ipgeolocation_module

if test -n "$ngx_module_link"; then
    ngx_module_type=HTTP
    ngx_module_name=ngx_http_ipgeolocation_module
    ngx_module_srcs="$ngx_addon_dir/ngx_http_ipgeolocation_module.c"
    ngx_module_libs="-lmaxminddb"
    . auto/module
else
    HTTP_MODULES="$HTTP_MODULES ngx_http_ipgeolocation_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_ipgeolocation_module.c"
    CORE_LIBS="$CORE_LIBS -lmaxminddb"
fi

This config file links libmaxminddb for you and supports both static and dynamic builds, so you do not have to pass the library by hand on the configure line.


2. Get the matching Nginx source

Download the source tarball for your version from the official Nginx download page:

# Replace x.y.z with the version that matches your installed Nginx
wget https://nginx.org/download/nginx-x.y.z.tar.gz
tar -xzf nginx-x.y.z.tar.gz
cd nginx-x.y.z

To find the version and build flags of an Nginx you already run, use nginx -V. Reuse those same configure arguments so your rebuilt binary stays compatible with your existing setup.


3. Build as

I. A static module (compiled into Nginx)

./configure --add-module=/path/to/ngx_http_ipgeolocation_module
make
sudo make install

Only make install needs sudo. If libmaxminddb is installed outside the default search paths (for example Homebrew on Apple Silicon), point the compiler at it:

./configure --add-module=/path/to/ngx_http_ipgeolocation_module \
  --with-cc-opt='-I/opt/homebrew/include' \
  --with-ld-opt='-L/opt/homebrew/lib'

II. As a dynamic module (optional)

If you prefer to load the module at runtime instead of compiling it in, build it dynamically. The Nginx source version must exactly match the Nginx binary that will load the module; otherwise, Nginx rejects it as "not binary compatible".

./configure --add-dynamic-module=/path/to/ngx_http_ipgeolocation_module --with-compat
make modules
sudo cp objs/ngx_http_ipgeolocation_module.so /etc/nginx/modules/

Then load it at the very top of nginx.conf, before the events block:

load_module modules/ngx_http_ipgeolocation_module.so;

4. Verify the build

# Static build: the module appears in the configure arguments
nginx -V 2>&1 | tr ' ' '\n' | grep -i ipgeolocation

# Dynamic build: confirm the load_module line is present, then check syntax
grep -n ipgeolocation /etc/nginx/nginx.conf

# Both: config syntax check
nginx -t

Quick start

Add the module configuration to the http block of your nginx.conf:

http {
    # Load one or more IPGeolocation.io databases. Declaration order is priority order.
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-location.mmdb;
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-security.mmdb;

    # Nginx faces the internet directly, so geolocate the real peer address
    ipgeolocation_trust_forwarded_header off;

    server {
        listen 80;
        server_name example.com;

        location / {
            # Pass enrichment to your backend as request headers
            proxy_set_header X-Geo-Country      $ip_country_code;
            proxy_set_header X-Geo-City         $ip_city_name;
            proxy_set_header X-Geo-Threat-Score $ip_threat_score;
            proxy_pass http://backend;
        }
    }
}

That is the whole setup. Every variable in the full $ip_* variable reference is now available. If Nginx runs behind a load balancer, read how to select the correct client IP before going live.


Testing your configuration

Add a debug endpoint that prints the resolved values, restricted to internal addresses:

# In the http block. Later examples reuse $is_internal.
geo $is_internal {
    default         0;
    127.0.0.1/32    1;
    ::1/128         1;
    10.0.0.0/8      1;
    172.16.0.0/12   1;
    192.168.0.0/16  1;
}

server {
    location = /debug/ipgeo {
        # `return` runs before allow/deny, so gate it with a variable instead
        if ($is_internal = 0) { return 403; }

        default_type text/plain;
        return 200 "country=$ip_country_code city=$ip_city_name asn=$ip_asn vpn=$ip_is_vpn tor=$ip_is_tor threat=$ip_threat_score\n";
    }
}

With ipgeolocation_trust_forwarded_header on, you can simulate any visitor from the server itself:

curl -H "X-Forwarded-For: 2.56.188.34" http://127.0.0.1/debug/ipgeo

Cross-check the output against the raw database record with mmdbio read --db <db> --ip 2.56.188.34.


Configuration directives


1. ipgeolocation_db

Syntax:   ipgeolocation_db <path-to-mmdb-file>;
Context:  http
Default:  none

Loads an IPGeolocation.io MMDB file and keeps it in memory. You can specify this directive more than once. Each database is queried in the order it was declared, and the first database that contains a requested field wins (see how database priority order works). Load only the databases whose data you need.

http {
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-location.mmdb;
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-company.mmdb;
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-asn.mmdb;
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-security.mmdb;
    ipgeolocation_db /etc/nginx/ipgeo/db-ip-abuse.mmdb;
}

If a database fails to open, Nginx refuses to start and logs the reason, so a bad path or a corrupt file is caught immediately rather than failing silently at request time.


2. ipgeolocation_trust_forwarded_header

Syntax:   ipgeolocation_trust_forwarded_header on | off;
Context:  http
Default:  off

Controls which IP address the module geolocates. By default (off), it uses the connection address, including the client address selected by the Nginx realip module. When explicitly set to on, it uses the first address in X-Forwarded-For, falling back to the connection address when the header is absent, empty, or too long.

The directive is only valid in the http block, so one setting applies to every server. We recommend off for most deployments, because clients can spoof X-Forwarded-For. See safe client IP selection behind proxies for details.

http {
    # Recommended for most deployments
    ipgeolocation_trust_forwarded_header off;
}

Variable reference

Every variable below resolves only if a database you loaded contains the matching field. Variables backed by a database you did not load simply return an empty value, so it is safe to reference any of them. See how booleans, lists, and empty values are formatted.


1. Location: country

Requires the IP Geolocation database.

VariableDescriptionExample
$ip_country_codeISO 3166-1 alpha-2 country codeUS
$ip_country_code3ISO 3166-1 alpha-3 country codeUSA
$ip_country_code_iocIOC country codeUSA
$ip_country_nameCountry name (English)United States
$ip_country_name_enLegacy alias of $ip_country_nameUnited States
$ip_country_name_officialOfficial country nameUnited States of America
$ip_country_capitalCapital cityWashington, D.C.
$ip_continent_codeContinent codeNA
$ip_continent_nameContinent nameNorth America
$ip_currency_codeISO 4217 currency codeUSD
$ip_currency_nameCurrency nameUS Dollar
$ip_currency_symbolCurrency symbol$
$ip_calling_codeInternational dialing code+1
$ip_languagesLanguages for the countryen-US,es-US
$ip_tldCountry top level domain.us

2. Location: state, district, city

VariableDescriptionExample
$ip_state_codeState or province codeUS-PA
$ip_state_nameState or province namePennsylvania
$ip_district_nameDistrict or county namePhiladelphia County
$ip_city_nameCity namePhiladelphia
$ip_city_name_enLegacy alias of $ip_city_namePhiladelphia

3. Location: postal, coordinates, time zone

$ip_accuracy_radius and $ip_confidence are only available in the Geo Advanced database tier.

VariableDescriptionExample
$ip_zip_codePostal or ZIP code19102
$ip_latitudeLatitude in decimal degrees39.952580
$ip_longitudeLongitude in decimal degrees-75.165220
$ip_geoname_idGeoNames place ID9849057
$ip_time_zoneTime zone nameAmerica/New_York
$ip_accuracy_radiusAccuracy radius in km (Geo Advanced tier)9.148
$ip_confidenceLocation confidence: low, medium, high (Geo Advanced tier)high

4. Company and ISP

Requires the IP Company database. Field details are in the IP Company database schema reference.

VariableDescriptionExample
$ip_company_nameCompany or ISP nameTele2 Sverige AB
$ip_company_domainCompany domaintele2.com
$ip_company_typeCompany type (for example ISP, HOSTING, BUSINESS)ISP
$ip_isp_nameISP name (mapped onto company name)Tele2 Sverige AB
$ip_organization_nameOrganization (falls back to ASN organization)Tele2 Sverige AB

5. ASN

Requires the IP to ASN database. Field details are in the IP to ASN extended database schema.

VariableDescriptionExample
$ip_asnAutonomous System NumberAS1257
$ip_asn_organizationASN organizationTele2 Sverige AB
$ip_asn_countryASN country codeSE
$ip_asn_domainASN domaintele2.com
$ip_asn_typeASN type (for example ISP, HOSTING)ISP
$ip_asn_rirRegional Internet RegistryRIPE
$ip_asn_date_allocatedASN allocation date2002-09-19

6. Security and threat intelligence

Requires the IP Security database. Field details are in the IP Security database fields and schema.

Boolean flags resolve to 1 or 0 so you can test them directly in if blocks and map directives. Provider lists (for example, proxy and VPN provider names) are joined into a single comma-separated value.

VariableDescriptionExample
$ip_threat_scoreOverall risk score, 0 to 10080
$ip_is_tor1 if the IP is a Tor exit node1
$ip_is_proxy1 if the IP is a proxy1
$ip_is_vpn1 if the IP is a VPN1
$ip_is_relay1 if the IP is a relay (for example, iCloud Private Relay)0
$ip_is_residential_proxy1 if the IP is a residential proxy1
$ip_is_anonymous1 if the IP is anonymized1
$ip_is_known_attacker1 if the IP is a known attacker1
$ip_is_bot1 if the IP is a known bot0
$ip_is_spam1 if the IP is a known spam source0
$ip_is_cloud_provider1 if the IP belongs to a cloud provider1
$ip_cloud_providerCloud provider namePackethub S.A.
$ip_proxy_providerProxy provider names, comma separatedZyte Proxy
$ip_vpn_providerVPN provider names, comma separatedNord VPN
$ip_relay_providerRelay provider name
$ip_proxy_confidenceProxy confidence score80
$ip_vpn_confidenceVPN confidence score80
$ip_proxy_last_seenDate the IP was last seen as a proxy2025-12-12
$ip_vpn_last_seenDate the IP was last seen as a VPN2026-01-19
$ip_bot_confidence_scoreConfidence score for detected bot activity, 0 to 10095
$ip_bot_operator_nameName of the bot operator, when availableChatGPT
$ip_bot_typeType of detected bot activityai_crawler
$ip_is_known_good_bot1 if the IP belongs to a known good bot, such as a search engine crawler1
$ip_bot_last_seenDate the IP was last seen exhibiting bot activity2026-09-03
$ip_is_corporate_gateway1 if the IP belongs to a corporate gateway1
$ip_corporate_gateway_typeType of corporate gatewaysecure_web_gateway
$ip_corporate_gateway_provider_nameName of the corporate gateway providerZscaler

7. Residential proxy

Requires the residential proxy detection database. Field details are in the Residential proxy database schema reference.

VariableDescriptionExample
$ip_residential_proxy_provider_nameResidential proxy provider nameEvomi Proxy
$ip_residential_proxy_last_seenDate last seen as a residential proxy2026-05-23

8. Hosting

Requires the IP hosting provider database. Field details are in the IP Hosting database schema reference.

VariableDescriptionExample
$ip_hosting_providerHosting provider nameAmazon

9. Abuse contact

Requires the IP Abuse Contact database. Field details are in the IP Abuse Contact database field reference.

VariableDescriptionExample
$ip_abuse_nameAbuse contact nameSwipnet Staff
$ip_abuse_country_codeAbuse contact countrySE
$ip_abuse_addressAbuse contact postal address
$ip_abuse_emailAbuse contact emailabuse@tele2.com
$ip_abuse_phoneAbuse contact phone+46 8 5626 42 10
$ip_abuse_kindContact kind (for example group, person)group
$ip_abuse_routeNetwork route or CIDR91.128.0.0/14

Real world examples

The snippets below assume the relevant database is loaded in your http block (see where to download each database). map, geo, limit_req_zone, and log_format belong in the http block; if and return go inside server or location. Mix and match them freely.

Using if only to return a response is safe. For anything more complex, compute a flag with map and test that flag, as the examples below do.


1. Block anonymous and malicious traffic (security)

Stop Tor exit nodes, known attackers, and high-risk IPs before they reach your application. This is useful in front of login pages, checkout flows, signup forms, and admin panels.

# Requires the IP Security database
map "$ip_is_tor$ip_is_known_attacker" $block_anon {
    default 0;
    "~1"    1;   # block if either flag is set
}

map $ip_threat_score $high_risk {
    default                  0;
    "~^(8[0-9]|9[0-9]|100)$" 1;   # threat score 80 to 100
}

server {
    location /login {
        if ($block_anon) { return 403; }
        if ($high_risk)  { return 429; }

        proxy_pass http://app;
    }
}

Avoid hard blocking $ip_is_relay, $ip_is_anonymous, or $ip_is_corporate_gateway on their own. iCloud Private Relay users and employees behind gateways such as Zscaler are usually legitimate customers, so challenge or rate limit them instead of returning 403.


2. Redirect and personalize by country (geo personalization)

Send visitors to the right regional site, set the correct currency, or serve localized content.

# Requires the IP Geolocation database
map $ip_country_code $regional_host {
    default  www.example.com;
    DE       de.example.com;
    FR       fr.example.com;
    JP       jp.example.com;
}

# Only the global site redirects, so regional sites can never loop
server {
    server_name www.example.com;

    location / {
        if ($regional_host != $host) {
            return 302 https://$regional_host$request_uri;
        }

        # Or pass locale hints to your app instead of redirecting
        proxy_set_header X-Geo-Country  $ip_country_code;
        proxy_set_header X-Geo-Currency $ip_currency_code;
        proxy_pass http://app;
    }
}

3. Geo-blocking and compliance (access control)

Restrict access to specific countries for licensing, sanctions, or regulatory reasons. The example allows two countries, always allows internal traffic (health checks, monitoring), and blocks the rest. It reuses $is_internal from Testing your configuration.

# Requires the IP Geolocation database
map "$is_internal:$ip_country_code" $country_allowed {
    default        0;
    "~^1:"         1;   # private and internal addresses have no country
    "~:(US|CA)$"   1;   # allowed countries
}

server {
    location / {
        if ($country_allowed = 0) { return 451; }   # 451: Unavailable For Legal Reasons
        proxy_pass http://app;
    }
}

If search visibility matters, you can also allow verified crawlers with $ip_is_known_good_bot from the IP Security database.


4. Separate humans from infrastructure (bot and datacenter filtering)

Cloud and hosting IPs rarely belong to real human visitors. Flag them so you can rate limit harder, skip ad rendering, or serve a lighter page.

# Requires the IP Security database (cloud flag) or the IP to ASN database (ASN type)
map "$ip_is_cloud_provider$ip_asn_type" $is_infra {
    default     0;
    "~1"        1;   # cloud provider flag set
    "~HOSTING"  1;   # ASN classified as hosting
}

# Human traffic gets an empty key, and requests with an empty key are not limited
map $is_infra $infra_limit_key {
    0  "";
    1  $binary_remote_addr;
}

limit_req_zone $infra_limit_key zone=infra:10m rate=30r/m;

server {
    location /api/ {
        limit_req zone=infra burst=10 nodelay;
        proxy_set_header X-Is-Infra $is_infra;
        proxy_pass http://api;
    }
}

See the Nginx limit_req module documentation for burst and delay tuning.


5. Enrich access logs for analytics and fraud review (observability)

Attach geolocation, network, and risk data to every log line. This makes fraud analysis, abuse triage, and traffic reporting far easier, with no changes to your application. escape=json keeps city and organization names with quotes or non-ASCII characters from breaking the log format.

log_format geo_json escape=json '{'
    '"time":"$time_iso8601","ip":"$remote_addr","request":"$request","status":"$status",'
    '"country":"$ip_country_code","city":"$ip_city_name",'
    '"asn":"$ip_asn","org":"$ip_asn_organization",'
    '"vpn":"$ip_is_vpn","proxy":"$ip_is_proxy","tor":"$ip_is_tor",'
    '"threat":"$ip_threat_score"}';

server {
    access_log /var/log/nginx/access.geo.log geo_json;
}

IP addresses combined with city-level location are personal data under regulations such as GDPR, so apply your normal log retention and access policies. More options are in the Nginx log_format documentation.


6. Surface the abuse contact for an offending IP (incident response)

When you spot abuse in your logs, the abuse contact data tells you exactly where to send a report, without a manual WHOIS lookup. This is a debug endpoint: it is restricted to internal addresses, and values are not JSON-escaped.

# Requires the IP Abuse Contact database
location = /debug/abuse {
    if ($is_internal = 0) { return 403; }

    default_type application/json;
    return 200 '{"ip_route":"$ip_abuse_route","abuse_email":"$ip_abuse_email","abuse_phone":"$ip_abuse_phone","country":"$ip_abuse_country_code"}';
}

How values are formatted

  • Booleans become 1 or 0. Whether the database stores a true boolean or the strings "true" and "false", the module normalizes both to 1 and 0. This means $ip_is_vpn, $ip_is_tor, and the other flags work directly in if tests and map blocks.
  • Lists are joined with ", ". Array fields such as proxy and VPN provider names are returned as a single comma-separated string.
  • Numbers are rendered as text. Scores and coordinates come back as plain strings, for example 80 or -75.165220, which is exactly what Nginx variables expect.
  • Empty when not found. If no loaded database contains a field for the client IP, the variable resolves to an empty value. Reference any variable safely, even for databases you did not load.
  • Not cached per request. These variables are evaluated each time they are used, so they always reflect the current request's client IP.

Client IP selection and X-Forwarded-For

The module geolocates a single IP address per request. Which one it uses depends on ipgeolocation_trust_forwarded_header.

  • Default (off): the module uses the connection address, including the client address selected by the Nginx realip module. It does not read X-Forwarded-For itself.
  • Explicit on: the module reads the first (leftmost) address in X-Forwarded-For, trimming surrounding spaces. If the header is absent, empty, or too long, it falls back to the connection address. Use this option only when your proxy overwrites the header.

A safe setup behind a load balancer:

http {
    ipgeolocation_trust_forwarded_header off;

    set_real_ip_from 10.0.0.0/8;          # your load balancer range
    real_ip_header   X-Forwarded-For;
    real_ip_recursive on;
}

Getting the databases

This module reads IPGeolocation.io MMDB files. You download the databases from your IPGeolocation.io account and point the module at the files. The static download links you receive do not change, so the same paths work for your first download and for every update.

Relevant databases and their documentation:

You can buy a single database, a ready-made bundle (for example IP to Location plus Company plus ASN plus Abuse), or a custom build. Compare IP database pricing and bundles.

After downloading, confirm which fields a file contains with mmdbio:

mmdbio read --db /etc/nginx/ipgeo/db-ip-security.mmdb --ip 2.56.188.34

Updating the databases

IPGeolocation.io publishes refreshed databases daily. Replace the files on disk, then reload Nginx so workers open the new versions.

The script needs curl, openssl, unzip, sha256sum and mmdbio. Before the first run, save the public key from your Public Key URL as /etc/nginx/ipgeo/public-key.pem.

#!/bin/sh
set -eu
# cron runs with a minimal PATH, so make sure nginx and mmdbio can be found.
export PATH="/usr/local/nginx/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
DIR=/etc/nginx/ipgeo
DB=db-ip-security.mmdb                              # the .mmdb file inside the archive
ARCHIVE_URL="https://your-database-archive-url"    # from your IPGeolocation.io account
SIGNATURE_URL="https://your-signature-file-url"    # the archive URL with downloadSignature=true
PUBLIC_KEY="$DIR/public-key.pem"                   # saved once from your Public Key URL

# Work inside $DIR so the final mv stays on one filesystem, which makes it atomic.
WORK=$(mktemp -d "$DIR/.update.XXXXXX")
trap 'rm -rf "$WORK"' EXIT

# 1. Download the release archive (a ZIP) and its signature, then check the signature.
curl -fsSL "$ARCHIVE_URL" -o "$WORK/release.zip"
curl -fsSL "$SIGNATURE_URL" -o "$WORK/release.sig"
openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$WORK/release.sig" "$WORK/release.zip"

# 2. Extract the archive and check each file against the SHA-256 sums in checksum.txt.
mkdir "$WORK/release"
unzip -q "$WORK/release.zip" -d "$WORK/release"
(cd "$WORK/release" && sha256sum -c checksum.txt)

# 3. Confirm the extracted file opens as an MMDB database.
mmdbio verify --db "$WORK/release/$DB"

# 4. Keep the current file (a hard link, so no copy), then swap the new one in.
if [ -f "$DIR/$DB" ]; then ln "$DIR/$DB" "$WORK/previous.mmdb"; fi
mv "$WORK/release/$DB" "$DIR/$DB"

# 5. Reload Nginx. If the config test or the reload fails, put the previous file back.
if ! { nginx -t && nginx -s reload; }; then
  if [ -f "$WORK/previous.mmdb" ]; then
    mv "$WORK/previous.mmdb" "$DIR/$DB"
    echo "nginx -t or the reload failed; the previous $DB was restored." >&2
  fi
  exit 1
fi

Schedule it with cron or a systemd timer, for example, daily at 04:00: 0 4 * * * /usr/local/bin/update-ipgeo.sh.

To update more than one database, run a copy of the script for each archive with its own DB, ARCHIVE_URL and SIGNATURE_URL.


Troubleshooting

Nginx fails to start with "failed to open" for a database.

Check that the path in ipgeolocation_db is correct, the file exists, and the Nginx worker user can read it. The module logs the exact error to help you diagnose it.

A variable is always empty.

Confirm you loaded a database that actually contains that field. Security variables need a Security database, ASN variables need an ASN database, and so on (see which databases provide each variable). Use mmdbio read to inspect the file. Also confirm the client IP is what you expect (see how the client IP is selected).

Variables are empty for local or private IPs.

Private and reserved IP ranges (for example 10.0.0.0/8 or 192.168.0.0/16) are not in any public geolocation database, so they resolve to empty. This is expected for internal or test traffic. To test real lookups locally, follow Testing your configuration.

Visitors behind a proxy all look like one IP.

Nginx is seeing the proxy's address. Use the realip setup for load balancers with ipgeolocation_trust_forwarded_header off, or keep it on only if your proxy overwrites X-Forwarded-For.

Nginx reports the module "is not binary compatible".

The dynamic module was built against a different Nginx version or without --with-compat. Rebuild it from the exact source version shown by nginx -v (see building as a dynamic module).

The build cannot find maxminddb.h or -lmaxminddb.

Install the libmaxminddb development package (see Requirements). The config file in Installation links the library automatically; for non-standard install paths, pass --with-cc-opt and --with-ld-opt as shown in the static build step.


Frequently Asked Questions

No. It reads local .mmdb database files and does not make any outbound requests. There are no per-request costs. To update the data, replace the database files safely and reload Nginx.
Both modules read MMDB files through libmaxminddb. However, this module is built specifically for IPGeolocation.io databases. It provides ready-to-use $ip_* variables for location, company, ASN, security, and abuse data, normalizes boolean values to 1 and 0, joins provider lists, and automatically supports multiple schema versions without requiring manual geoip2 field mappings.
Yes. You can declare ipgeolocation_db once for each database file. Databases are checked in declaration order, and the first one containing the requested field is used (see how database priority order works).
No. Load only the databases you need. Variables associated with databases that are not loaded simply return empty values, making it safe to reference them in your configuration.
Yes. IPGeolocation.io MMDB databases support both IPv4 and IPv6 addresses, and the module automatically performs lookups using whichever address the client provides. Make sure your server also listens on IPv6 (for example, listen [::]:80;).
IPGeolocation.io publishes refreshed databases daily. Download the new file, swap it in atomically with mv, and reload Nginx. See update databases without downtime for a ready-to-use script.
Yes. Use proxy_set_header to pass geolocation headers to your backend, allowing it to receive geolocation and risk information without performing its own database lookups.

License

See the LICENSE file for details.

Built for IPGeolocation.io databases. Questions about the data, tiers, or bundles are answered in the IPGeolocation.io database documentation and on the IP database pricing page.

Subscribe to Our Newsletter

Get the latest in geolocation tech, straight to your inbox.