Skip to content
BoringStack
GitHub

Profiles & overlays

4 min read

Compose overlays

The infra stack defaults small: Postgres + Valkey + your apps. Everything else (observability, error tracking, queue dashboard, image-update detection, local email catcher) is opt-in via a flag. Traefik runs in the prod profile only; dev uses Vite’s dev-server proxy for same-origin DX. Composition happens in dev.sh, which assembles the docker-compose invocation based on env vars.

1

base compose stack

5

opt-in overlays

2

stack profiles

flowchart LR
  base["base stack<br/>postgres · valkey<br/>api · ui"]
  stack{"STACK= ?"}
  dev["+ development-labels<br/>host ports for data services"]
  prod["+ production-labels<br/>traefik · HTTPS · ACME · path routing"]
  obs["+ observability"]
  glitch["+ glitchtip"]
  bullmq["+ bullmq (dev only)"]
  wud["+ wud"]
  mailpit["+ mailpit (dev only)"]
  base --> stack
  stack -->|dev| dev
  stack -->|prod| prod
  base -.->|WITH_OBSERVABILITY=1| obs
  base -.->|WITH_GLITCHTIP=1| glitch
  base -.->|WITH_BULLMQ=1| bullmq
  base -.->|WITH_WUD=1| wud
  base -.->|WITH_MAILPIT=1| mailpit

The result is a single docker compose -f ... -f ... --profile ... command. dev.sh is plain bash; you can read exactly what gets merged.

Base stack always-on, everything else opt-in

First-time setup boots fast and uses minimal RAM.

Flags compose freely (WITH_OBSERVABILITY=1 WITH_GLITCHTIP=1 ...)

No combinatorial config files; each overlay is independent.

Separate dev / prod overlays for labels

HTTPS, ACME, and security headers live in prod-only files.

WITH_BULLMQ only valid in dev

Bull-board has no auth and no place in production.

Profiles + overlay files (not one giant file)

docker compose config stays readable; overlays can be skipped cleanly.

WITH_OBSERVABILITY=1

Adds Prometheus, Grafana, Loki, Promtail, Alertmanager, and exporters.

WITH_GLITCHTIP=1

GlitchTip (Sentry-compatible error tracking); reuses base Postgres + Valkey.

WITH_BULLMQ=1

Bull-board UI at bullmq.localhost; dev only.

WITH_WUD=1

WUD watches container images. In prod, app images (api, ui) are auto-deployed while base images remain notify-only; Discord/Slack webhooks are optional.

WITH_MAILPIT=1

Mailpit SMTP catcher at :8025; dev only.

Combinations: WITH_OBSERVABILITY=1 WITH_GLITCHTIP=1 ./scripts/compose-up.sh is supported (and runs in CI).

API + UI

dev: bind-mounted source, hot reload. prod: pre-built images pulled from GHCR.

Traefik

dev: not started; Vite dev-server proxies /api/* directly. prod: started; terminates TLS, path-routes /api/* and /health to api, everything else to ui.

Host(s)

dev: http://localhost:3001. prod: https://${PUBLIC_UI_HOST} (one domain, same-origin).

TLS

dev: none. prod: Let’s Encrypt ACME via Traefik.

Data ports

dev: Postgres on :5432, Valkey on :6379 published to host. prod: internal-only, not published.

STACK=prod adds Traefik and path-routing on top of the same data plane (Postgres + Valkey). No CORS in either profile.

Read Live Config & Status
$ STACK=dev WITH_OBSERVABILITY=1 ./dev.sh config | less
$ ./dev.sh ps

#   docker-compose.yml merged with docker-compose.observability.yml
ok  postgres-dev is up on 5432
ok  valkey-dev is up on 6379
ok  api-dev is up on 3000
ok  ui-dev is up on 3001

./dev.sh forwards every argument to docker compose with the merged file list; so any compose command works (logs, exec, top, etc.).

  1. Write a new docker-compose.<name>.yml with the additional services.
  2. Add a WITH_<NAME>=1 clause in dev.sh mirroring the existing ones.
  3. Document the flag in compose/.env.example.
  4. Update the Commands cheatsheet.

Overlays are independent files, so you can ship one without touching the base.

compose/dev.sh; the orchestrator. compose/docker-compose.*.yml; the base + overlays.