Customize Companion Hub compose and Traefik config
Change how the Hub stack publishes ports and how Traefik routes traffic. This page covers what Hub applies today and what it doesn’t, because several older ways of customizing the stack no longer take effect.
This guide is for operators who already know Docker and Traefik. Broken changes can take the Hub offline, and support may not be able to recover a customized main stack for you.
What doesn’t work in the current release:
- A
user-config/hub-compose.ymloverride isn’t applied when you start the stack withcihub upor the desktop app. Hub reads it only when it builds its own log commands. It never changes the running containers. - Hub rewrites
state/traefik/config/traefik.ymlevery time the Hub container starts, so an edit lasts only until the next restart. The Persist Traefik config setting andpersistTraefikConfiginsettings.jsondon’t change this. - Traefik’s dashboard and API stay disabled (
insecure: false), by design. Hub turns them on only for source development.
What Hub generates
Your Hub data root (ROOT_FOLDER_HOST) holds the files that configure the stack. Find its path with cihub doctor.
- traefik.yml
- dynamic.yml
- acme_storage.json
- settings.json
- docker-compose.prod.yml
- .env.dev
| File | Written by | Safe to edit? |
|---|---|---|
| state/traefik/config/traefik.yml | Hub, on every Hub start | No — overwritten on restart |
| state/traefik/dynamic/dynamic.yml | Hub, the desktop app, and `cihub up`, on start | No — overwritten on restart |
| Other files in state/traefik/dynamic/ | You, using names other than hub.yml, apps.yml, and port-expose.yml | Yes — Traefik watches the folder |
| state/traefik/acme_storage.json | Traefik, as it issues certificates | No |
| docker-compose.prod.yml | The desktop app, copied from its bundle on every launch | No — the desktop app overwrites it |
| .env.dev (.env on Windows) | The CLI, and the desktop app on every launch | Only for a Hub you start with cihub up |
Change the published ports
Hub publishes these ports on your machine. On a Hub you start with cihub up, set the variable in your env file — .env.dev in the data root on Linux and macOS, .env on Windows — then restart the stack.
The desktop app rewrites the env file every time it starts. It keeps only its
own values, such as ROOT_FOLDER_HOST, the generated secrets,
TAILSCALE_AUTHKEY, and TAILSCALE_SERVE_USER_DISABLED, and its port manager
picks the port variables below afresh on each launch. On the desktop app,
changes to this file don’t last. This section, and the Let’s Encrypt email
below, apply to a Hub that you start with cihub up.
| Port | Service | Variable |
|---|---|---|
| 5002 | Hub dashboard and API | API_PORT |
| 80 (8880 in the Linux desktop app) | Traefik, HTTP | HTTP_PORT |
| 443 (8443 in the Linux desktop app) | Traefik, HTTPS | HTTPS_PORT |
| 8080 | Traefik dashboard port | TRAEFIK_DASHBOARD_PORT |
| 6543 | PostgreSQL, on 127.0.0.1 only | POSTGRES_PORT |
| 5001 | RabbitMQ, on 127.0.0.1 only | RABBITMQ_PORT |
cihub down
cihub up --detachedIf a port is busy when the stack starts, the CLI picks a free one and writes it to the env file for you. See Hub stack won’t start.
Set the Let’s Encrypt email
Hub writes ACME_EMAIL into traefik.yml at startup. If you don’t set it, Hub uses admin@localhost. Set it in the env file to receive certificate expiry notices at a real address:
ACME_EMAIL=you@example.comOn Linux, only root can open ports below 1024, so the desktop app publishes Traefik on 8880 and 8443. cihub up on rootful Docker uses 80 and 443 when they are free and otherwise moves to 8880 and 8443; under rootless Docker it always uses those two. Public Web through Cloudflare doesn’t depend on these host ports.
Restart the Hub for the change to apply.
dynamic.yml holds the TLS 1.2 floor, the forward-auth middleware that passes Hub’s signed identity headers to your apps, the middleware that strips forwarding headers a visitor sets, and the <app> is starting… page Hub shows when an exposed app answers 502 or 504. It points at the Hub container, ci-hub.
Add your own Traefik routes
Traefik watches state/traefik/dynamic/ and reloads a file when it changes, with no restart. Hub writes dynamic.yml, hub.yml, apps.yml, and port-expose.yml there, so use other file names and your files survive restarts. Use a separate file for routers, services, middlewares, and TLS options.
For example, this routes printer.example.lan through the Hub’s Traefik to a device on your network. Save it as state/traefik/dynamic/printer.yml:
http:
routers:
printer:
rule: Host(`printer.example.lan`)
entryPoints:
- web
service: printer
services:
printer:
loadBalancer:
servers:
- url: http://192.168.1.50:80Dynamic configuration can’t change Traefik’s static settings, such as entry points, certificate resolvers, and the log level. Those live in traefik.yml, which Hub rewrites.
Read Traefik’s logs
Traefik logs JSON at INFO level. Read them with Docker:
docker logs traefikSee the Log viewer for the rest of the stack.
Customize one app instead
To add environment values or host mounts to a single app, use the per-app override files in user-config/. See Customize app config.