Skip to main content

Traefik

Traefik is bundled in the Full Docker layout (docker-compose.full.yml) and the Swarm layout (docker-compose.swarm.yml). It runs as the traefik service, terminates HTTP on :80, routes by path prefix and exposes a dashboard at /traefik. There's nothing to install separately.

TLS comes via two additive compose overlays wired by install.sh --ssl:

ModeOverlayWhen
letsencryptdocker-compose.tls-letsencrypt.ymlPublic-internet hosts. ACME TLS-ALPN challenge — needs :80 and :443 open from the public internet. Cert + renewal handled by Traefik automatically.
provideddocker-compose.tls-provided.ymlCorporate CA, internal PKI, air-gapped install. Operator supplies the .crt and .key; Traefik serves them via the file provider.
none (default)(no overlay)HTTP-only on :80. Fine for local dev or behind another reverse proxy that terminates TLS upstream.

For the conceptual map of compose files + scripts, read Docker first. For the cross-page COMPOSE_FILE rule, see Docker → COMPOSE_FILE discipline.


What's pre-wired

Routing priorities

The full layout uses path-prefix routing on a single Traefik HTTP entrypoint (:80). Higher priority wins; the catchall liberty-next router runs last:

RouterRulePriorityService
traefik-dashboardPathPrefix(`/traefik`) + a strict allow-list of /api/* GETs1000Traefik's internal api@internal service.
pgadminPathPrefix(`/pgadmin`)100pgAdmin container on :80.
portainerPathPrefix(`/portainer`) (stripped to /)100Portainer container on :9000.
liberty-nextPathPrefix(`/`) (catchall)1liberty-next container on :8000.

Liberty-next stays the catchall — every request that doesn't match a more specific prefix lands there. Adding a new app behind the same Traefik just means a new router at priority > 1.

Sticky cookies for Socket.IO

Liberty-next keeps Socket.IO state in-process. When you scale it to more than one replica, the sticky cookie pins each client to the replica it first hit:

docker-compose.full.yml (excerpt)
traefik.http.services.liberty-next.loadbalancer.sticky.cookie.name: "liberty_sticky"

The cookie is on by default — harmless at replicas: 1, essential at > 1. Note: stickiness keeps a given client pinned, but it does NOT share Socket.IO state across replicas. Live dashboards / chat streams emitted by one replica won't reach clients pinned to another. For real multi-replica, a Redis adapter is needed — not yet built in.

The dynamic config

Traefik reads two kinds of configuration:

SourceWhat
Container labelsPer-service routes and middlewares, declared inline in the compose file.
traefik/dynamic/*.ymlShared middlewares whose values contain $ (basic-auth bcrypt hashes — Compose substitution would eat them) and TLS cert references for the provided mode (tls.yml, generated by install.sh).

Two flags wire it up:

docker-compose.full.yml (Traefik command)
- --providers.file.directory=/etc/traefik/dynamic
- --providers.file.watch=true

file.watch=true means edits reload in seconds, no container restart needed.

The bundled dynamic file ships three middlewares:

MiddlewareWhat it doesUse
traefik-authBasic-auth gate (default admin / admin — bcrypt hash).Attached to the dashboard router via traefik-auth@file.
security-headersframeDeny, contentTypeNosniff, browserXssFilter, referrerPolicy: no-referrer-when-downgrade. STS lines commented out (uncomment when serving HTTPS).Attach to any router with <router>.middlewares: "security-headers@file".
redirect-to-https301 redirect HTTP → HTTPS.Attach to every HTTP-entrypoint router once the websecure entrypoint is enabled.

When --ssl provided is wired, install.sh also generates traefik/dynamic/tls.yml (gitignored) referencing the cert + key filenames inside /etc/certs. Operators can edit it manually for fancier setups (multi-domain, separate intermediates, SNI rules) — Traefik picks the edits up via file.watch.


Change the dashboard password

The default admin / admin works for the first 30 seconds. Then change it.

docker run --rm httpd:alpine htpasswd -nbB admin "<your-password>"
# admin:$2y$05$abc...

Paste the one line of output into release/traefik/dynamic/dynamic.yml under http.middlewares.traefik-auth.basicAuth.users:

release/traefik/dynamic/dynamic.yml
http:
middlewares:
traefik-auth:
basicAuth:
users:
- "admin:$2y$05$<your-new-hash>"
# Multiple users? Add one per line:
# - "ops:$2y$05$..."

file.watch=true picks it up within seconds — no container restart. Refresh the dashboard, sign in with the new credentials.

bcrypt vs apr1

htpasswd -nbB produces a bcrypt hash ($2y$). Traefik also accepts apr1 ($apr1$) but bcrypt is the recommended modern choice — works in any env without escaping issues.


Mode 1 — Let's Encrypt

For public-internet hosts. Traefik fetches the certificate via the ACME TLS-ALPN challenge, persists it in the traefik-acme named volume (mounted at /acme inside Traefik) and renews automatically.

Install

./install.sh full --ssl letsencrypt \
--domain liberty.example.com \
--email ops@example.com

Requirements:

RequirementWhy
The hostname resolves to this host (DNS A and/or AAAA record).Without DNS, ACME can't issue a cert.
:80 AND :443 reachable from the public internet.The TLS-ALPN challenge runs on :443 but Traefik also needs :80 open for HTTP-to-HTTPS redirects.
Read-write access to the traefik-acme named volume.Where the issued cert is stored.
A real, monitored email address for ACME_EMAIL.Let's Encrypt sends expiry warnings there.

What install.sh does:

  1. Appends docker-compose.tls-letsencrypt.yml to COMPOSE_FILE in .env.
  2. Sets LIBERTY_DOMAIN=<hostname> and ACME_EMAIL=<address> in .env.
  3. Removes any leftover traefik/dynamic/tls.yml from a previous provided install.
  4. Runs docker compose up -d — Traefik comes up on :80 + :443 and requests the cert on the first HTTPS request.

The overlay (small):

docker-compose.tls-letsencrypt.yml
services:
traefik:
environment:
TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443"
TRAEFIK_CERTIFICATESRESOLVERS_LE_ACME_EMAIL: "${ACME_EMAIL:?ACME_EMAIL required for letsencrypt mode}"
TRAEFIK_CERTIFICATESRESOLVERS_LE_ACME_STORAGE: "/acme/acme.json"
TRAEFIK_CERTIFICATESRESOLVERS_LE_ACME_TLSCHALLENGE: "true"
ports:
- "${TRAEFIK_HTTPS_PORT:-443}:443"
labels:
traefik.http.routers.traefik-dashboard.entrypoints: "web,websecure"
traefik.http.routers.traefik-dashboard.tls: "true"
traefik.http.routers.traefik-dashboard.tls.certresolver: "le"

liberty-next:
labels:
traefik.http.routers.liberty-next.entrypoints: "web,websecure"
traefik.http.routers.liberty-next.tls: "true"
traefik.http.routers.liberty-next.tls.certresolver: "le"

pgadmin:
labels:
traefik.http.routers.pgadmin.entrypoints: "web,websecure"
traefik.http.routers.pgadmin.tls: "true"
traefik.http.routers.pgadmin.tls.certresolver: "le"

portainer:
labels:
traefik.http.routers.portainer.entrypoints: "web,websecure"
traefik.http.routers.portainer.tls: "true"
traefik.http.routers.portainer.tls.certresolver: "le"

Three things to notice:

DetailWhy
Settings go through Traefik's TRAEFIK_<UPPER_DOT_TO_UNDER>=value env-var interface.Compose overlay-merging is REPLACE-by-key for array values (command:) and ADD-by-key for env. Using env-vars avoids losing the base compose's CLI flags when the overlay merges.
ACME storage at /acme/acme.json.The base compose mounts traefik-acme at /acme (NOT /etc/traefik/acme/ — that path is the read-only /etc/traefik bind mount, so a second subdir mount under it isn't possible).
Every router opts into both entrypoints (web,websecure).Lets users hit either http:// or https:// until you wire a redirect — see below.

Force HTTP → HTTPS

Add the redirect-to-https@file middleware (already defined in dynamic.yml) to each router, only on the web entrypoint:

<your-override.yml> — append to COMPOSE_FILE
services:
liberty-next:
labels:
traefik.http.routers.liberty-next.middlewares: "redirect-to-https@file"

Or edit the base compose's labels directly.


Mode 2 — Operator-provided certs

For corporate CA, internal PKI or air-gapped installs that can't reach Let's Encrypt.

Install

./install.sh full --ssl provided \
--domain liberty.internal.example.com \
--cert-dir /etc/pki/tls \
--cert-file liberty.crt \
--key-file liberty.key

Requirements:

RequirementWhy
A directory on the host containing the .crt (or .pem) AND the private key file.Both bind-mounted into the container at /etc/certs:ro.
The cert file matches --cert-file and the key file matches --key-file inside that directory.install.sh validates both exist before continuing.
The cert is valid for the --domain you pass.Browsers reject hostname mismatches.

What install.sh does:

  1. Appends docker-compose.tls-provided.yml to COMPOSE_FILE.
  2. Sets CERT_HOST_PATH=<abs-path-to-cert-dir> in .env — Traefik bind-mounts that directory at /etc/certs:ro.
  3. Sets LIBERTY_DOMAIN=<hostname> in .env (or localhost if --domain was omitted).
  4. Generates traefik/dynamic/tls.yml (gitignored — re-running --ssl provided overwrites it) referencing the cert + key filenames inside /etc/certs.
  5. Runs docker compose up -d — Traefik comes up on :80 + :443 serving the supplied cert.

The overlay:

docker-compose.tls-provided.yml
services:
traefik:
environment:
TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443"
ports:
- "${TRAEFIK_HTTPS_PORT:-443}:443"
volumes:
- "${CERT_HOST_PATH:?CERT_HOST_PATH required for provided mode}:/etc/certs:ro"
labels:
traefik.http.routers.traefik-dashboard.entrypoints: "web,websecure"
traefik.http.routers.traefik-dashboard.tls: "true"

liberty-next:
labels:
traefik.http.routers.liberty-next.entrypoints: "web,websecure"
traefik.http.routers.liberty-next.tls: "true"

pgadmin: { labels: { traefik.http.routers.pgadmin.entrypoints: "web,websecure", traefik.http.routers.pgadmin.tls: "true" } }
portainer: { labels: { traefik.http.routers.portainer.entrypoints: "web,websecure", traefik.http.routers.portainer.tls: "true" } }

And the generated tls.yml:

traefik/dynamic/tls.yml (generated by install.sh --ssl provided)
tls:
stores:
default:
defaultCertificate:
certFile: /etc/certs/liberty.crt
keyFile: /etc/certs/liberty.key
certificates:
- certFile: /etc/certs/liberty.crt
keyFile: /etc/certs/liberty.key

Notice the routers do NOT carry a certresolver — the cert comes via the file-provider default store. Traefik picks it up automatically.

Edit tls.yml for more cert / SNI rules

tls.yml is gitignored and reload-watched. For multi-domain setups, replace the file by hand:

traefik/dynamic/tls.yml — multi-cert example
tls:
certificates:
- certFile: /etc/certs/liberty.crt
keyFile: /etc/certs/liberty.key
- certFile: /etc/certs/api.crt
keyFile: /etc/certs/api.key
stores:
default:
defaultCertificate:
certFile: /etc/certs/liberty.crt
keyFile: /etc/certs/liberty.key

Traefik watches the file and re-reads it within seconds.

Re-running install.sh --ssl provided overwrites tls.yml

A re-run of ./install.sh full --ssl provided with new --cert-file / --key-file values REPLACES tls.yml from the template. Custom multi-cert edits are lost. Keep a copy of your edited version, or version it outside the release/ checkout.


Switching modes later

Re-run ./install.sh full --ssl <new-mode> ... with the same secrets in place. install.sh:

  • Swaps the overlay path in COMPOSE_FILE.
  • Rewrites tls.yml (for provided) or removes it (for letsencrypt).
  • Runs docker compose up -d, which picks up the new overlay.

Operator-managed state (the traefik-acme volume, the cert host directory) is preserved across mode flips.


Compose vs Swarm

The full layout's Traefik service uses --providers.docker to discover backends from container labels. Swarm uses --providers.swarm (reads from deploy.labels instead of top-level labels). The two compose files differ accordingly:

Compose (docker-compose.full.yml)Swarm (docker-compose.swarm.yml)
Provider flag--providers.docker=true--providers.swarm=true
Network filter--providers.docker.network=liberty-network--providers.swarm.network=liberty-network
Endpointimplicit (docker.sock)--providers.swarm.endpoint=unix:///var/run/docker.sock
Labels locationTop-level labels: block per servicedeploy.labels: block per service
Middleware suffix@docker (e.g. traefik-strip@docker)@swarm (e.g. traefik-strip@swarm)
File-provider middlewares@file (same on both)@file (same on both)

Both layouts:

  • Mount the Docker socket read-only.
  • Mount ./traefik:/etc/traefik:ro for the dynamic config.
  • Use the traefik-acme named volume mounted at /acme (for cert storage in LE mode).

Swarm caveat — Traefik must run on a manager

The Swarm provider reads the Docker API on the socket — only manager nodes expose it. The bundled compose pins Traefik via:

docker-compose.swarm.yml (Traefik deploy block)
deploy:
placement:
constraints:
- node.role == manager

Swarm --ssl caveat

install.sh --ssl is Compose-only — Swarm operators apply the TLS overlay manually by passing -c repeatedly:

docker stack deploy \
-c docker-compose.swarm.yml \
-c docker-compose.tls-letsencrypt.yml \
liberty

(With LIBERTY_DOMAIN, ACME_EMAIL exported into the shell first — deploy-swarm.sh does that.)


Common operations

Reload the dynamic config

file.watch=true is on — just edit traefik/dynamic/dynamic.yml (or tls.yml) and save. Traefik picks it up in ~5 s and emits a log line. No restart.

Reload after a static-config change

Anything in the command: block of the Traefik service is static config — needs a container restart:

docker compose up -d traefik # COMPOSE_FILE picks the right files

Or, in Swarm:

docker service update --force liberty_traefik

View live routers and services

The dashboard at /traefik shows every router with its rule, entrypoint, middleware chain and the backing service. Useful when debugging "why isn't this route matching" — Traefik shows you what it sees.

Tail Traefik logs

docker compose logs -f traefik
# or in Swarm:
docker service logs -f liberty_traefik

ACME issuance / renewal lands here. So do routing errors (no available server) when a backend service is down.


Common pitfalls

MistakeSymptomFix
Domain doesn't resolve when Traefik runs the ACME challenge.Logs show acme: error 400 — DNS problem or unauthorized.Verify dig <domain> returns the server's IP from a public resolver.
:80 or :443 blocked at the firewall / cloud LB.TLS-ALPN challenge fails.Open both at the host firewall + the cloud LB. ACME needs :80 open too.
Re-ran --ssl provided — manual tls.yml edits are gone.Custom multi-cert config disappeared.Keep a copy outside release/. Or version-control your own tls.yml.
Passing -f docker-compose.full.yml after install.TLS overlay + apps overlay silently dropped on next up -d.NEVER -f after install — COMPOSE_FILE is the source of truth. See Docker → COMPOSE_FILE discipline.
Edit to dynamic.yml doesn't reload.Old basic-auth password still works.Confirm --providers.file.watch=true is in the static command block; check Traefik logs for parse errors.
Swarm Traefik fails to start with cannot connect to Docker socket.Traefik reports Cannot connect to the Docker daemon.Traefik is on a worker — Swarm rescheduled it. Pin to manager.
Catchall liberty-next intercepts /pgadmin / /portainer.pgAdmin / Portainer URLs return Liberty's SPA.Check router priorities — liberty-next must be priority 1; pgAdmin / Portainer at 100.
ACME cert stored in /etc/traefik/acme/ instead of /acme/.Cert path errors.The base compose mounts /etc/traefik:ro (read-only); ACME storage goes at /acme instead. The bundled overlay already uses /acme.

What's next