Skip to content
New storyWe built Skald to count visitors. It caught an AI-key thief.
SkaldEvery visit, truly told.

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

root@web-server: ~ · sample run
$ 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.

flip it
root@web-server: ~ · sample run
$ 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.

access.log · combined · sample1 line
  1. 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

[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.

/var/log/nginx/example.com.access.log · sample5 lines
  1. 09:14:02 203.0.113.24 "GET /pricing/" 200 "GPTBot/1.2"AI · training
  2. 09:14:03 198.51.100.7 "GET /" 200 "google.com" Chrome/130people
  3. 09:14:03 198.51.100.7 "GET /assets/app.css" 200 Chrome/130proof it rendered
  4. 09:14:05 203.0.113.66 "GET /blog/" 200 "Googlebot/2.1"search
  5. 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.

Skald's Overview with sample data: visitors now, pages read, leads, visitors from AI, a map of visitors and the newest visits
The Overview, with sample data, read from the access log.

[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.
root@web-server: ~
$ 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)
root@web-server: ~ · undo
$ sudo skald-ship-uninstall

Sample output, shortened.

[05] other set-ups

Skald on the same nginx server, behind a CDN, and the limits

  1. 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.

  2. 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.

  3. One log per site

    Each site needs its own access_log in 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.

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.

Start free trial Live demo