infrastructure
Platform engineering
Composable Docker infrastructure
Independent edge and database stacks that applications join, so TLS and Postgres lifecycle stay out of every service repo.
Docker Compose · Traefik · PostgreSQL · Shell
Application stacks should attach to a proxy and a database. They should not each reinvent TLS issuance, HTTP routing, or backup policy.
This pair of repositories is the platform layer those applications join.
Architecture
The infrastructure is two Compose stacks, not one monolith.
Internet
│
▼
┌─────────────────┐
│ infra-traefik │
│ Traefik │
└────────┬────────┘
│ infra_proxy_net
┌────┼────┐
▼ ▼ ▼
App App App
│ │ │
└────┼────┘
▼
┌─────────────────┐ infra_db_net
│ infra-postgres │ (internal)
│ PostgreSQL │
└─────────────────┘
infra-traefik publishes web (:80, redirect) and websecure (:443). The Docker provider is pinned to infra_proxy_net with exposedByDefault: false, so only labeled containers are routed.
infra-postgres puts the cluster on infra_db_net with internal: true. Data lives in a named volume. Cluster config is bind-mounted; application roles are not.
Why separate stacks?
| Concern | Owner |
|---|---|
| TLS termination | Traefik |
| HTTP routing | Traefik (application labels) |
| Certificate issuance | Host (acme.sh), not Traefik ACME |
| Application lifecycle | Application repo |
| PostgreSQL lifecycle | infra-postgres |
| Backups | infra-postgres, opt-in |
| Application roles and schema | Application deployment |
| Network boundaries | Docker networks |
Engineering constraint
Application repositories should not own TLS or PostgreSQL lifecycle.
Splitting edge and data means either stack can change without the other. A Traefik bump does not imply a Postgres bump.
Security boundaries
Traefik mounts the Docker socket read-only and keeps the dashboard off. Shared middleware (security-headers@file) is opt-in: HSTS, nosniff, frame deny, stripped Server / X-Powered-By.
Certificates are issued on the host and mounted read-only. Traefik does not run ACME in this design. File-provider watch reloads dynamic YAML; replacing cert files on disk still needs a Traefik restart.
Postgres authentication is scram-sha-256. Init SQL installs core extensions (pgcrypto, citext, uuid-ossp, pg_stat_statements) and nothing about application users. create-db.sh creates an empty database. Roles stay with the app or with external IaC.
Operations
Backups are off until BACKUP_ENABLED is set. Then backup.sh writes custom-format dumps, and rotate-backups.sh can promote a GFS layout. Restore is explicit and destructive (pg_restore --clean --if-exists). There is no silent cron in the core compose file.
Images are versioned from env (traefik:v3.3, postgres:17-alpine). Healthchecks use Traefik’s ping endpoint and pg_isready.
There is no GitHub Actions pipeline in these repos. Validation is local: scripts/validate-config.sh runs compose config, checks static files and TLS paths, and can ping a running Traefik.
What this demonstrates
Platform ownership: networks, TLS at the edge, database lifecycle, and backup policy as shared infrastructure, not copy-paste into every app.