Skip to content

Docker compose

The repository ships a deploy/ directory with a four-container stack:

  • caddy - TLS termination (self-signed via local_certs), forward-auth integration with Authelia.
  • painscaler-api - the Go binary, distroless, port 8080 internal only.
  • painscaler-web - nginx serving the built React SPA.
  • authelia - file-based auth with TOTP MFA.

Caddy is the only container with published ports. Everything else lives on the painscaler bridge network and is reached through Caddy.

Quickstart

Terminal window
cd deploy
make init # generate .env, secrets, render templated configs
$EDITOR .env # fill ZPA_CLIENT_ID, ZPA_CLIENT_SECRET, ZPA_CUSTOMER_ID, ZPA_VANITY, ZPA_IDP
make build
make up
make show-admin # print the generated admin password
make ca # extract Caddy root CA -> ./painscaler-ca.crt

Add to /etc/hosts on every machine that should reach the stack:

<docker-host-ip> painscaler.lan auth.lan

Trust painscaler-ca.crt in your browser/OS, then visit https://painscaler.lan.

Make targets

targetpurpose
make helplist everything
make initenv + secrets + rendered configs
make upstart the stack
make downstop (keep volumes)
make logstail every container’s logs
make caextract the root CA cert
make rotateregenerate all secrets (invalidates sessions)
make hash PASSWORD=xxxargon2id-hash a custom password
make mfatail Authelia notifications.txt for the TOTP enrolment URL
make nukewipe volumes (destructive)

Generated files

PathWhat
secrets/Random secrets (gitignored, mode 600). Never commit.
authelia/configuration.ymlRendered from .tmpl (gitignored)
authelia/users_database.ymlRendered from .tmpl (gitignored)
.envZPA credentials (gitignored)

What’s actually exposed

80, 443 -> caddy
8080 -> painscaler-api (intra-network only, scrape /metrics from inside the network)
80 -> painscaler-web (intra-network only, served via Caddy)
9091 -> authelia (intra-network only, forward-auth target)

Anything you can reach from outside goes through Caddy. Caddy enforces authentication via Authelia before forwarding upstream. Direct API access from outside the compose network is impossible because painscaler-api only exposes via expose:, never ports:.

Domain note

Default uses painscaler.lan and auth.lan. Change in Caddyfile and authelia/configuration.yml.tmpl (session.cookies[0].domain, authelia_url, default_redirection_url) to use a different suffix.

.local triggers mDNS resolution on macOS and Linux - avoid. .lan and .home.arpa are safe.

First MFA enrolment

Authelia’s file-notifier writes TOTP enrolment links + codes to authelia/notifications.txt:

Terminal window
make mfa # tail it live

Open the link from a fresh tab to register your authenticator app.

Going public

Default config uses local_certs (Caddy’s built-in self-signed root CA). For a public deployment with painscaler.com:

  1. Replace local_certs in Caddyfile with the production directive (Let’s Encrypt by default - just remove tls internal / local_certs).
  2. Open ports 80 and 443 publicly.
  3. Point DNS at the host.

Authelia’s user database, session secrets, and the rest of the stack require no changes.