Docker Compose and Traefik
How make up turns one YAML file into a running exchange platform: containers, networks, volumes, health checks and start order, then a reverse proxy that gives every service a friendly name on a single port.
1. The problem
Prayog needs PostgreSQL, Kafka, Redis, Keycloak and a proxy, each at a specific version and configuration, started in the right order, on every developer's machine and in CI. Installing them by hand gives "works on my machine" bugs. Compose describes the whole stack in one file, so everyone runs exactly the same thing.
2. A business example: the shopping mall
- Each shop is a container: self-contained, built from a standard fit-out plan (the image).
- The mall plan (Compose file) says which shops exist, where their storerooms are (volumes) and which shop needs which other one to open first (depends_on).
- A shop's door being unlocked doesn't mean it's ready to serve. The manager waits for the "open" sign (health check) before sending customers.
- The front desk (Traefik) is the only public entrance. Ask for "Keycloak" and you're walked to that shop; ask for a shop that doesn't exist and you get "no such shop" (404).
3. Images and containers
- Image: a read-only file system in layers plus a default command, such as
postgres:16.15-alpine. Built once, pulled from a registry (Docker Hub, quay.io). - Container: a running process isolated with Linux namespaces (its own file system view, network, process list) and limited with cgroups (memory, CPU). Not a virtual machine: it shares the host kernel, so it starts in milliseconds. On a Mac, Docker Desktop runs one small Linux VM that hosts all containers.
- Tags vs. digests: a tag like
16-alpinemoves when a new patch is published;16.15-alpinerarely does; a digest (@sha256:...) never does. Prayog pins exact patch tags. - Architecture: images are published per CPU type. All of Prayog's images ship ARM builds, so they run natively on Apple Silicon.
4. Compose basics
| Concept | What it does | Prayog example |
|---|---|---|
| Service | A container definition: image, command, environment | kafka, keycloak |
| Network | Compose puts all services on one private network with DNS: each service is reachable by its name | Keycloak connects to postgres:5432; containers use kafka:9092 |
| Port mapping | Publishes a container port on the host | 127.0.0.1:5432:5432: only your own machine can connect |
| Named volume | Storage that outlives the container | postgres-data, kafka-data, redis-data |
| Bind mount | A host folder inside the container | The realm file at /opt/keycloak/data/import, read-only |
Environment and .env | ${VAR} is filled from .env; ${VAR:?msg} refuses to start if it's missing | Every password and client secret |
| Profiles | Start only groups of services | infra now, app from S10, obs in S21 |
| One-shot job | A container that does one task and exits 0 | kafka-init creates the topic |
5. Started vs. ready
A container "starts" when its process starts. PostgreSQL then needs a few seconds before it accepts connections; Keycloak needs about 20. If Keycloak starts first and tries to connect, it fails and exits.
| Tool | Waits for |
|---|---|
depends_on: [postgres] | Postgres started. Not enough. |
depends_on: postgres: condition: service_healthy | Postgres's health check passing |
condition: service_completed_successfully | A one-shot job exiting 0 |
docker compose up --wait | Every service healthy (or running, if it has no check) before the command returns |
A health check should ask the question clients care about: pg_isready rather than "is port 5432 open", a real Kafka API call, Keycloak's /health/ready. Prayog's Keycloak check opens a TCP connection with bash's built-in /dev/tcp, because the image has no curl.
6. Try it: start order
Each service takes a while to start and then to become ready. Compare plain depends_on with health conditions.
7. Reverse proxy and Traefik
A reverse proxy receives every request on one port and forwards it to the right service. Benefits: one address to remember, friendly host names, one place for TLS, redirects and access logs, and services that never expose ports themselves.
Traefik's model:
| Piece | Meaning | Prayog |
|---|---|---|
| Entrypoint | A port Traefik listens on | web = :80 |
| Router | A rule that matches requests | Host(`auth.prayog.localhost`) |
| Service | Where matched requests go | Keycloak container port 8080 |
| Middleware | Changes on the way (auth, headers, rate limits) | None yet |
| Provider | Where Traefik learns routes | Docker: labels on each container, read through the Docker socket |
With exposedbydefault=false, a container is routed only if it says traefik.enable=true, so nothing is published by accident. Traefik forwards the original host and client address in X-Forwarded-* headers; Keycloak trusts them (KC_PROXY_HEADERS=xforwarded) to build correct URLs.
.localhost? RFC 6761 reserves localhost and every name under it for the local machine. Browsers, curl and (on this Mac) Python resolve anything.prayog.localhost to 127.0.0.1 with no DNS setup. Some tools don't, hence the /etc/hosts line in the README.8. Try it: routing
Type a host name. The table of routers is Prayog's real one.
9. Security edges
- Ports bound to 127.0.0.1.
"5432:5432"would listen on every network interface, so anyone on the same Wi-Fi could reach your database."127.0.0.1:5432:5432"keeps it on your machine. - The Docker socket. Whoever can talk to it controls Docker, which is close to root on the host. Read-only stops writes, but reading still reveals every container's environment, secrets included. Fine on a laptop; hosted setups put a filtering socket proxy in between or use file-based routes.
- Plain HTTP. Acceptable only on loopback. Keycloak's
sslRequired: externalenforces HTTPS for non-private addresses. - Secrets come from a git-ignored
.envgenerated per machine; CI generates throwaway ones.
10. In Prayog
| Command | Does |
|---|---|
make env | Creates .env with random secrets (once) |
make up | compose --profile infra up -d --wait: returns when all is healthy |
make smoke | 20 end-to-end checks; also run in CI |
make ps, make logs SERVICE=kafka | Status and logs |
make down / make reset | Stop, keeping data / stop and delete data (needed after realm or init changes) |
Measured on an 8 GB M1: about 2.5 minutes for the first start (image pulls), 22 seconds afterwards, about 1.1 GB of memory in total.
11. Questions
How does this map to Kubernetes?
Services become Deployments (or StatefulSets for databases and Kafka), health checks become readiness and liveness probes, Traefik's labels become Ingress or Gateway API routes, named volumes become PersistentVolumeClaims, and .env becomes Secrets. The ideas carry over directly.
Why Traefik and not nginx?
Both work. Traefik discovers routes from container labels and updates them live, so adding a service needs no proxy config file and no reload. nginx needs a config entry per service.
Why not run Keycloak without a database for local use?
Its development mode can use an embedded file database, but then the local setup differs from a real one. Running it on PostgreSQL costs about 50 MB and tests the real path.