# ADR-017: All Service Routing Uses Traefik + Docker Labels — No Nginx Config Files

## Status

Accepted, 2026-06-25.

## Status History

```yaml
status_history:
  - date: 2026-06-25
    status: Proposed
    changed_by: hkl
    reason: Formalising routing architecture — undocumented but followed for all services
    changed_via: adr-kit (360lm)
  - date: 2026-06-25
    status: Accepted
    changed_by: hkl
    reason: All services on VPS use Traefik labels; pattern stable
    changed_via: adr-kit (360lm)
```

## Context

360lm runs 10+ Docker containers on a single VPS (PostgREST, httpd, OCR proxy, dispatch-ai, slides proxy, tour-pg proxy, Paperclip, OpenClaw, etc.). Each needs HTTPS routing from the public domain. A traditional Nginx reverse proxy would require: editing a config file per new service, testing the config, reloading Nginx — and the config file becomes a long list of location blocks that must be kept in sync with running containers. If a container is renamed or removed, the Nginx config must be manually updated or it serves 502 errors.

## Decision

Traefik is the reverse proxy for all services. Routing is declared via Docker labels on each container in `docker-compose.yml` — Traefik autodiscovers them at runtime with no reload required. New service = add labels to compose file, `docker compose up -d <service>`. No Nginx config files for routing. (Nginx may run *inside* a container to serve static files — e.g., `360lm-web` uses Apache httpd — but it never does cross-service routing.)

All services must be on the `traefik-net` network (mapped to external `root_default`) to be reachable by Traefik.

Standard label pattern for a new service:
```yaml
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myservice.rule=Host(`${WEB_DOMAIN}`) && PathPrefix(`/my-path`)"
  - "traefik.http.routers.myservice.entrypoints=websecure"
  - "traefik.http.routers.myservice.tls=true"
  - "traefik.http.routers.myservice.tls.certresolver=mytlschallenge"
  - "traefik.http.services.myservice.loadbalancer.server.port=<PORT>"
networks:
  - traefik-net
```

**Decision Maker:** hkl

## Alternatives Considered

- **Nginx reverse proxy with config files per service.** Rejected: config files must be manually synced with running containers; requires `nginx -t && nginx -s reload` on every change; a removed container leaves a dangling upstream with no auto-cleanup.
- **Caddy as reverse proxy.** Rejected: Traefik was already in place when the project started; migration cost with no functional benefit; Docker label integration is equivalent between the two.
- **Direct port exposure per container (no reverse proxy).** Rejected: no TLS termination; requires opening many ports on the VPS firewall; no path-based routing; breaks the single-domain URL scheme all PWAs share.
- **Cloudflare Tunnels.** Rejected: adds external dependency; routing decisions leave the VPS; not suitable for self-hosted PostgREST with DB credentials in transit. // ponytail: upgrade trigger=DDoS protection needed or CDN caching required

## Consequences

**Positive:**
- New service: add labels to docker-compose.yml + `docker compose up -d` — Traefik picks it up immediately, no reload.
- Container removed: Traefik stops routing to it automatically — no stale upstreams.
- TLS certificates auto-managed via Let's Encrypt (certresolver=mytlschallenge).
- Dashboard at Traefik's internal port for live routing visibility.

**Negative / Trade-offs:**
- Traefik label syntax is verbose and easy to mistype — a wrong label silently fails with a 404.
- All containers must join `root_default` network — forgetting `networks: traefik-net` is a common mistake.
- Traefik itself is a single point of failure — if it crashes, all services go offline simultaneously.

**Risks and mitigations:**
- Traefik crash: mitigated by `restart: always` on the Traefik container.
- Label typo → 404: mitigated by testing new service route in dev before promoting to prod.
- Dev domain routing: Traefik rule must use `(Host(prod) || Host(dev.prod))` when a service should be reachable on both domains (see ADR-015).

## Related Decisions

- ADR-015 (dev/prod two stacks) — both domains served through the same Traefik instance.
- ADR-010 (cross-schema via proxy) — proxy services added to Traefik using this label pattern.

## References

- `memory/infra_vps.md` — Traefik rationale, container list, label pattern
- `/root/360lm-web/docker-compose.yml` — all service definitions with labels
