# Phase 7: Deploy v1 - Discussion Log

> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.

**Date:** 2026-05-15
**Phase:** 7-Deploy v1
**Areas discussed:** Deploy target, Secret management, Image design, Migration execution

---

## Deploy Target

| Option | Description | Selected |
|--------|-------------|----------|
| Fly.io | Container PaaS with first-class Docker support, fly secrets, fly.toml | |
| Hetzner VM | Raw VPS ~€5/mo, full Docker control, SSH, Caddy/nginx for TLS | ✓ |
| Cloud Run (GCP) | Serverless containers, familiar from JS API, but heavier setup | |

**User's choice:** Hetzner VM

---

| Option | Description | Selected |
|--------|-------------|----------|
| docker compose on the VM | docker-compose.prod.yaml, `docker compose pull && up -d` | ✓ |
| docker run directly | Two docker run invocations, no compose | |

**User's choice:** docker compose on the VM
**Notes:** User mentioned prior Dokploy usage but explicitly chose plain docker compose for this phase. Dokploy can be layered on later.

---

| Option | Description | Selected |
|--------|-------------|----------|
| Design for Dokploy | Standard image + compose, Dokploy triggers, dual runbook | |
| Plain docker compose only | SSH + docker compose only, no panel | ✓ |

**User's choice:** No — plain docker compose only

---

| Option | Description | Selected |
|--------|-------------|----------|
| Postgres on the VM in docker compose | Same compose stack, volume-backed, pg_dump for backups | ✓ |
| Managed Postgres (external) | Neon / Supabase Postgres / Hetzner Managed DB | |

**User's choice:** On the VM in docker compose

---

| Option | Description | Selected |
|--------|-------------|----------|
| Caddy as a service in docker-compose.prod.yaml | caddy service, Caddyfile bind-mounted, auto-HTTPS | ✓ |
| Caddy installed on the host VM (outside Docker) | systemd service on VM, proxies localhost:8080 | |

**User's choice:** Caddy as a service in docker-compose.prod.yaml
**Notes:** User explicitly requested Caddy — mentioned as an addition to the compose stack.

---

## Secret Management

| Option | Description | Selected |
|--------|-------------|----------|
| .env file on the VM | Host-side file, gitignored, docker compose --env-file reads it | ✓ |
| SOPS-encrypted secrets | Encrypted with SOPS + age, decrypted at deploy | |
| Docker secrets (Swarm-style) | Secrets API, requires Swarm mode | |

**User's choice:** .env file on the VM

---

| Option | Description | Selected |
|--------|-------------|----------|
| Cloudflare R2 | PROJECT.md target, zero egress fees, S3-compatible | ✓ |
| MinIO on the same VM | Already in compose.yaml for dev, simpler but owns storage ops | |

**User's choice:** Cloudflare R2

---

## Image Design

| Option | Description | Selected |
|--------|-------------|----------|
| One image, two docker-compose services | Single multi-stage build, compose runs it twice | ✓ |
| Two separate Dockerfiles / images | Independent image tags, doubles CI complexity | |

**User's choice:** One image, two docker-compose services

---

| Option | Description | Selected |
|--------|-------------|----------|
| Separate binaries in one image | /app/web and /app/worker, compose sets command | ✓ |
| Single binary with subcommand | cmd/xtablo dispatches on os.Args[0] or subcommand | |

**User's choice:** Separate binaries in one image

---

| Option | Description | Selected |
|--------|-------------|----------|
| Embedded via go:embed | All static files baked in at build time | ✓ |
| Mounted volume / copied into image | Assets COPYed into image, no embed | |

**User's choice:** Embedded via go:embed

---

## Migration Execution

| Option | Description | Selected |
|--------|-------------|----------|
| Entrypoint script in the container | goose.Up() before binary serves, automatic on docker compose up | ✓ |
| Separate migrate service in docker-compose.prod.yaml | One-shot service + depends_on --wait | |
| Manual step in the runbook only | SSH + docker compose run, fully explicit | |

**User's choice:** Entrypoint script (goose.Up() programmatic)

---

| Option | Description | Selected |
|--------|-------------|----------|
| Go-programmatic migration in binary | web calls goose.Up(), mirrors rivermigrate pattern | ✓ |
| goose CLI binary in image | Multi-stage installs goose CLI, entrypoint calls it | |

**User's choice:** Go-programmatic migration in the binary

---

| Option | Description | Selected |
|--------|-------------|----------|
| Redeploy previous image tag | Keep :v0.1.0 tag, rollback = change tag + compose up | ✓ |
| goose down in runbook | Explicit schema rollback step | |
| Claude decides | Planner figures out rollback story | |

**User's choice:** Document in runbook: redeploy previous image tag

---

## Claude's Discretion

- Dockerfile base image for builder stage (golang:1.26-alpine vs golang:1.26)
- Final runtime base (distroless/static vs alpine)
- Caddyfile content
- Whether docker-compose.prod.yaml includes healthcheck for postgres service
- Whether web service depends_on postgres with health condition

## Deferred Ideas

- Dokploy layer — user has prior experience; deferred, can layer on later
- CI/CD pipeline — automated build + deploy on git push; runbook covers manual for v1
- pg_dump backup cron — future ops work
- MinIO for prod — user chose R2; MinIO-as-prod deferred
