Running the container

The minimum viable deployment, a compose file, and the two variables that matter most.

One command

bash
docker run -d --name bollwark -p 3000:3000 ghcr.io/hauju/bollwark:latest

That is a working service on port 3000 with every default in place — and it is deliberately useless for anything but a smoke test, because without ADMIN_TOKEN you cannot register a site. POST /v1/sites returns 404; there is no anonymous provisioning.

The real minimum:

bash
docker run -d --name bollwark -p 3000:3000 \
  -e ADMIN_TOKEN="$(openssl rand -hex 32)" \
  -e SITE_DB_PATH=/data/sites.db \
  -v bollwark-data:/data \
  ghcr.io/hauju/bollwark:latest

Check it with curl localhost:3000/healthz — it answers ok.

Compose

bash
curl -O https://raw.githubusercontent.com/hauju/bollwark-api/main/docker-compose.yml
# uncomment ADMIN_TOKEN, and SITE_DB_PATH if sites should survive a restart
docker compose up -d
docker compose logs -f

The file in the repository is a single service with a named volume for /data and every optional variable present but commented out. To build from a checkout instead of pulling, swap image: for build: ..

The image

Registry ghcr.io/hauju/bollwark
Tags latest, 1.2.3 / 1.2 / 1, sha-<commit>
Platforms linux/amd64, linux/arm64
Base debian:trixie-slim
User non-root, uid/gid 10001
Port 3000
Data dir /data

Pin a version tag in production. latest moves on every push to main.

STATIC_DIR and LISTEN_ADDR are baked in and should not need overriding.

The two variables that matter

Variable Why
ADMIN_TOKEN Gates POST /v1/sites and all of /v1/admin/*. Unset, those routes 404. Generate with openssl rand -hex 32.
SITE_DB_PATH Without it, sites are in-memory only and every restart invalidates every integrator's secret_key. Set it to a path on your volume.

Everything else is inert until configured: ADMIN_DB_PATH (the decision log and validation dashboard — note it requires ADMIN_TOKEN or the server refuses to start), GEOIP_DB_PATH, IP_REPUTATION_FILE, TLS_FINGERPRINT_HEADER, LOAD_LADDER, PUZZLE_ALGORITHM and the difficulty knobs, LOG_FORMAT=json.

All of them, with defaults and semantics, are in CONFIGURATION.md.

Healthcheck

The image ships a HEALTHCHECK that curls /healthz. If you probe at the orchestrator level instead, point it at GET /healthz — it returns 200 ok and is the intended liveness probe.

It only proves the process is alive inside the container. It can't see a broken certificate on the proxy in front of it, which is what the next page is about.