Nginx keeps two logs. The access log records every request: who asked, for what, and what they got back. The error log records problems: missing files, permission errors and failed connections to PHP or your app. Knowing how to read them turns most website problems from guesswork into a five-minute fix. This guide shows where they live, what a line means, and a set of commands for answering common questions.
Before you start
- A Linux VPS with Nginx installed.
- SSH access with sudo. Log files are readable by root and the
admgroup on Ubuntu and Debian.
Where the logs live
On Ubuntu, Debian, AlmaLinux and Rocky Linux the defaults are:
/var/log/nginx/access.log/var/log/nginx/error.log
Individual sites may write elsewhere. To list every log path in your active configuration:
sudo nginx -T 2>/dev/null | grep -E "^\s*(access_log|error_log)"Watch requests arrive live while you load a page:
sudo tail -f /var/log/nginx/access.logPress Ctrl + C to stop.
Read an access log line
The default combined format produces lines like this:
198.51.100.20 - - [09/Nov/2026:10:15:32 +0000] "GET /blog/ HTTP/2.0" 200 5124 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."From left to right:
- Client IP:
198.51.100.20. - User:
-unless HTTP basic auth was used. - Time of the request.
- Request line: method, path and protocol.
- Status code:
200OK,301redirect,404not found,500/502server errors. - Bytes sent: the response body size.
- Referer: the page that linked here.
- User agent: the browser or bot.
Add timing to the log
The default format does not show how long a request took. A custom format in the http context adds it:
sudo nano /etc/nginx/conf.d/log-format.conflog_format timed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time urt=$upstream_response_time';Use it in a server block with access_log /var/log/nginx/example.com.access.log timed;. rt is the total time Nginx spent on the request, and urt is the time PHP or your app took. Slow pages stand out immediately.
Give each site its own logs
Inside each site's server block:
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log warn;Per-site logs make it much easier to debug one site on a server that hosts several. To skip logging for noisy paths such as health checks:
location = /health {
access_log off;
return 200 "ok\n";
}Set the error log level
error_log takes a level. Nginx logs that level and everything more serious:
debug,info,notice: very chatty.debugalso needs a special build.warn: a good default for production.error: the usual default. Shows failures only.crit,alert,emerg: serious problems.
Temporarily raising a site to info can help when chasing a problem. Set it back afterwards, because busy sites fill the disk quickly.
Common error log messages:
open "/var/www/.../favicon.ico" failed (2: No such file or directory): a missing file, usually harmless.connect to unix:/run/php/php8.3-fpm.sock failed: PHP-FPM is down or the socket path is wrong.upstream timed out (110: Connection timed out): the backend was too slow.Permission denied: file permissions or SELinux.
Answer common questions
Top 10 client IPs:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | headMost requested URLs:
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | headCount of each status code:
sudo awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rnURLs returning 404:
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | headEvery request from one IP:
sudo grep "^198.51.100.20 " /var/log/nginx/access.log | tail -20Rotated logs end in .1 (plain text) or .2.gz and older (compressed). Search the compressed ones with zgrep or zcat.
Log rotation
Without rotation, logs grow until the disk is full. The Nginx packages install a logrotate rule at /etc/logrotate.d/nginx. It rotates /var/log/nginx/*.log on a schedule, compresses old files, and tells Nginx to reopen its log files.
Per-site logs inside /var/log/nginx/ are covered automatically because of the *.log pattern. Logs elsewhere need their own rule. See Logrotate guide.
Test the rule without changing anything:
sudo logrotate -d /etc/logrotate.d/nginxWarning: Do not delete a log file Nginx is writing to. Nginx keeps writing to the deleted file and the disk space is not freed until a reload. To empty a huge log, run sudo truncate -s 0 /var/log/nginx/access.log instead.
Check it works
After adding a per-site log, reload and request a page:
sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null https://example.com/
sudo tail -n 1 /var/log/nginx/example.com.access.logYou should see your request at the bottom of the new file.
Troubleshooting
- The access log is empty: the site writes to its own log, or
access_log offis set. Run thenginx -Tcommand above to find the real path. - Every request shows the same IP: you are behind Cloudflare or a proxy. Set up the real IP module.
- Nginx will not start after adding a log path: the folder does not exist. Create it, and on AlmaLinux or Rocky keep logs under
/var/log/nginxso SELinux allows writing. - Disk filling with logs: check rotation runs (
sudo cat /var/lib/logrotate/statuson Debian/Ubuntu,/var/lib/logrotate/logrotate.statuson RHEL-family) and lower the error log level. - Error log full of "No such file" for favicon.ico: add a small favicon or
location = /favicon.ico { log_not_found off; access_log off; }.
Next steps
- Record real visitor IPs behind a CDN: Show real visitor IPs in Nginx behind Cloudflare.
- Turn log patterns into bans: Fail2ban jails for Nginx.
- Where other system logs live: Linux log files locations.
