Files
duocthu/docs/adr/0010-interim-single-host-compose-deployment.md
T

3.9 KiB

ADR 0010: Single-host Docker Compose as the interim deployment

Status

Accepted. Recorded retrospectively during the 2026-08-12 documentation pass. Does not supersede ADR 0002, whose own status line says it remains the target:

Accepted — still the target, not yet implemented. Not superseded by the current production setup.

Context

ADR 0002 chose GitOps on the team's ArgoCD instance. A complete Helm chart (infra/helm/medical-chatbot/) and three ArgoCD Application manifests exist. Neither has been applied: each Application carries three unresolved TODOs (project/RBAC scope, repo URL, target cluster), infra/k8s/base|overlays/ hold only .gitkeep, and no image registry is configured anywhere.

Meanwhile the product is live at https://realvuxbaro.me.

Decision

Run production as Docker Compose on a single EC2 host, with Caddy terminating TLS, and deploy by SSH from GitHub Actions.

Verifiable from the repository:

  • infra/docker/docker-compose.prod.yml — postgres, qdrant, ai-service, web, caddy, with named volumes.
  • infra/docker/docker-compose.observability.yml — the OTel/Prometheus/Tempo/ Grafana overlay, which also sets OTEL_ENABLED=true.
  • infra/docker/Caddyfilerealvuxbaro.meweb:3000, /grafana/*grafana:3000.
  • .github/workflows/deploy.ymlappleboy/ssh-action, git reset --hard, docker compose up -d --build, caddy reload, python -m migrate, then ~18 assertions.

Consequences

Accepted trade-offs

  • Images are built on the production host and are untagged, so there is no artifact to roll back to; recovery is a revert commit plus a rebuild.
  • Deploys are in-place, with brief per-service downtime.
  • No horizontal scaling. That happens to align with the in-process agent state described in 02-system-architecture.md, but the alignment is coincidental, not enforced.
  • Configuration and secrets live in an uncommitted .env.prod on the host, so production configuration cannot be reviewed in Git.
  • postgres and qdrant are deliberately absent from the workflow's up -d list, so a code deploy never restarts the stateful services — and changes to their service definitions do not take effect until someone restarts them.

Preserved despite the simpler runtime

The deploy script asserts far more than a Compose deploy usually does: service health, a real grounded answer from the real corpus (decision=answerable with a chi_dinh citation), both Grafana datasources, the provisioned dashboard, public reachability of /grafana/login, and end-to-end trace propagation by asserting that a specific X-Trace-ID becomes retrievable from Tempo. That verification block is what makes the simpler runtime defensible.

Migration path

The Helm chart already maps every setting in config.py to a ConfigMap, mounts POSTGRES_DSN from a Secret, and configures readiness/liveness/startup probes against the same /ready and /health endpoints Compose uses. Moving to Kubernetes therefore needs: an image registry and tagging, a corpus-load or snapshot-restore step (the chart provisions an empty Qdrant, against which ai-service's manifest check refuses to start), the three ArgoCD TODOs resolved, and the bump-image-tag workflow that infra/ci/github-actions/README.md describes but does not contain.

Rationale

Decision observed; rationale not fully recoverable from the repository. The Compose header comment records one constraint —

No GPU, no team k3s — Bedrock calls go out over the instance's IAM role … so no AWS access keys live in this file or its env files.

— and ADR 0002 remaining un-superseded shows the Kubernetes target was not abandoned. Beyond that, whether the driver was cost, cluster access, or time to first deployment is not determinable from the code.