GeoIP в Nginx обычно нужен не ради «красивых отчётов», а для прикладных задач: региональные правила, антифрод, маршрутизация на разные upstream, условные редиректы, метки в логах для аналитики. В 2025 году почти всегда речь идёт о модуле ngx_http_geoip2_module и базах MaxMind в формате .mmdb (GeoLite2/GeoIP2).
Ниже — рабочая схема: проверяем наличие GeoIP2-модуля, легально скачиваем GeoLite2 через MaxMind, подключаем базы в Nginx, заводим переменные (страна/город/ASN) и настраиваем регулярное обновление через systemd timer так, чтобы базы заменялись атомарно, а reload проходил без простоя.
Что важно знать про MaxMind и GeoLite2 до настройки
MaxMind распространяет бесплатные базы GeoLite2 по лицензии EULA: скачивание идёт по персональному License Key. «Просто скачать по прямой ссылке» нельзя, поэтому для автоматизации обновлений ключ нужно хранить отдельно (желательно файл с правами 600) и регулярно обновлять базы.
Чаще всего используют такие базы:
GeoLite2-Country — страна (обычно достаточно для большинства правил).
GeoLite2-City — город/регион/координаты (тяжелее, но даёт больше деталей).
GeoLite2-ASN — ASN и организация провайдера (полезно для антибот/антифрод и сегментации).
Нюанс: GeoIP — вероятностная оценка по IP. Для легитимных пользователей иногда будут «ошибочные» страны (мобильные сети, VPN, CGNAT). Поэтому жёсткие блокировки по гео лучше избегать: чаще практичнее мягкие ограничения (лимит, доп. проверка), чем вечный 403.
Проверяем, есть ли GeoIP2 в вашем Nginx
Сначала выясните, собран ли модуль GeoIP2 в текущий Nginx. Это зависит от способа установки (пакеты дистрибутива, официальный репозиторий, сборка из исходников).
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'geoip|geoip2'
Если увидели упоминания geoip2, это хороший знак. Важно не перепутать со старым http_geoip_module (legacy, формат .dat): для .mmdb нужен именно GeoIP2.
Варианты установки
Динамический модуль из пакета: в ряде дистрибутивов GeoIP2 идёт отдельным пакетом; тогда в конфиге будет
load_module.Статическая сборка: если у вас кастомная сборка Nginx, GeoIP2 подключают при компиляции.
Дальше конфигурация одинаковая: если модуль динамический — добавите load_module, если статический — пропустите этот шаг.

Устанавливаем зависимости и GeoIP2-модуль (пример для Debian/Ubuntu)
Обычно нужна библиотека для чтения mmdb. Если GeoIP2 доступен как пакетный модуль для вашего Nginx — ставьте его отдельно, но названия пакетов зависят от репозитория.
apt update
apt install -y libmaxminddb0 libmaxminddb-dev
Проверить, есть ли модуль GeoIP2 в репозитории:
apt install -y nginx
apt-cache search geoip2 | grep -i nginx
Если подходящего пакета нет, практичный вариант — собрать Nginx с ngx_http_geoip2_module. Но сборку лучше оформлять отдельной инструкцией под вашу версию и ABI. В этой статье фокус — эксплуатация: подключение .mmdb и обновления без простоя.
Готовим каталоги и права под базы .mmdb
Храните базы в отдельном каталоге, который логично воспринимать как обновляемые данные, например /var/lib/geoip:
install -d -m 0755 /var/lib/geoip
Обновление лучше делать от root, а Nginx — только читать. Поэтому достаточно прав 0644 на файлы и 0755 на каталог. Главное — обновлять базы атомарно (через замену файла), чтобы Nginx не увидел «полфайла».
Скачиваем GeoLite2 через MaxMind легально (License Key)
Ключ удобнее хранить в отдельном файле с жёсткими правами и не светить его в истории shell и логах:
install -d -m 0700 /etc/maxmind
bash -lc 'umask 077; printf "%s" "YOUR_MAXMIND_LICENSE_KEY" > /etc/maxmind/license_key'
Далее можно использовать прямую загрузку архивов GeoLite2, распаковку и копирование .mmdb в нужный каталог. Это проще, чем тонко настраивать geoipupdate, и лучше контролируется в проде.
Скрипт обновления баз с атомарной заменой и безопасным reload
Скрипт ниже делает следующее: скачивает архивы, извлекает .mmdb во временный каталог, проверяет, что файл не пустой, заменяет целевой файл через mv (атомарно в пределах одного filesystem), затем выполняет nginx -t и только после успешной проверки — reload.
install -d -m 0755 /usr/local/sbin
cat > /usr/local/sbin/update-geolite2-mmdb <<'EOF'
#!/bin/sh
set -eu
LICENSE_FILE="/etc/maxmind/license_key"
DBDIR="/var/lib/geoip"
TMPBASE="/var/tmp/geolite2"
if [ ! -s "$LICENSE_FILE" ]; then
echo "License key file is missing or empty: $LICENSE_FILE" 1>&2
exit 1
fi
LICENSE_KEY="$(cat "$LICENSE_FILE")"
download_and_install() {
EDITION="$1"
TARGET="$2"
mkdir -p "$TMPBASE"
WORKDIR="$(mktemp -d "$TMPBASE.XXXXXX")"
ARCHIVE="$WORKDIR/${EDITION}.tar.gz"
cleanup() {
rm -rf "$WORKDIR"
}
trap cleanup EXIT
curl -fsSL -o "$ARCHIVE" "https://download.maxmind.com/app/geoip_download?edition_id=${EDITION}&license_key=${LICENSE_KEY}&suffix=tar.gz"
tar -xzf "$ARCHIVE" -C "$WORKDIR"
MMDB_PATH="$(find "$WORKDIR" -type f -name '*.mmdb' | head -n 1)"
if [ -z "$MMDB_PATH" ]; then
echo "No .mmdb found in archive for $EDITION" 1>&2
exit 1
fi
if [ ! -s "$MMDB_PATH" ]; then
echo "Downloaded mmdb is empty for $EDITION" 1>&2
exit 1
fi
install -d -m 0755 "$DBDIR"
install -m 0644 "$MMDB_PATH" "$DBDIR/${TARGET}.new"
mv -f "$DBDIR/${TARGET}.new" "$DBDIR/${TARGET}"
}
download_and_install "GeoLite2-Country" "GeoLite2-Country.mmdb"
download_and_install "GeoLite2-City" "GeoLite2-City.mmdb"
download_and_install "GeoLite2-ASN" "GeoLite2-ASN.mmdb"
nginx -t
systemctl reload nginx
EOF
chmod 0755 /usr/local/sbin/update-geolite2-mmdb
Reload перечитывает конфиг и переинициализирует модуль. Новые воркеры начнут использовать обновлённые базы, а старые доработают текущие соединения на прежней версии — это ожидаемое поведение graceful reload без простоя.
Подключаем GeoIP2 в конфиг Nginx
Дальше подключаем базы и заводим переменные (страна/город/ASN), а затем добавляем их в access_log. Удобнее вынести это в отдельный файл в вашей схеме (conf.d, modules-enabled и т. п.).
Если модуль динамический: load_module
Если GeoIP2 установлен как динамический модуль, добавьте строку в начале основного конфига (main context, до events/http):
load_module modules/ngx_http_geoip2_module.so;
Если модуль статический, эта строка не нужна.
HTTP-конфигурация: базы и переменные
http {
geoip2 /var/lib/geoip/GeoLite2-Country.mmdb {
$geoip2_country_code country iso_code;
$geoip2_country_name country names en;
}
geoip2 /var/lib/geoip/GeoLite2-City.mmdb {
$geoip2_city_name city names en;
$geoip2_region_name subdivisions 0 names en;
}
geoip2 /var/lib/geoip/GeoLite2-ASN.mmdb {
$geoip2_asn_number autonomous_system_number;
$geoip2_asn_org autonomous_system_organization;
}
log_format main_geoip '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'cc=$geoip2_country_code city="$geoip2_city_name" '
'asn=$geoip2_asn_number asn_org="$geoip2_asn_org"';
access_log /var/log/nginx/access.log main_geoip;
}
Что стоит учитывать в проде:
Локали: в примере
names en. Можно выбрать другой язык, но покрытие по полям/локалям различается.subdivisions 0— «первый уровень региона» (область/штат), часто бывает пустым.Переменные могут быть пустыми для части IP (частные диапазоны, некоторые новые сети) — это нормально.
Если вы параллельно оптимизируете отдачу больших ответов и кэширование, полезно свериться с материалом про диапазонные запросы: как работает HTTP Range в Nginx/Apache и почему это важно для кэша.
Практические сценарии: ограничения и маршрутизация
GeoIP2 удобно использовать вместе с map, чтобы не плодить if в server/location. Ниже два типовых сценария.
Мягкое ограничение по странам (без жёсткой блокировки)
map $geoip2_country_code $geo_soft_limit {
default 0;
RU 0;
KZ 0;
BY 0;
CN 1;
BR 1;
}
server {
location / {
if ($geo_soft_limit) {
add_header Retry-After "3600" always;
return 429;
}
proxy_pass http://app_backend;
}
}
Почему часто выбирают 429 вместо 403: вы не создаёте «вечный бан» на уровне кэшей/прокси и оставляете легитимным пользователям шанс повторить запрос позже. Для API это обычно сочетают с rate limiting.
Маршрутизация на разные upstream по гео
map $geoip2_country_code $geo_upstream {
default app_default;
US app_us;
CA app_us;
DE app_eu;
FR app_eu;
}
upstream app_default { server 10.0.0.10:8080; }
upstream app_us { server 10.0.1.10:8080; }
upstream app_eu { server 10.0.2.10:8080; }
server {
location / {
proxy_pass http://$geo_upstream;
}
}
Механизм простой, но учитывайте «скачки» геоданных и погрешности. Если решение критично для бизнеса, логируйте $geoip2_country_code и сверяйте логику на реальном трафике.
Автообновление через systemd timer (вместо cron)
На серверах с systemd таймер обычно удобнее cron: логирование в journal, понятное управление через systemctl, и возможность «догонять» пропущенные запуски.
Юнит сервиса обновления
cat > /etc/systemd/system/geolite2-update.service <<'EOF'
[Unit]
Description=Update MaxMind GeoLite2 mmdb databases and reload nginx
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-geolite2-mmdb
EOF
Таймер раз в сутки
cat > /etc/systemd/system/geolite2-update.timer <<'EOF'
[Unit]
Description=Daily GeoLite2 update
[Timer]
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now geolite2-update.timer
systemctl list-timers --all | grep -i geolite2
RandomizedDelaySec полезен, если у вас много серверов: они не будут одновременно «ломиться» за обновлениями. Persistent=true означает, что пропущенный запуск выполнится после включения сервера.

Диагностика: как понять, что GeoIP2 реально работает
Быстрый набор проверок:
Проверить конфиг:
nginx -t.Посмотреть
error_logпосле старта/reload: нет ли ошибок чтения mmdb.Сделать запрос и увидеть поля в
access_log(например,cc=,asn=).
nginx -t
systemctl reload nginx
tail -n 50 /var/log/nginx/error.log
tail -n 5 /var/log/nginx/access.log
Если переменные пустые для вашего IP — это может быть нормой. Для теста удобно сделать запрос с VPS в другой стране или с мобильной сети.
Типовые ошибки и как их избежать
1) Nginx не стартует: «unknown directive geoip2»
GeoIP2-модуль не загружен. Проверьте:
правильность
load_moduleи наличие файла.so;совместимость модуля с вашей версией Nginx (ABI);
что это именно GeoIP2, а не legacy GeoIP.
2) Reload проходит, но данные «не обновились»
Частые причины: обновление пишет файл «поверх» (неатомарно), Nginx держит старый дескриптор, или вы обновили базу не по тому пути, который указан в директиве geoip2. Решение — атомарная замена через mv в том же каталоге и затем reload (как в скрипте).
3) Обновление ломается из-за ключа/сети
Скрипт должен быть «безопасным по умолчанию»: если скачивание не удалось, не трогайте старые базы и не делайте reload. При set -e и корректных проверках существующие .mmdb останутся на месте.
Когда имеет смысл выносить GeoIP2 на отдельный слой
Если у вас несколько фронтендов/балансировщиков и много правил, GeoIP2 часто выгоднее держать на edge-узле (первый Nginx/Ingress), а дальше прокидывать нормализованный результат в заголовках к приложению.
Важно: заголовки от клиента нужно игнорировать/перезаписывать, иначе получите spoofing (клиент подменит «свою страну»).
proxy_set_header X-Geo-Country $geoip2_country_code;
proxy_set_header X-Geo-ASN $geoip2_asn_number;
Если гео-логика завязана на домены и вы параллельно планируете переносы/склейки, полезно заранее продумать редиректы и HSTS: миграция домена, 301 и HSTS: как не потерять SEO и не сломать HTTPS.
Итог
Связка nginx + geoip2 + maxmind geolite2 стабильно работает, если выстроить три вещи: правильный модуль, понятную конфигурацию переменных и регулярные обновления баз. Самый «админский» вариант — обновлять через systemd timer и делать безопасный reload: атомарно заменили .mmdb, проверили nginx -t, сделали reload без разрыва соединений.
Дальше можно наращивать пользу: добавлять геометки в логи, вводить мягкие лимиты по странам/ASN, маршрутизировать часть трафика на отдельные upstream и измерять эффект по метрикам.
Для проектов на Fastfox это обычно ставят на фронтенд-Nginx на VDS, а небольшие сайты и панели можно держать на виртуальном хостинге, если GeoIP2 не нужен на уровне веб-сервера.


