Docker Compose guide

The shortest path to a working CapScope: one compose service, one named volume, one password. This guide covers the published image and a plain docker run, then hardens the setup with a read-only socket proxy and shows how to size the database.

Prerequisites

  • A Docker host — any machine with the Docker Engine and its socket available.
  • A long random password for Basic auth: openssl rand -base64 24.
  • Outbound internet on the host if you build from source (the first build downloads Go modules).
  • A TLS-terminating reverse proxy in production — CapScope listens on 8080 with plain HTTP.
Docker socket access is root equivalent. Mounting /var/run/docker.sock gives the container full control of the host. Use Basic auth, HTTPS, and preferably the read-only socket proxy below in production.

Quick start with docker run

bash
docker run -d --name capscope \
  -p 8080:8080 \
  -e AUTH_USER=admin \
  -e AUTH_PASS='change-me' \
  -v capscope-data:/data \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --restart unless-stopped \
  ghcr.io/hacrex/capscope:latest

Open http://localhost:8080 and log in. The named volume capscope-data holds the SQLite database at /data/capscope.db — do not drop it, or history dies with the container.

Docker Compose

docker-compose.yml
services:
  capscope:
    image: ghcr.io/hacrex/capscope:latest
    ports:
      - "8080:8080"
    environment:
      AUTH_USER: admin
      AUTH_PASS: change-me
      # LOG_RETENTION: 7d
      # MAX_DB_MB: 2048
      # EXCLUDE: '^captain-'
    volumes:
      - capscope-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

volumes:
  capscope-data:

AUTH_PASS defaults to change-me — change it before exposing the port. The repo also ships deploy/docker-compose.yml, which builds from source (build: ..) instead of pulling the published image:

bash
cd deploy
docker compose up -d --build   # then open http://<host>:8080

Harden with a socket proxy

Mounting the socket is the simplest setup, but a read-only socket proxy keeps the raw socket out of CapScope entirely. Add a tecnativa/docker-socket-proxy service that exposes only the read endpoints CapScope needs, drop the socket mount from CapScope, and point DOCKER_HOST at the proxy:

docker-compose.yml (safer variant)
services:
  socket-proxy:
    image: tecnativa/docker-socket-proxy
    environment: { CONTAINERS: 1, EVENTS: 1, PING: 1, POST: 0 }
    volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]

# capscope: drop the docker.sock mount and set:
#   DOCKER_HOST=tcp://socket-proxy:2375

CONTAINERS=1 is what allows GET /containers/* — inspect, logs and stats. POST=0 blocks every write. Do not publish the proxy's port publicly; keep it on the same Docker network as CapScope.

Retention and size caps. Age-based retention runs 30 seconds after start, then every 10 minutes: old rows are deleted in batches, then per-app budgets, then the MAX_DB_MB cap drops the oldest logs first. MAX_DB_MB wins over LOG_RETENTION if the two ever conflict.

Environment highlights

Everything is configured through environment variables. The full list lives in the configuration reference; these are the ones you almost always touch on a Docker host:

VariableDefaultMeaning
AUTH_PASSnoneMandatory. The server refuses to start without it.
DOCKER_HOSTunix:///var/run/docker.sockunix:// socket or tcp://host:port for a socket proxy.
LOG_RETENTION7dDelete logs older than this (d, h, m).
SNAPSHOT_RETENTION30dSame for crash snapshots.
MAX_DB_MB1024Hard cap on database size; oldest data dropped first.
STATS_INTERVAL10sMetric sample interval per container (min 1s).
INCLUDE / EXCLUDEnoneRegexes matched against container and app names.
REDACTtrueMask JWTs, bearer tokens, AWS keys and PEM keys before storage.
ALERTS_FILE/data/alerts.jsonHot-reloaded JSON alert-rules file.
ALLOW_NO_AUTHfalseLocal testing only — never on a shared host.

Verify

Health endpoint

curl -k http://localhost:8080/healthz returns 200. The path is auth-free by design so proxies and monitors can probe it.

Log in and check the sidebar

Open the UI with Basic auth. The header chip shows a container count above zero, and the sidebar lists your apps within about 10 seconds.

Search a known string

Pick any container, open a log line you expect to exist, and run an FTS5 text search. Results come back with timestamps.

Crash something on purpose

Run a throwaway container that exits with code 1. The Crashes tab shows a snapshot with exit code, log tail and a plain-language summary you can export as Markdown.

Prove the volume works

Restart the CapScope container, then search again — logs, metrics and snapshots survive, which confirms /data is persisted.

Troubleshooting

SymptomCause / fix
Container exits instantly with "AUTH_PASS …"Password not set — set it in the compose file or with -e AUTH_PASS=….
Exits with "cannot reach Docker"Socket not mounted, or the DOCKER_HOST proxy URL is wrong.
Sidebar emptySocket not mounted / wrong path, or an EXCLUDE regex that is too greedy.
Live tail stallsThe reverse proxy is buffering SSE — honor X-Accel-Buffering: no or disable buffering for /api/logs/stream.
Data gone after redeploy/data is not on a persistent volume.
Corrupt DB / locked errorsMore than one replica writing the same file — scale to exactly 1.
Container shows no logsIts logging driver does not support reading (docker logs fails for it). Supported drivers: json-file, local, journald.

Capacity

Measured on a sandbox: about 400 bytes of database per stored log line for a 130-byte JSON line (three indexes included). Estimate with bytes per day = lines per second × 86400 × 400:

Lines per second (all apps)Per dayPer 7 days
1about 35 MBabout 240 MB
10about 350 MBabout 2.4 GB
100about 3.5 GBabout 24 GB

Metrics are cheap: one row per container every 10 seconds is roughly 8,600 rows per container per day. Set MAX_DB_MB to fit your volume — the oldest logs are dropped early to stay under the cap.


Next: platform-specific steps for CapRover, Coolify, Dokploy, Dokku or Portainer & others.