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
cd deploymake init # generate .env, secrets, render templated configs$EDITOR .env # fill ZPA_CLIENT_ID, ZPA_CLIENT_SECRET, ZPA_CUSTOMER_ID, ZPA_VANITY, ZPA_IDPmake buildmake upmake show-admin # print the generated admin passwordmake ca # extract Caddy root CA -> ./painscaler-ca.crtAdd to /etc/hosts on every machine that should reach the stack:
<docker-host-ip> painscaler.lan auth.lanTrust painscaler-ca.crt in your browser/OS, then visit
https://painscaler.lan.
Make targets
| target | purpose |
|---|---|
make help | list everything |
make init | env + secrets + rendered configs |
make up | start the stack |
make down | stop (keep volumes) |
make logs | tail every container’s logs |
make ca | extract the root CA cert |
make rotate | regenerate all secrets (invalidates sessions) |
make hash PASSWORD=xxx | argon2id-hash a custom password |
make mfa | tail Authelia notifications.txt for the TOTP enrolment URL |
make nuke | wipe volumes (destructive) |
Generated files
| Path | What |
|---|---|
secrets/ | Random secrets (gitignored, mode 600). Never commit. |
authelia/configuration.yml | Rendered from .tmpl (gitignored) |
authelia/users_database.yml | Rendered from .tmpl (gitignored) |
.env | ZPA credentials (gitignored) |
What’s actually exposed
80, 443 -> caddy8080 -> 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:
make mfa # tail it liveOpen 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:
- Replace
local_certsinCaddyfilewith the production directive (Let’s Encrypt by default - just removetls internal/local_certs). - Open ports 80 and 443 publicly.
- Point DNS at the host.
Authelia’s user database, session secrets, and the rest of the stack require no changes.