Коротко. По умолчанию nginx пишет в /var/log/nginx/access.log и /var/log/nginx/error.log. Реальные пути задают директивы access_log и error_log — найти их можно командой nginx -T | grep -E 'access_log|error_log'. В контейнере логи уходят в stdout и читаются через docker logs. Ротацией занимается logrotate, и она работает только вместе с сигналом USR1 процессу nginx.
Лог nginx — это не «файл, куда что-то капает». Это единственный источник правды о том, что именно сервер отдал клиенту, за какое время и с каким кодом. Если научиться читать его и добавить в формат три-четыре переменные, лог превращается из архива в инструмент диагностики: по нему видно медленные апстримы, ботов, всплески 5xx и битые ссылки. Ниже — где логи лежат, как устроен формат, какие однострочники реально работают и как настроить ротацию так, чтобы диск не забился за неделю.
Где лежат логи nginx и как найти реальный путь
Путь по умолчанию
В пакетных сборках для Debian, Ubuntu, RHEL-подобных систем и в официальных сборках с nginx.org каталог логов один и тот же:
/var/log/nginx/access.log # каждый обслуженный HTTP-запрос
/var/log/nginx/error.log # ошибки воркеров, отказы апстримов, проблемы с правами и TLS
Каталог обычно принадлежит root, а файлы — пользователю, под которым работают воркеры (www-data, nginx), с группой adm или root. Если вы читаете логи не из-под root, добавьте себя в эту группу вместо того, чтобы менять права на 0644 — access.log содержит IP-адреса посетителей.
Почему у конкретного сайта путь другой
Директивы access_log и error_log действуют на уровнях http, server и location, причём вложенный уровень полностью переопределяет внешний. На многосайтовом сервере почти всегда есть отдельные файлы на каждый виртуальный хост, и глобальный /var/log/nginx/access.log при этом остаётся пустым. Не гадайте — спросите у самого nginx:
# полный дамп собранной конфигурации со всеми include
nginx -T | grep -nE 'access_log|error_log'
# то же, но сразу видно, из какого файла пришла директива
nginx -T | grep -nE '^# configuration file|access_log|error_log'
# какие файлы реально открыты рабочим процессом прямо сейчас
sudo ls -l /proc/$(pgrep -o nginx)/fd | grep -i log
nginx -T сначала проверяет синтаксис, а затем печатает итоговый конфиг — с раскрытыми include и комментариями, из какого файла взят каждый блок. Это единственный надёжный способ на сервере, который вы видите впервые. Разбор структуры конфигурации и приоритетов директив — в отдельном материале про конфигурацию nginx.
Логов нет вообще: три причины
Логирование выключено. Где-то в конфиге стоит access_log off; — часто на статике или на health-check-локейшене, но иногда и на уровне server «для производительности». Проверяется тем же nginx -T.
nginx работает в контейнере. В официальном образе /var/log/nginx/access.log — это симлинк на /dev/stdout, а error.log — на /dev/stderr. Внутри контейнера файл пустой по определению, логи забирает драйвер логирования Docker:
docker logs --since 1h --tail 200 nginx
docker logs -f nginx 2>/dev/null # только access (stdout)
docker logs -f nginx 1>/dev/null # только error (stderr)
# где физически лежит json-file лог контейнера
docker inspect --format '{{.LogPath}}' nginx
Права или SELinux. Если nginx не смог открыть файл на запись, он скажет об этом при старте и в journalctl -u nginx. На системах с SELinux каталог логов должен иметь контекст httpd_log_t, иначе запись блокируется молча для приложения, но громко в audit.log.

Прежде чем искать причину проблемы в логе, убедитесь, что вы смотрите тот самый лог. Половина «загадочных» случаев «в логах пусто, а сайт отдаёт 500» — это чтение глобального access.log при включённом per-server логировании.
Формат combined: разбор строки по полям
Формат по умолчанию называется combined и встроен в nginx — объявлять его не нужно. Определение выглядит так:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
Реальная строка:
203.0.113.42 - - [06/Aug/2026:12:41:07 +0300] "GET /articles/nginx-logs-guide HTTP/1.1" 200 18342 "https://example.com/articles" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
Разбор по полям:
203.0.113.42—$remote_addr, адрес того, кто открыл TCP-соединение. За балансировщиком или CDN это будет адрес прокси, а не посетителя.- первый
-—$remote_userпри отсутствии HTTP-аутентификации; исторически на этом месте был identd-логин. - второй
-— пустой$remote_user(логин basic-auth, если он есть). [06/Aug/2026:12:41:07 +0300]—$time_local, момент окончания обработки запроса в локальной таймзоне сервера. Медленный запрос попадает в лог позже, чем начался."GET /articles/nginx-logs-guide HTTP/1.1"—$request: метод, URI с query-строкой и версия протокола, как их прислал клиент.200—$status, итоговый код ответа. Что означает каждый — в справочнике по кодам HTTP-ответов.18342—$body_bytes_sent: только тело, без заголовков. Для подсчёта реального трафика нужен$bytes_sent."https://example.com/articles"—$http_referer, откуда пришёл клиент.- последнее поле —
$http_user_agent, строка клиента как есть.
Ключевое ограничение combined: в нём нет ни времени ответа, ни имени хоста, ни адреса апстрима. То есть по такому логу невозможно ответить на вопрос «почему сайт тормозил в 14:20» — видно только, что запросы были и коды были 200.
Свой log_format: превращаем лог в инструмент диагностики
Пять переменных меняют ценность лога радикально. Все они перечислены в индексе переменных nginx:
$request_time— полное время обработки в секундах с миллисекундами: от чтения первого байта запроса до записи последнего байта ответа в сокет. Включает медленного клиента, поэтому большое значение не всегда означает медленный бэкенд.$upstream_response_time— сколько времени отвечал апстрим. Разница между ним и$request_time— это сеть до клиента, буферизация и работа самого nginx.$upstream_connect_timeи$upstream_header_time— где именно потерялось время: на установлении соединения, на ожидании первого байта заголовков или на передаче тела.$upstream_addr— какой именно бэкенд обслужил запрос. При ретраях здесь будет несколько адресов через запятую — мгновенный признак того, что первый апстрим отвалился.$host— какой виртуальный хост обслужил запрос. Обязателен, если несколько сайтов пишут в один файл.$http_x_forwarded_for— цепочка адресов от прокси. Нужен, пока не настроенreal_ip, и полезен как страховка после.
Рабочий формат, который стоит держать на каждом проксирующем сервере:
# в блоке http {}
log_format ext '$remote_addr "$http_x_forwarded_for" - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'host=$host rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time '
'ua=$upstream_addr us=$upstream_status cache=$upstream_cache_status';
access_log /var/log/nginx/access.log ext;
Имена полей вида rt= и urt= не украшательство: они позволяют вытаскивать значения по имени, а не по номеру колонки, и формат можно расширять, не ломая все накопленные скрипты.
JSON вместо позиционного формата
Если логи уходят в сборщик (Loki, Elasticsearch, ClickHouse), позиционный формат — лишний этап парсинга. В актуальных версиях nginx есть escape=json, который корректно экранирует кавычки и управляющие символы в User-Agent и URI:
log_format json_ext escape=json
'{'
'"time":"$time_iso8601",'
'"host":"$host",'
'"remote_addr":"$remote_addr",'
'"xff":"$http_x_forwarded_for",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes_sent":$bytes_sent,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.json json_ext;
Без escape=json любой User-Agent с кавычкой или обратным слэшем сломает JSON-строку, и сборщик будет молча терять записи. Проверять надо не на своём браузере, а на реальном трафике — ломают формат обычно сканеры.
Буферизация и выборочное логирование
На нагруженном сервере синхронная запись каждой строки — заметная нагрузка на диск. Буфер и сброс по таймеру снимают её почти полностью, ценой потери последних секунд лога при жёстком падении процесса:
access_log /var/log/nginx/access.log ext buffer=64k flush=5s;
# не писать в лог успешные ответы на статику и health-check
map $status $loggable {
~^[23] 0;
default 1;
}
server {
location = /healthz {
access_log off;
return 200 "ok\n";
}
location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|woff2?)$ {
access_log /var/log/nginx/static.log ext if=$loggable;
}
}
Полностью отключать access_log на статике не стоит: именно там видно, какие файлы отдают 404 и кто выкачивает сайт целиком. Лучше вынести статику в отдельный файл. Другие способы снять нагрузку — в материале про тюнинг производительности nginx.

error_log: уровни логирования и типовые сообщения
Директива error_log принимает путь и уровень. Уровни идут по возрастанию критичности, и заданный уровень означает «этот и всё, что серьёзнее»:
debug— полная трассировка обработки каждого запроса, сотни строк на запрос;info— информационные сообщения, включая разрывы соединений клиентом;notice— штатные события вроде перечитывания конфигурации;warn— предупреждения: подобранный буфер маловат, апстрим ответил странно;error— значение по умолчанию: запрос не обслужен как надо;crit,alert,emerg— сервер в аварийном состоянии.
Практический дефолт — warn на проде: он ловит проблемы с буферами и сертификатами до того, как они станут ошибками, и не даёт заметного объёма.
Цена уровня debug
debug — это не «чуть подробнее». На сервере с сотнями запросов в секунду он способен занять десятки гигабайт за считанные минуты и заметно замедлить обработку. Кроме того, он доступен только в сборке с --with-debug. Правильный способ — включать отладку точечно, для одного адреса:
# есть ли поддержка отладки в этой сборке
nginx -V 2>&1 | tr ' ' '\n' | grep -- --with-debug
# в блоке events {} — debug только для одного клиента
events {
debug_connection 203.0.113.42;
}
# отдельный файл под отладку конкретного сайта
server {
error_log /var/log/nginx/example.debug.log debug;
}
Выключайте debug в том же окне обслуживания, в котором включили. Забытый debug-лог — самая частая причина внезапно закончившегося места на диске и самая неприятная утечка: в него попадают заголовки запросов целиком, включая куки и токены.
Типовые сообщения и что они значат
Строка error_log имеет вид дата [уровень] pid#tid: *номер_соединения сообщение, и дальше — контекст: client, server, request, upstream, host. Контекст важнее самого сообщения: он показывает, какой сайт и какой URL пострадали.
connect() failed (111: Connection refused) while connecting to upstream— бэкенд не слушает указанный порт или сокет. Процесс приложения упал либо слушает другой адрес. Наружу это отдаётся как 502.upstream timed out (110: Connection timed out) while reading response header— бэкенд принял соединение, но не успел ответить заproxy_read_timeout. Наружу — 504. Лечится не увеличением таймаута, а поиском медленного запроса.no live upstreams while connecting to upstream— все серверы в блокеupstreamпомечены как недоступные поmax_failsи ждутfail_timeout. Признак того, что бэкенды падали пачкой. Полный разбор — в статье про ошибку 502 Bad Gateway.open() "/var/www/site/index.html" failed (13: Permission denied)— воркеру не хватает прав. Проверяйте не только сам файл, но и право на исполнение (x) на всех родительских каталогах, а на RHEL — контекст SELinux.SSL_do_handshake() failed— не договорились о TLS. Если в контекстеclient— старый клиент или сканер; если в контекстеupstream— проблема сертификата или SNI на бэкенде.client intended to send too large body— превышенclient_max_body_size, наружу уходит 413. Значение по умолчанию — 1 МБ, чего не хватает почти любой форме с загрузкой файлов.upstream sent too big header while reading response header— не хватаетproxy_buffer_size. Обычно виноваты длинные Set-Cookie или отладочные заголовки фреймворка.worker_connections are not enough— упёрлись в лимит соединений на воркер; поднимать вместе сworker_rlimit_nofile.directory index of "/var/www/site/" is forbidden— нет ни одного файла изindex, аautoindexвыключен. Наружу — 403.
Какой лог смотреть в какой ситуации
| Файл или поток | Что в нём | Когда смотреть | Чем грепать |
|---|---|---|---|
/var/log/nginx/access.log | Каждый обслуженный запрос: IP, URI, код, объём, время | Всплеск 4xx/5xx, подозрение на ботов, разбор нагрузки | awk '$9 ~ /^5/' |
/var/log/nginx/error.log | Отказы апстримов, права, TLS, лимиты буферов | 502, 504, 403, белый экран, «сайт лежит» | grep -E 'upstream|denied|SSL' |
Per-server логи из nginx -T | Трафик конкретного виртуального хоста | Многосайтовый сервер, жалоба на один домен | nginx -T | grep access_log |
access.log.1, *.log.*.gz | Ротированные копии за прошлые сутки и недели | Ретроспектива, разбор инцидента задним числом | zgrep, zcat |
/dev/stdout, /dev/stderr | Тот же поток, но в драйвере логов Docker | nginx в контейнере, файл в томе пустой | docker logs --since 1h |
journalctl -u nginx | Сообщения systemd о старте, падении и перезагрузке мастера | nginx не стартует, конфиг не прошёл проверку | journalctl -u nginx -n 50 --no-pager |
Отдельный error_log ... debug | Полная трассировка обработки запроса | Воспроизводимый баг, только временно | grep '*12345 ' по номеру соединения |
Анализ логов nginx: рабочие однострочники
Всё ниже рассчитано на формат combined, где позиции полей фиксированы: $1 — IP, $4 — время, $7 — URI, $9 — код ответа, $10 — объём тела. Если вы добавили поля в начало формата, номера сдвинутся — это ещё один аргумент за именованные поля.
Кто и что запрашивает
L=/var/log/nginx/access.log
# топ-20 IP по числу запросов
awk '{print $1}' $L | sort | uniq -c | sort -rn | head -20
# топ-20 URL
awk '{print $7}' $L | sort | uniq -c | sort -rn | head -20
# распределение кодов ответа
awk '{print $9}' $L | sort | uniq -c | sort -rn
# топ User-Agent (поле между 6-й и 7-й кавычкой)
awk -F'"' '{print $6}' $L | sort | uniq -c | sort -rn | head -20
# уникальные адреса за файл
awk '{print $1}' $L | sort -u | wc -l
# трафик по IP в мегабайтах
awk '{b[$1]+=$10} END {for (i in b) printf "%8.1f MB %s\n", b[i]/1048576, i}' $L \
| sort -rn | head -20
Ошибки и время
# все 5xx с начала текущего часа
awk -v h="$(date '+%d/%b/%Y:%H')" 'index($4, h) && $9 ~ /^5/' $L
# какие URL чаще всего отдают 404
awk '$9 == 404 {print $7}' $L | sort | uniq -c | sort -rn | head -20
# распределение запросов по часам
awk '{print substr($4, 2, 14)}' $L | uniq -c
# коды ответа по URL: где именно сыпется
awk '$9 ~ /^[45]/ {print $9, $7}' $L | sort | uniq -c | sort -rn | head -30
# срез за конкретную минуту инцидента
grep '06/Aug/2026:14:20:' $L | awk '{print $9}' | sort | uniq -c
Медленные запросы
Работает, если в формате есть поле rt= из примера выше. Поиск по имени поля, а не по номеру, поэтому формат можно расширять:
# топ-20 самых медленных запросов: время, URL
awk '{ for (i=1;i<=NF;i++) if ($i ~ /^rt=/) { t=substr($i,4); print t, $7 } }' $L \
| sort -rn | head -20
# средний и максимальный request_time по URL
awk '{ for (i=1;i<=NF;i++) if ($i ~ /^rt=/) t=substr($i,4);
n[$7]++; s[$7]+=t; if (t>m[$7]) m[$7]=t }
END { for (u in n) printf "%6d avg=%.3f max=%.3f %s\n", n[u], s[u]/n[u], m[u], u }' $L \
| sort -k3 -rn | head -20
# сколько запросов ушло дольше секунды
awk '{ for (i=1;i<=NF;i++) if ($i ~ /^rt=/ && substr($i,4)+0 > 1) c++ } END {print c+0}' $L
Если медленных запросов много и они разбросаны по разным URL, проблема обычно не в nginx — смотрите в сторону диагностики нагрузки на сервер.
Отсев ботов и работа с архивами
# трафик без известных краулеров
grep -viE 'bot|crawl|spider|slurp|yandex|google|bing|ahrefs|semrush' $L \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# только краулеры: кто и сколько выкачал
grep -iE 'bot|crawl|spider' $L | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head
# те же выборки по ротированным архивам
zcat /var/log/nginx/access.log.*.gz | awk '$9 ~ /^5/ {print $7}' | sort | uniq -c | sort -rn
zgrep -h '203.0.113.42' /var/log/nginx/access.log.*.gz | head
# живой хвост с фильтром
tail -f $L | grep --line-buffered -E ' (50[0-9]|429) '
Когда однострочников становится мало, ставьте goaccess — консольный анализатор, который читает combined и умеет отдавать интерактивный HTML-отчёт по IP, URL, кодам, ботам и географии. Он же умеет работать в режиме реального времени поверх tail.

Реальный IP за прокси и CDN
Как только перед nginx появляется балансировщик, CDN или ещё один nginx, $remote_addr перестаёт быть адресом посетителя: там адрес прокси. Последствия неприятные и не всегда заметные сразу — вся аналитика показывает два-три уникальных IP, limit_req и limit_conn считают лимиты на прокси целиком, а fail2ban при бане блокирует не атакующего, а собственный фронт.
Лечится модулем real_ip. Сначала проверьте, что он собран:
nginx -V 2>&1 | tr ' ' '\n' | grep realip
# ожидаем: --with-http_realip_module
# в блоке http {}
set_real_ip_from 10.0.0.0/8; # только реальные сети ваших прокси
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# для CDN, который присылает собственный заголовок:
# real_ip_header CF-Connecting-IP;
real_ip_recursive on заставляет nginx идти по цепочке X-Forwarded-For справа налево и брать первый адрес, который не входит в доверенные сети. Без него будет взят последний адрес в списке — то есть снова прокси, если их два.
Никогда не указывайтеset_real_ip_from 0.0.0.0/0.X-Forwarded-For— обычный заголовок, его подделывает кто угодно. Доверяя всем, вы отдаёте злоумышленнику право писать себе любой IP в логи, обходить rate limit и подставлять чужие адреса под бан.
Механика заголовка, порядок адресов в цепочке и альтернативный Forwarded разобраны в отдельном материале про заголовок X-Forwarded-For. Подробности директив — в документации модуля ngx_http_realip_module.
После настройки держите в формате обе величины: $remote_addr уже покажет клиента, а $http_x_forwarded_for оставит исходную цепочку — это спасает при разборе инцидентов с несколькими слоями прокси.
Ротация логов nginx: logrotate и сигнал USR1
nginx не умеет ротировать логи сам: он открывает файл при старте и пишет в него по файловому дескриптору, пока его не попросят переоткрыть. Ротацией занимается внешняя утилита logrotate, которую systemd запускает раз в сутки таймером logrotate.timer.
Файл /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
if [ -f /run/nginx.pid ]; then
kill -USR1 $(cat /run/nginx.pid)
fi
endscript
}
Что делает каждая строка:
daily— ротация раз в сутки. Альтернативы:weekly,size 100M(по размеру),maxsize 1G(по расписанию, но раньше, если файл вырос).rotate 14— сколько поколений хранить. Здесь же фактически задаётся срок хранения персональных данных.compress— сжимать архивы gzip; для access.log сжатие обычно даёт десятикратную экономию.delaycompress— сжимать не последний архив, а предыдущий. Обязательно вместе сpostrotate: между переименованием файла и обработкой сигнала проходит время, и nginx успевает дописать несколько строк в старый файл.notifempty— не ротировать пустые файлы, чтобы не плодить пустые архивы.create 0640 www-data adm— сразу создать новый файл с нужным владельцем и правами. Без этого новый файл получит владельца root и права по umask.sharedscripts— выполнитьpostrotateодин раз для всей группы файлов, а не после каждого. Без него на десяти сайтах nginx получит десять сигналов подряд.missingok— не ругаться, если файла нет.
Почему без сигнала место не освободится
logrotate переименовывает access.log в access.log.1. Для nginx ничего не меняется: у него открыт дескриптор на тот же inode, и он продолжает писать — теперь в архив. Когда через сутки logrotate удалит самое старое поколение, файл исчезнет из каталога, но inode останется живым, пока его держит процесс. Классическая картина: df показывает 100% занятого диска, du по /var/log — сотню мегабайт.
kill -USR1 заставляет мастер-процесс закрыть текущие лог-файлы и открыть их заново по тем же путям, после чего старый inode освобождается. Проверить, что проблема именно в этом:
# файлы, которые удалены, но всё ещё держатся процессами
sudo lsof +L1 | grep -i nginx
# какие лог-файлы открыты мастером сейчас
sudo lsof -p $(cat /run/nginx.pid) | grep log
# ручное переоткрытие логов
sudo kill -USR1 $(cat /run/nginx.pid)
# или через init-скрипт дистрибутива
sudo nginx -s reopen
Не заменяйте сигнал наrestart: перезапуск рвёт активные соединения.USR1иnginx -s reopenделают ровно то, что нужно, без единого потерянного запроса.
Тест и отладка ротации
# сухой прогон: показать, что будет сделано, ничего не меняя
sudo logrotate -d /etc/logrotate.d/nginx
# принудительная ротация с подробным выводом
sudo logrotate -fv /etc/logrotate.d/nginx
# когда таймер сработает в следующий раз
systemctl list-timers logrotate.timer
journalctl -u logrotate -n 50 --no-pager
# состояние: когда какой файл ротировался в последний раз
sudo cat /var/lib/logrotate/status
Три ловушки, на которых чаще всего спотыкаются:
- Права на сам конфиг. logrotate игнорирует файлы правил, доступные на запись всем или принадлежащие не root, и пишет об этом в вывод
-d. Правильно: владелец root, права 0644. - Состояние после
-f. Принудительный запуск обновляет status-файл, и ближайшая штатная ротация может быть пропущена. Для проверки используйте-d, а не-f. copytruncateвместо сигнала. Этот режим копирует файл и обнуляет оригинал, не трогая дескриптор. Он выручает, когда сигнал послать нельзя, но между копированием и обнулением теряются строки, а на большом файле ещё и удваивается расход диска и I/O. Для nginx он не нужен — сигнал работает.
В контейнере logrotate неприменим: логи идут в stdout, и ограничивать их надо драйвером логирования Docker.
# /etc/docker/daemon.json — глобально для всех контейнеров
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "5" }
}
Без этих опций json-file растёт без ограничений и рано или поздно съедает раздел с /var/lib/docker.

Сколько хранить логи и что делать с персональными данными
IP-адрес вместе с User-Agent, временем и списком просмотренных страниц позволяет выделить конкретного пользователя. В российской практике применения 152-ФЗ и в логике GDPR это рассматривается как обработка персональных данных со всеми вытекающими: нужны основание, ограниченный срок хранения и минимизация состава. Хранить сырой access.log годами «на всякий случай» — риск, а не запас.
Практичная схема выглядит так:
- Оперативный слой, 7–30 дней. Полный лог с IP — для диагностики инцидентов и расследования атак. Именно этот срок задаётся параметром
rotate. - Аналитический слой, дольше. Агрегаты без IP: количество запросов по URL, распределение кодов, перцентили времени ответа. Персональных данных здесь уже нет.
- Обезличивание на входе, если полный IP не нужен для безопасности.
Обезличить адрес можно прямо в nginx, не трогая приложение:
# в блоке http {} — обрезаем последний октет IPv4 и хвост IPv6
map $remote_addr $remote_addr_anon {
~^(?<ipv4>\d+\.\d+\.\d+)\.\d+$ $ipv4.0;
~^(?<ipv6>[0-9a-fA-F]+:[0-9a-fA-F]+): $ipv6::;
default 0.0.0.0;
}
log_format anon '$remote_addr_anon - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" rt=$request_time';
access_log /var/log/nginx/access.log anon;
Отдельная и часто забываемая проблема — query-строка. В $request попадает URI целиком, а значит, туда утекают токены восстановления пароля, ключи API и подписи, которые приложение принимает в GET-параметрах. Если убрать их из приложения нельзя, логируйте $uri вместо $request_uri — это путь без query-строки.
Проверьте, кто ещё видит ваши логи. Права 0644 на access.log, доступ к каталогу по SFTP для подрядчика, бэкап логов в незашифрованное объектное хранилище — всё это полноценные каналы утечки, которые обычно не попадают в модель угроз.
Централизованный сбор
Как только серверов больше одного, grep по каждому из них перестаёт масштабироваться. Стандартная схема: агент на хосте (promtail, vector, filebeat, fluent-bit) читает файл или journald, парсит JSON-формат и отправляет в хранилище с поиском. Три вещи, которые надо решить сразу:
- Формат. JSON на стороне nginx избавляет агент от хрупких regex-парсеров.
- Локальная копия. Оставляйте короткую локальную ротацию даже при централизации — когда сеть до коллектора отвалится, логи инцидента нужны именно локально.
- Ретенция и доступ. Централизованное хранилище наследует все требования к персональным данным, только на большем объёме и с большим числом людей, имеющих доступ.
Общие принципы — уровни, форматы, ретенция, разделение потоков — собраны в материале про управление логами.
Как проверить
Лог показывает, что nginx считает отданным. Внешняя проверка показывает, что клиент реально получил. Расхождение между ними — отдельный класс проблем: кэш промежуточного прокси, CDN, WAF, некорректный proxy_pass.
- Сверьте код ответа и заголовки, которые сервер отдаёт снаружи, с тем, что записано в access.log — проверка HTTP-заголовков. Если в логе 200, а снаружи 301 или 403, отвечает не ваш nginx.
- Найдите источник 404 в логе: битые внутренние ссылки видно быстрее сканером, чем обратным разбором referer — поиск битых ссылок.
- Поставьте внешний мониторинг, чтобы всплеск 5xx был замечен раньше, чем вы откроете лог — мониторинг доступности. Лог отвечает на вопрос «почему», мониторинг — на вопрос «когда началось».
После любой правки формата или путей логирования обязательны две команды:
sudo nginx -t # синтаксис и доступность путей
sudo nginx -s reload # применить без разрыва соединений
Затем сделайте один запрос и убедитесь, что строка появилась там, где вы её ждёте:
curl -sSI https://example.com/ -o /dev/null
sudo tail -n 1 /var/log/nginx/access.log
Частые вопросы
Где лежат логи nginx в Ubuntu и Debian?
В /var/log/nginx/: access.log и error.log, рядом — ротированные access.log.1 и access.log.2.gz и дальше. Если на сервере несколько сайтов, у каждого может быть свой файл в том же каталоге. Точный список даёт nginx -T | grep -E 'access_log|error_log'.
Как посмотреть логи nginx в реальном времени?
sudo tail -f /var/log/nginx/access.log. С фильтром нужен --line-buffered, иначе grep будет копить вывод в буфере: tail -f access.log | grep --line-buffered ' 500 '. Оба файла сразу — sudo tail -f /var/log/nginx/*.log.
Почему в логе IP прокси, а не посетителя?
Потому что $remote_addr — это адрес того, кто открыл TCP-соединение, а его открывает прокси или CDN. Настройте set_real_ip_from с реальными сетями ваших прокси и real_ip_header X-Forwarded-For. До этого вся аналитика по IP и любые баны по адресу работают неверно.
Логи заняли весь диск, что делать прямо сейчас?
Не удаляйте файл через rm: место не вернётся, пока nginx держит дескриптор. Правильная последовательность — sudo logrotate -f /etc/logrotate.d/nginx, затем sudo kill -USR1 $(cat /run/nginx.pid). Если файл уже удалён, хватит одного сигнала. Проверить остатки — sudo lsof +L1.
Можно ли выключить access_log ради производительности?
Почти никогда не нужно. Сначала включите buffer=64k flush=5s — это снимает основную часть дисковой нагрузки. Затем вынесите статику и health-check в отдельные файлы или отключите логирование точечно для них. Полное отключение лишает вас единственного источника данных о трафике и делает разбор инцидентов невозможным.
Как найти в логе конкретный запрос по времени?
Формат времени в combined — 06/Aug/2026:14:20:07, поэтому обычный grep по префиксу работает: grep '06/Aug/2026:14:2' access.log даст десять минут. Для точных диапазонов удобнее awk с substr($4, 2, 17) и сравнением строк.
Чеклист
- Знаете реальные пути логов, проверенные через
nginx -T, а не по памяти. - В формате есть
$request_time,$upstream_response_time,$upstream_addrи$host. - Для сборщика логов настроен отдельный
log_formatсescape=json. error_logна уровнеwarn;debugнигде не забыт.- За прокси и CDN настроен
set_real_ip_fromс конкретными сетями, а не с0.0.0.0/0. - В
/etc/logrotate.d/nginxестьpostrotateсkill -USR1иsharedscripts. logrotate -dпроходит без предупреждений о правах на конфиг.- Для контейнеров заданы
max-sizeиmax-fileв драйвере логов. - Срок хранения сырых логов с IP определён и совпадает с параметром
rotate. - Токены и ключи не попадают в лог через query-строку.
- Права на каталог логов ограничены; бэкапы логов шифруются.
- Есть внешний мониторинг, который заметит всплеск ошибок раньше, чем вы откроете лог.