Reverse proxy
Opt-in Caddy or Traefik for TLS termination and routing on plain Docker and Swarm — custom domains, Let's Encrypt, and the socket-proxy security model.
Pin images.proxy when you need HTTPS at the edge on plain docker compose or
Docker Swarm. Kubernetes uses cluster Ingress / Gateway API instead — this page
is for the compose recipes.
The one rule
Leave the no-proxy default until you need TLS or horizontal scale on compose. Then pick Caddy
(simplest automatic HTTPS) or Traefik (label auto-discovery for --scale app=N). Never mount
the raw Docker socket into Traefik.
Quick start
Opt in (pick one)
images: {
"store.sql": "postgres:18-alpine",
// …
proxy: "caddy:2-alpine", // or "traefik:v3.3"
},Pin either Caddy or Traefik — never both. Re-run oke docker --prod.
Set the public host and ACME email
OKE_PROXY_HOST=app.example.com
OKE_PROXY_ACME_EMAIL=admin@example.com.env.docker is preserved across regeneration. Point DNS A/AAAA at the host.
Bring the stack up
cd docker
docker compose -f compose.yml -f compose.store.sql.yml \
-f compose.proxy.yml -f compose.prod.yml up -dPass every -f file oke docker listed. The edge publishes 80/443; app
stays on the internal oke network (no host bind on 6530).
Add your public hostname to serve allowedHosts
(Security).
Which proxy?
| Choice | Best for | How routing works |
|---|---|---|
Caddy (caddy:2-alpine) | hello / minimal / standard — single instance, simplest TLS | Generated Caddyfile → reverse_proxy app:6530; automatic HTTPS |
Traefik (traefik:v3.3) | Horizontally scaled plain compose | Docker labels on app; replicas auto-discovered and load-balanced |
(omit images.proxy) | Local / private networks | App publishes 6530 — default, unchanged |
Consequence: Caddy is the recommended default when you just need a certificate.
Choose Traefik when docker compose up --scale app=N is the deployment shape.
Caddy
Emits docker/Caddyfile:
{$OKE_PROXY_HOST:localhost} {
reverse_proxy app:6530
}| Setting | What it does |
|---|---|
OKE_PROXY_HOST | Site address. localhost → local TLS; a public DNS name → Let's Encrypt HTTP-01 |
OKE_PROXY_ACME_EMAIL | Written into .env.docker for the proxy role. Traefik consumes it; for Caddy, add a global email block if you want a specific ACME contact |
Custom domain:
- DNS for
app.example.com→ this host OKE_PROXY_HOST=app.example.comindocker/.env.docker- Ports
80/443reachable from the public internet allowedHostsincludesapp.example.com
Override the file via compose.override.yml or replace docker/Caddyfile —
oke docker regenerates the default on the next run.
Traefik
Generated compose.proxy.yml labels app:
| Label | Meaning |
|---|---|
traefik.enable=true | Advertise this service to Traefik |
traefik.http.routers.app.rule=Host(…) | Match OKE_PROXY_HOST (default localhost) |
traefik.http.routers.app.entrypoints=websecure | HTTPS entrypoint |
traefik.http.routers.app.tls.certresolver=letsencrypt | ACME via Let's Encrypt |
traefik.http.services.app.loadbalancer.server.port=6530 | Upstream port on the compose network |
HTTP (:80) redirects to HTTPS. ACME email comes from
OKE_PROXY_ACME_EMAIL (default admin@example.com) on the Traefik command line.
Scaled replicas need no label edits — docker compose up --scale app=N registers
every replica through the Docker provider.
Why docker-socket-proxy (Traefik)
Traefik's Docker provider watches the Engine API for containers and labels.
The naive setup mounts /var/run/docker.sock into Traefik itself.
That socket is root-equivalent for the host Docker daemon. If the edge process
is compromised (public 80/443), an attacker with the raw socket can start
privileged containers, mount the host filesystem, or escape the container.
Traefik documents this risk
and recommends a filtered proxy. OKE never mounts docker.sock into Traefik —
a companion tecnativa/docker-socket-proxy mounts it read-only and exposes only:
| Flag | Why Traefik needs it |
|---|---|
CONTAINERS | List / inspect for labels |
EVENTS | Watch start/stop for scale |
PING | Health of the Engine API |
VERSION | API negotiation |
NETWORKS | Network membership |
Destructive Engine calls (images, volumes, exec, swarm admin, …) stay denied.
Traefik uses --providers.docker.endpoint=tcp://socket-proxy:2375 on the
internal oke network only — do not publish 2375 on the host.
Consequence: a compromised Traefik still talks to a narrow API surface, not the full Docker control plane.
Environment
| Variable | Default | Used by |
|---|---|---|
OKE_PROXY_HOST | localhost | Caddy site address; Traefik Host() rule |
OKE_PROXY_ACME_EMAIL | admin@example.com | Traefik ACME account email |
Where this applies
| Surface | Edge / ingress |
|---|---|
| Docker | Opt-in Caddy or Traefik (images.proxy) |
| Docker Swarm | Same proxy recipes + Swarm routing mesh |
| Kubernetes | Ingress / Gateway API (cluster-native — not this recipe) |
Troubleshooting
You still have 6530:6530 on app. Confirm images.proxy is set and
compose.proxy.yml is in the -f list so derivation omitted the host bind.
OKE_PROXY_HOST must be a public DNS name pointing at this host; ports 80/443
reachable. Set OKE_PROXY_ACME_EMAIL for Traefik. For localhost-only smoke tests, prefer Caddy.
Confirm app labels are present (traefik.enable=true) in compose.proxy.yml,
containers share the oke network, and socket-proxy is healthy. Traefik must
not mount docker.sock itself.
Serve validates Host. Add your public hostname to allowedHosts
(Security).
OKE_PROXY_HOST is still localhost (or unset). Set the public DNS name in
docker/.env.docker and recreate the proxy container so it reloads the Caddyfile.
Learn more
- Deployment — pick compose, Swarm, or Kubernetes
- Docker — plain compose path (includes proxy opt-in)
- Docker Swarm —
docker stack deploy - CLI —
oke docker,oke docker --prod - Configuration —
imagespins - Security —
allowedHostsbehind a reverse proxy