Skip to content
EN
← Все статьи

Логи nginx: где лежат, как читать и как настроить ротацию

Коротко. По умолчанию 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.

Схема расположения логов nginx: каталог /var/log/nginx, отдельные файлы на виртуальные хосты и вывод в stdout контейнера
Один и тот же nginx может писать в общий файл, в файл конкретного сайта или в stdout контейнера — путь задаёт директива, а не соглашение.
Прежде чем искать причину проблемы в логе, убедитесь, что вы смотрите тот самый лог. Половина «загадочных» случаев «в логах пусто, а сайт отдаёт 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.

Анатомия строки лога nginx: поля формата combined и дополнительные переменные request_time, upstream_response_time, upstream_addr
Combined отвечает на вопрос «что запросили», расширенный формат — на вопрос «почему это было медленно».

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Тот же поток, но в драйвере логов Dockernginx в контейнере, файл в томе пустой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.

Схема конвейера анализа лога: сырой access.log проходит через awk, sort и uniq и превращается в топ IP, топ URL и распределение кодов ответа
Три утилиты закрывают 90% задач по разбору лога — специализированный анализатор нужен уже для отчётов, а не для диагностики.

Реальный 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.

Цикл ротации логов nginx: logrotate переименовывает файл, отправляет сигнал USR1, nginx переоткрывает дескриптор и старый inode освобождается
Без сигнала USR1 nginx продолжает писать в переименованный или уже удалённый файл, и место на диске не возвращается.

Сколько хранить логи и что делать с персональными данными

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-строку.
  • Права на каталог логов ограничены; бэкапы логов шифруются.
  • Есть внешний мониторинг, который заметит всплеск ошибок раньше, чем вы откроете лог.

Проверьте ваш сайт прямо сейчас

Следить за своим сервером →
Другие статьи: Инфраструктура
Инфраструктура
Почта на своём домене: Яндекс 360, VK WorkSpace или свой сервер — настройка, отправка с сайта и переезд
21.07.2026 · 302 просм.
Инфраструктура
Алгоритмы балансировки нагрузки: Round Robin, Least Connections и другие
16.03.2026 · 265 просм.
Инфраструктура
Стратегии версионирования API: URL, заголовки и параметры запроса
16.03.2026 · 262 просм.
Инфраструктура
Rate Limiting в API: зачем и как настроить
14.03.2026 · 255 просм.