# Self-hosted analytics with Docker, *in about 10 minutes*

> The page https://skaldanalytics.com/self-host/docker/ as Markdown. Every page and guide: https://skaldanalytics.com/llms.txt . Skald is source-available (Elastic License 2.0).

Self-host · Docker · ghcr.io/spcaeo/skald:2

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](https://skaldanalytics.com/self-host/).

Copy the Docker command [Open the live demo](https://spcaeo.github.io/skald/)

Free to self-host · Elastic License 2.0 · rather not run a server? 30-day free trial, no card

- Run Skald with Docker, Compose or the…
- Put the dashboard behind your web…
- Sign in, then fill the days your log…
- Upgrade the Skald image without…

ghcr.io/spcaeo/skald:2 · what is in the box

1. `ENTRYPOINT`entrypoint.py serve: starts the four programs
2. `HEALTHCHECK`every 60 s: the sign-in page, and the scheduler's heartbeat
3. `EXPOSE 8080`nginx inside, the only port
4. `VOLUME /var/lib/skald`history, accounts, settings, backups
5. `COPY`Skald's code to /opt/skald
6. `useradd 10001`its own user, in group adm to read /var/log/nginx
7. `pip`PyYAML, the one optional library
8. `apt`nginx-light, with auth_request and realip
9. `FROM`python: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.

pick one

root@your-server: ~ · as you typed it

```
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:ro` mounts your log read-only. The container's user is in group `adm`, which reads /var/log/nginx on stock Debian and Ubuntu.
- `skald-data` is the volume that holds everything Skald keeps. Keep it.
- `TRUSTED_PROXIES` lets Skald take each visitor's address from your nginx's header, and only from a private address.

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.

switch one

**a visitor** `203.0.113.9`

**your nginx** `:443 · your certificate`sets X-Real-IP

**published port** `127.0.0.1:8080`

**the container** `nginx :8080`helpers 127.0.0.1:8765 and :8766, never published

**a stranger online** `198.51.100.40`plain 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.

nginx · inside your site's HTTPS server block

```
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;
}
```

check, then reload

```
$ 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.

Caddy instead · inside the site's block

```
@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}
    }
}
```

[nginx log analytics](https://skaldanalytics.com/server-log-analytics/nginx/) [Caddy log analytics](https://skaldanalytics.com/server-log-analytics/caddy/)

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

Old log files your server keeps: **14**  slide

the files in /var/log/nginx

1. access.log
2. access.log.1
3. access.log.2.gz
4. access.log.3.gz
5. …
6. access.log.14.gz

backfill --days 30, yesterday on the left

1. 2.
3. 4.
5. 6.
7. 8.
9. 10.
11. 12.
13. 14.
15. 16.
17. 18.
19. 20.
21. 22.
23. 24.
25. 26.
27. 28.
29. 30.

- **stored
- **stored, labelled partial
- **nothing stored

```
backfill: ok 14, withheld 0, empty 16, skipped 0
```

1. **Make the first owner**

It prints a "set your password" link, valid 60 minutes. Change the organisation and the email first.

edit, then copy

```
$ docker exec -it skald skald accounts init --org "Example Co" --owner-email owner@example.com
```

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

as written

```
$ docker exec skald skald backfill --days 14
```

3. **Ask the doctor**

One line per part, with the fix. Before the first daily run, "has never run here" is expected.

as written

```
$ 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.

[See what you will get: the live demo](https://spcaeo.github.io/skald/) [Rather not run a server? 30 days free](https://skaldanalytics.com/beta/?choice=trial#signup)

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

land one

`:2.31.0`exactly this release

`:2.31`fixes to 2.31 only

`:2`every 2.x release

`:latest`a new major too

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

take a copy, then upgrade

```
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 upgrade` does 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 run` gets 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.

[Send another server's log to this Skald](https://skaldanalytics.com/docs/setup/one-command/) [Every place Skald can run](https://skaldanalytics.com/self-host/) [self-hosted analytics on Coolify](https://skaldanalytics.com/self-host/coolify/) [self-hosted analytics on Fly.io](https://skaldanalytics.com/self-host/fly/) [self-hosted analytics on Kubernetes](https://skaldanalytics.com/self-host/kubernetes/)

keep reading

## More on self-hosted analytics with Docker

- [Coolify, CapRover, Dokku](https://skaldanalytics.com/self-host/coolify/): The image with persistent storage and HTTPS from the panel you already run.
- [Nginx log analytics](https://skaldanalytics.com/server-log-analytics/nginx/): One command finds the site's access log in nginx's settings and ships it, with or without Docker.
- [Skald pricing](https://skaldanalytics.com/pricing/): Per site, a 30-day free trial with no card, or free to self-host.

questions

## Self-hosted analytics with Docker FAQ

Straight answers, including what Skald does not do.

[Read the docs](https://skaldanalytics.com/docs/) [Ask us a question](https://skaldanalytics.com/contact/)

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