set-up · nginx · with or without Docker
Nginx log analytics from the log nginx already writes
Nginx log analytics with no change to nginx for most sites. One command on your server finds the site's access log in nginx's own settings and sends each new line to your Skald, with or without Docker.
Nginx log analytics is reading the access log nginx writes for every request it answers, and turning it into visits, sources and pages. Skald reads nginx's default combined format, and its own extended format with the host, the time taken and the country.
30-day free trial · no card · we set it up within one working day · free to self-host
$ curl -fsSL https://<your Skald>/traffic-kit/<code> | sudo bash Skald: installing the log shipper for example.com. == 1. How it runs here with systemd (no Docker; Python 3.12 and systemd are here) == 2. The access log of example.com found in the web server's settings: /var/log/nginx/example.com.access.log Done. The shipper for example.com runs with systemd and sends to your Skald.
Sample output, shortened. Your Skald shows the full command, with your own code in it.
[01] one command
One command finds the nginx access log and ships it
The command is the same on every server. It looks at what the server has, and picks the way to run. Flip the switch to see both.
$ curl -fsSL https://<your Skald>/traffic-kit/<code> | sudo bash == 1. How it runs here with docker (Docker works here) == 2. The access log of example.com found in the web server's settings: /var/log/nginx/example.com.access.log == 3. The token == 4. Start the shipper (Docker)
What it leaves on the server
A small skald-ship container that reads the log, mounted read-only. Docker restarts it after a reboot.
The token is kept in /srv/skald/ship-token (root:adm, 0640). It is used, never printed, and it can only add lines to this one site.
Two commands for later: skald-ship-check and skald-ship-uninstall.
How it finds the log
It reads nginx.conf, conf.d/ and sites-enabled/ for the server block whose server_name is your site or www. your site, and takes its access_log, with $host filled in. A block with no access_log of its own writes to the http block's. Finds several: it asks, the biggest first. Finds none: it tries the usual places, such as /var/log/nginx/access.log.
Options go after sudo bash -s --: --log <path> names the file, --yes asks nothing, --dry-run shows every step and changes nothing.
[02] the log format
Which nginx log_format Skald reads
Most sites change nothing. Two fields at the end of the line add something, and both are read by the main count and by the audit.
pick a format
nginx's combined format, as it is
If your access_log line names no format, or names combined, Skald reads it as it is. It holds the six facts Skald needs: address, time, request, status, referrer and user agent.
- 198.51.100.7 - - [23/Oct/2026:09:14:03 +0000] "GET / HTTP/2.0" 200 8312 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0) Chrome/130.0"people
A Next.js site: one field at the end
A click inside a Next.js site sends no HTML request, and a link prefetch looks the same in a plain log. This field tells them apart, so a click is a page view and a prefetch never is. Add it at the very end, inside the log_format quotes:
rsc_prefetch=$http_next_router_prefetchThen test and reload:
$ sudo nginx -t && sudo systemctl reload nginx
The request's size, for Security
With this field, Security can name POST bodies of 1,000,000 bytes or more. Without it, Security says the size is not recorded, never 0. It sits at the end of the line, beside rsc_prefetch= if you have both.
request_length=$request_length$ sudo nginx -t && sudo systemctl reload nginx
Skald's extended format
It adds the host, the time nginx took, and the country Cloudflare resolved, when the site is behind Cloudflare. A line may also end with rid=$request_id, so a line nginx wrote twice is dropped.
log_format intel '$time_iso8601 $remote_addr $host "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_time '
'cc=$http_cf_ipcountry ray=$http_cf_ray';Then name it on the site's access_log line, test and reload. If Skald then says no line could be read, set the site's log format to Detect it (auto) in Settings > Add or remove sites.
[03] what you get
What an nginx access log shows that a script tag cannot
nginx writes a line for every request, whether a script ran or not. So the crawlers and bots that never run a script are in the log too.
Each line lands in one of five groups: people, AI assistants, search engines, bots and scanners, and your own traffic. AI crawlers are named, and checked against the addresses their owners publish. People get visits, sources, pages and countries, counted from the same lines.
- 09:14:02 203.0.113.24 "GET /pricing/" 200 "GPTBot/1.2"AI · training
- 09:14:03 198.51.100.7 "GET /" 200 "google.com" Chrome/130people
- 09:14:03 198.51.100.7 "GET /assets/app.css" 200 Chrome/130proof it rendered
- 09:14:05 203.0.113.66 "GET /blog/" 200 "Googlebot/2.1"search
- 09:14:10 192.0.2.14 "GET /.env" 404 "zgrab/0.x"bots · probe
Sample lines. The stylesheet is not a page view: it is the proof that a browser drew the page.

[04] check and undo
Check the nginx shipper, or take it away
The check reads only and changes nothing. Each line says OK, or PROBLEM with the fix under it.
- Never lost: the shipper saves its place in the log only after Skald has taken the batch.
- Never twice: batches are numbered, and Skald drops one it already has.
- Log rotation: it follows logrotate's renames and copytruncate. Keep
delaycompress. - Small sends: gzip batches of at most 2,000 lines or 4 MiB.
$ sudo skald-ship-check ✓ OK the web server (nginx) is running ✓ OK the access log /var/log/nginx/example.com.access.log is there and group adm may read it ✓ OK the access log is growing (written in the last 10 minutes) ✓ OK www.example.com writes to /var/log/nginx/example.com.access.log ✓ OK the send address answers (401 without a token is expected) ✓ OK skald-ship is running (0 restarts)
$ sudo skald-ship-uninstall
Sample output, shortened.
[05] other set-ups
Skald on the same nginx server, behind a CDN, and the limits
Skald on the same server
No shipper at all. Run Skald in Docker and mount the log folder read-only, for example
-v /var/log/nginx:/logs:ro. Skald reads the log in place.Behind Cloudflare
A page Cloudflare serves from its cache never reaches nginx. Add the Skald Worker beside the shipper: Skald keeps the Worker's cache hits and takes the rest from nginx.
One log per site
Each site needs its own
access_login its own server block. A log shared by several host names mixes their visitors, and the check says so.
What it does not do
- It does not edit nginx. The Next.js field and the extended format are yours to add, and the check only says when they are missing.
- It sees only requests that reach nginx. Behind a CDN, cached pages need the CDN's own log or the Skald Worker.
- It has no country for a plain combined line unless a GeoIP country file is set on the Skald server. Without one, the country says Not recorded.
- It sends only lines written after the install, from the moment it starts. Older lines in the file are not sent.
keep reading
More on nginx log analytics
questions
Nginx log analytics FAQ
Straight answers, including what Skald does not do.
Do I have to change my nginx log format?
No, for most sites. Skald reads nginx's default combined format as it is. A Next.js site needs one field added at the end of the log_format, so a click inside the site counts as a page view. Skald's extended format, with the host, the time taken and Cloudflare's country, is optional.
Does it need Docker?
No. The one command uses Docker when Docker works on the server. Otherwise it installs the shipper as a systemd service, which needs Python 3.9 or newer. If neither is there, it stops and says what to install.
Will I lose lines when nginx's log rotates?
No. The shipper follows logrotate's renames and copytruncate, and it saves its place only after Skald has taken a batch. Keep delaycompress in the logrotate settings, so the last rotated file is not compressed while the shipper may still be reading it.
Can a line be counted twice if the network fails?
No. Batches are numbered and Skald drops a batch it already has. A batch that did not reach Skald is sent again, so a line is neither lost nor counted twice.
Several sites share one access log. Is that a problem?
Yes. Each site needs its own access_log line in its own server block, or the lines of the other sites are mixed in. sudo skald-ship-check says when the log is shared with other host names.
Where do the country figures come from?
From the log when it carries one: Skald's extended format records Cloudflare's country header as cc=. Otherwise from a GeoIP country file on the Skald server, when one is set. With neither, the country says Not recorded. It is never guessed.
$ tail -f access.log | skald
See what AI does with your site. Free for 30 days.
No card. We set up hosted Skald for you within one working day, or you self-host it free. Every line of your log, sorted and added up.