Self-host · Docker · ghcr.io/spcaeo/skald:2
Self-hosted analytics with Docker, in about 10 minutes
Self-hosted analytics with Docker is one container: nginx, two helpers and a schedule inside, your log mounted read-only, every figure in one volume. Type your site below and copy the command.
Self-hosted analytics with Docker means running Skald's image, ghcr.io/spcaeo/skald, on a server you control. The container reads your web server's access log read-only and keeps its history in a Docker volume, so no figure leaves the machine. It is one way to self-host Skald free.
Free to self-host · Elastic License 2.0 · rather not run a server? 30-day free trial, no card
ENTRYPOINTentrypoint.py serve: starts the four programsHEALTHCHECKevery 60 s: the sign-in page, and the scheduler's heartbeatEXPOSE 8080nginx inside, the only portVOLUME /var/lib/skaldhistory, accounts, settings, backupsCOPYSkald's code to /opt/skalduseradd 10001its own user, in group adm to read /var/log/nginxpipPyYAML, the one optional libraryaptnginx-light, with auth_request and realipFROMpython:3.12 slim, Debian bookworm
The layers of docker/Dockerfile, in build order from the bottom. One image holds nginx, the two helpers and the scheduler; your data lives in the volume, never in the image.
[01] start it
Run Skald with Docker, Compose or the install script
Type your site's details and pick how your server should run it. The command writes itself. Each way runs the same code and keeps the same files. This command and the next three folds are the whole first install, about 10 minutes on a server that already runs nginx.
docker run -d --name skald --restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-v /var/log/nginx:/logs:ro \
-v skald-data:/var/lib/skald \
-e SKALD_SITE=example.com \
-e SKALD_HOSTS="example.com www.example.com" \
-e SKALD_TIMEZONE=America/New_York \
-e SKALD_BASE_URL=https://www.example.com \
-e SKALD_TRUSTED_PROXIES=private -e SKALD_REAL_IP_HEADER=X-Real-IP \
ghcr.io/spcaeo/skald:2
127.0.0.1:publishes the port on this machine only. Your web server gives it to the world over HTTPS (next fold)./logs:romounts your log read-only. The container's user is in groupadm, which reads /var/log/nginx on stock Debian and Ubuntu.skald-datais the volume that holds everything Skald keeps. Keep it.TRUSTED_PROXIESlets Skald take each visitor's address from your nginx's header, and only from a private address.
- The repository's
docker-compose.ymlreads this.env. It already publishes on 127.0.0.1 only and trusts the private ranges. SKALD_LOGSis the folder on the host that holds your log and its rotations. It is mounted read-only.- Mail logins go in this
.envtoo, never in the compose file.
- The
httpsprofile adds Caddy on ports 80 and 443. It gets the certificate forSKALD_DOMAINby itself. - Point that name at this server first. Use it when nothing else on the server holds ports 80 and 443.
- Skald still reads your web server's log from
SKALD_LOGS, read-only.
- For Ubuntu 22.04, 24.04 and 26.04 and Debian 12 and 13. It installs Python, a system user, a systemd service and the schedule.
- Without Docker,
skald upgradetakes a tested backup, checks health after the upgrade and rolls back if a check fails. --uninstallremoves the service and keeps your data;--uninstall --purgeremoves the data too.
The container checks three settings when it starts. A site name that is not a host name, a time zone that is not a zone name, or an address that is not http(s) stops it with the line shown above, in the container's own words.
[02] HTTPS in front
Put the dashboard behind your web server's HTTPS
Sign-in needs HTTPS, and your web server already has it. It sends Skald's paths to the container and serves everything else as before. Two settings decide who can reach the container, and whose address the sign-in limits see. Your visits are still counted from your own log.
203.0.113.9:443 · your certificatesets X-Real-IP127.0.0.1:8080nginx :8080helpers 127.0.0.1:8765 and :8766, never published198.51.100.40plain http, straight to :8080- Who can reach port 8080
- Only this machine: your web server.
- The address sign-in sees
- 203.0.113.9, the visitor's own, from your nginx's header.
location ~ ^/traffic(?:$|/|-(?:auth|api|ingest|mcp|hooks|beacon|js)(?:/|$)) {
access_log off;
client_max_body_size 6m;
proxy_read_timeout 60s;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
$ sudo nginx -t && sudo systemctl reload nginx
access_log off keeps your own dashboard views out of your visitors. Your website shares this nginx, so the test runs before the reload. The nginx inside the container does the rest: the session check on every dashboard file, the sign-in limits and the security headers.
@skald path_regexp ^/traffic($|/|-(auth|api|ingest|mcp|hooks|beacon|js)(/|$))
log_skip @skald
handle @skald {
reverse_proxy 127.0.0.1:8080 {
header_up X-Real-IP {remote_host}
}
}
[03] first run
Sign in, then fill the days your log still holds
Your log rotates every day and keeps a few old files. Backfill turns each day those files still hold into a stored day, so the charts do not start empty. Slide how many old files your server keeps.
the files in /var/log/nginx
- access.log
- access.log.1
- access.log.2.gz
- access.log.3.gz
- …
- access.log.14.gz
backfill --days 30, yesterday on the left
- stored
- stored, labelled partial
- nothing stored
backfill: ok 14, withheld 0, empty 16, skipped 0
Make the first owner
It prints a "set your password" link, valid 60 minutes. Change the organisation and the email first.
$ docker exec -it skald skald accounts init --org "Example Co" --owner-email owner@example.com
Fill the days
A day the log no longer holds is not stored at all: a row of zeroes would be a lie. A day that does not add up is stored as withheld and never shown.
$ docker exec skald skald backfill --days 14
Ask the doctor
One line per part, with the fix. Before the first daily run, "has never run here" is expected.
$ docker exec skald skald doctor
Sample files and days. Today fills by itself within two minutes, and the daily run stores yesterday each morning. With no country in your log, give Skald a GeoIP file (the switch in fold 01); without one, countries stay not recorded.
[04] upgrade
Upgrade the Skald image without losing a day
An image is never changed in place: you pull the tag again and start the container. Which tag you pin decides what a pull brings. Land three sample releases, one after another, and watch where each of the four tags points.
:2.31.0exactly this release:2.31fixes to 2.31 only:2every 2.x release:latesta new major tooEvery tag is on 2.31.0, the newest release in this sample.
Pin :2. Before you pull, take a copy: the backup restores itself to prove it worked. The databases are brought up to date when the new container starts. Never upgrade during the daily run.
docker exec skald skald backup
docker pull ghcr.io/spcaeo/skald:2
docker rm -f skald
Then start it with the same docker run command as before; the volume keeps every figure. With Compose: docker compose pull && docker compose up -d.
What it does not do
skald upgradedoes not run inside a container: with Docker you pull the new tag. Watchtower (a container that pulls newer images for you) can do the pulling, but it takes no backup and never rolls back.- It runs one copy on one volume. Never start a second container on the same data.
- A plain
docker rungets no certificate: your web server's HTTPS, or Compose's https profile, does that. - Its nightly backups stay inside the volume. Copy them off the server too:
docker cp skald:/var/lib/skald/backups ./skald-backups. - Without a server log (Shopify, Webflow, Squarespace, or the script tag alone), it sees no AI crawlers and no bots.
Sample releases after 2.31.0, to show how the tags follow. The tag rules are the ones every Skald release is published with.
keep reading
More on self-hosted analytics with Docker
questions
Self-hosted analytics with Docker FAQ
Straight answers, including what Skald does not do.
Which Docker tag should I use for Skald?
Pin ghcr.io/spcaeo/skald:2. It follows every 2.x release, fixes and features, and never a breaking change. :latest follows every release, a new major version too. A tag such as :2.30.0 stays on exactly one release.
Where does the Skald container keep its data?
In one volume mounted at /var/lib/skald: the history, accounts, settings, backups and any shipped logs. Keep that volume. Removing the container keeps the data; only docker volume rm deletes it.
Does Skald change my nginx log or my website?
No. The log is mounted read-only, and Skald only reads it. You add one location block to your web server so Skald's paths reach the container, then check and reload nginx yourself.
Why does sign-in fail on plain http?
The session cookie is Secure, so a browser keeps it only over HTTPS, or at http://localhost. Put your web server's HTTPS in front, or start Compose with the https profile, which adds Caddy and gets a certificate by itself.
How do I upgrade Skald in Docker?
Pull the new image, remove the container and start it again with the same command. The volume keeps every figure, and the databases are brought up to date when the new container starts. Take a copy first with docker exec skald skald backup if you want one. Never upgrade during the daily run.
Can I run Skald without Docker?
Yes. On Ubuntu 22.04, 24.04 and 26.04 or Debian 12 and 13, the install script puts Skald beside your nginx as a system user with systemd and cron. Its skald upgrade command takes a tested backup, checks health and rolls back if a check fails.
$ 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.