diff --git a/.planning/phases/07-deploy-v1/07-CONTEXT.md b/.planning/phases/07-deploy-v1/07-CONTEXT.md
new file mode 100644
index 0000000..77a8ac6
--- /dev/null
+++ b/.planning/phases/07-deploy-v1/07-CONTEXT.md
@@ -0,0 +1,119 @@
+# Phase 7: Deploy v1 - Context
+
+**Gathered:** 2026-05-15
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Package the Go backend into a production-ready Docker image, deploy it to a Hetzner VM via docker compose, wire Caddy as a TLS-terminating reverse proxy, inject secrets from a host-side `.env` file, run goose migrations programmatically on startup, and document the deploy + rollback runbook in `backend/README.md`.
+
+Delivers DEPLOY-01, DEPLOY-02, DEPLOY-03, DEPLOY-04, DEPLOY-05. **Not in scope:** CI/CD pipelines, container registry automation, Dokploy or any PaaS layer, multi-node deployment, backup tooling, monitoring beyond `/healthz` + `/readyz`.
+
+
+
+
+## Implementation Decisions
+
+### Deploy Target
+- **D-01:** Production host is a **Hetzner VM** running Docker Compose. No PaaS, no Kubernetes.
+- **D-02:** The full stack runs via **plain `docker compose`** — no Dokploy or Swarm mode in v1. (User previously used Dokploy but will not in this phase; can be layered on later.)
+- **D-03:** **Postgres runs on the VM** inside the compose stack, volume-backed. No managed Postgres service for v1.
+- **D-04:** **Caddy** is a service in `docker-compose.prod.yaml`. It proxies to `web:8080` and handles TLS via Let's Encrypt. Config via a bind-mounted `Caddyfile`.
+
+### Secret Management
+- **D-05:** Production secrets (`SESSION_SECRET`, `DATABASE_URL`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT_URL`, `AWS_BUCKET`, `PORT`, `ENV`) are stored in a **`.env` file on the Hetzner host** (gitignored). `docker compose --env-file .env.prod up` reads it. No SOPS, no Docker secrets API.
+- **D-06:** S3-compatible storage in production is **Cloudflare R2**. R2 credentials (account ID, access key, secret key, bucket name, endpoint URL) live in the host `.env` file. MinIO remains in `compose.yaml` for local dev only.
+
+### Image Design
+- **D-07:** A **single multi-stage Dockerfile** produces one image. The image contains **two binaries**: `/app/web` (from `cmd/web`) and `/app/worker` (from `cmd/worker`). Both are compiled in the builder stage and copied to the final `gcr.io/distroless/static` (or `alpine`) runtime stage.
+- **D-08:** `docker-compose.prod.yaml` runs the same image twice: one service with `command: /app/web`, one with `command: /app/worker`. No subcommand dispatcher needed in Go code.
+- **D-09:** All static assets (Tailwind-compiled CSS, HTMX JS, Sortable.js, templ-generated HTML templates) are **embedded via `//go:embed`** at build time. No volume mounts for assets. The container has zero runtime file dependencies beyond the binary.
+
+### Migration Execution
+- **D-10:** Migrations run **programmatically inside the `web` binary** at startup: `web` calls `goose.Up()` via the goose library before binding the HTTP server. Mirrors the `rivermigrate` pattern already in `cmd/worker`. No separate goose CLI binary in the image, no separate migrate service.
+- **D-11:** **Rollback strategy**: redeploy the previous image tag. The runbook documents tagging each build (e.g., `:v0.1.0`, `:latest`). Rollback = update compose image tag + `docker compose up -d`. Destructive schema rollbacks via `goose down` are documented as a break-glass step only; they are not part of the normal rollback path.
+
+### Health Checks
+- **D-12:** `/healthz` — liveness: returns 200 OK immediately if the server is up (no DB ping). Used by Caddy / uptime monitor.
+- **D-13:** `/readyz` — readiness: returns 200 OK only if the DB pool is reachable (one `db.Ping()` call). Returns 503 during startup until migrations complete and the pool is healthy. Worker does not expose HTTP; health is observable via structured logs only.
+
+### Claude's Discretion
+- Exact Dockerfile base image for the builder stage (e.g., `golang:1.26-alpine` vs `golang:1.26`).
+- Final runtime base: `distroless/static` (smallest, no shell) vs `alpine` (has shell for debugging). Either is acceptable.
+- Caddyfile content (reverse proxy config, TLS directive, HTTPS redirect).
+- Whether `docker-compose.prod.yaml` includes a `healthcheck:` directive for the Postgres service (matching `compose.yaml`'s existing pattern).
+- Exact docker compose version / syntax used (`compose.yaml` already uses v2 syntax).
+- Whether the `web` service in prod compose depends_on the `postgres` service with a health condition.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Requirements
+- `.planning/REQUIREMENTS.md` §Deploy (DEPLOY-01..05) — The 5 deploy requirements this phase delivers
+- `.planning/PROJECT.md` — Core constraints: single VPS, no Supabase, S3-compatible storage (R2), single binary + background worker
+- `.planning/ROADMAP.md` §Phase 7 — Success criteria and user-in-loop decisions
+
+### Prior Phase Context
+- `.planning/phases/06-background-worker/06-CONTEXT.md` — D-01..D-08 (river setup, worker binary structure, cmd/worker entrypoint pattern, rivermigrate programmatic migration approach)
+- `.planning/phases/05-files/05-CONTEXT.md` — S3 client setup, MinIO in compose.yaml, file store interface (orphan-cleanup job and web handlers both use the same S3 client)
+- `.planning/phases/01-foundation/01-CONTEXT.md` — cmd/web and cmd/worker entrypoints, goose migration conventions, justfile targets
+
+### Codebase Entry Points
+- `backend/cmd/web/main.go` — Web binary entrypoint; will call goose.Up() before serving
+- `backend/worker/cmd/worker/main.go` — Worker binary entrypoint; already calls rivermigrate at startup (model for goose.Up() pattern)
+- `backend/compose.yaml` — Dev compose with postgres + minio; docker-compose.prod.yaml will be a sibling file with postgres + web + worker + caddy
+- `backend/migrations/` — goose migration files numbered 0001..000N; embedded via go:embed in web binary
+- `backend/.env.example` — Canonical list of env vars; prod .env on the host must mirror this
+- `backend/justfile` — Bootstrap and dev workflow; runbook can reference just targets for local validation
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `db.NewPool(ctx, dsn)` — already called in both cmd/web and cmd/worker; prod image uses the same function with DATABASE_URL from env
+- `web.NewSlogHandler(env, os.Stdout)` — structured JSON logging in production (`ENV=production`); already switches on `ENV`
+- `signal.NotifyContext` pattern — already in cmd/worker for graceful shutdown; cmd/web uses the same approach; both binaries already handle SIGINT/SIGTERM correctly
+- `rivermigrate` startup call in cmd/worker — the pattern to follow for goose.Up() in cmd/web
+
+### Established Patterns
+- **go:embed** — templates package already embeds templ output. Same pattern applies for `static/` assets (CSS, JS).
+- **ENV var for environment selector** — `ENV=development` uses text slog, `ENV=production` uses JSON slog. Already wired; no changes needed.
+- **`just` + bootstrap** — local dev uses justfile; the runbook can reference `just build` or `go build` steps for pre-push validation.
+
+### Integration Points
+- `backend/compose.yaml` → `docker-compose.prod.yaml`: prod file reuses the same postgres service definition + minio replaced by R2 credentials
+- `backend/.env.example` → host `.env.prod`: same var names, real production values
+- `backend/internal/db/` → goose.Up() at web startup: migrations directory path must be resolvable (via go:embed FS or an explicit path argument)
+
+
+
+
+## Specific Ideas
+
+- User wants **Caddy** as the reverse proxy — this is deliberate, not just a recommendation. Caddyfile should be part of the repo under `backend/deploy/Caddyfile` or similar, bind-mounted at runtime.
+- User explicitly chose **plain docker compose** (not Dokploy, not Swarm) — runbook must be purely SSH + compose commands, no panel UI references.
+- Worker health is **log-observable only** — no HTTP endpoint needed for the worker service.
+
+
+
+
+## Deferred Ideas
+
+- **Dokploy layer** — user mentioned prior Dokploy usage; deferred. Can be added on top of the docker compose structure without changing the image.
+- **CI/CD pipeline** — automated image build + deploy on git push. Not in v1 scope; runbook documents manual steps.
+- **pg_dump backup cron** — Postgres backup strategy. Noted as future ops work.
+- **MinIO for prod** — user chose R2; MinIO-as-prod-storage is deferred if ever needed.
+
+
+
+---
+
+*Phase: 7-Deploy v1*
+*Context gathered: 2026-05-15*
diff --git a/.planning/phases/07-deploy-v1/07-DISCUSSION-LOG.md b/.planning/phases/07-deploy-v1/07-DISCUSSION-LOG.md
new file mode 100644
index 0000000..1504a58
--- /dev/null
+++ b/.planning/phases/07-deploy-v1/07-DISCUSSION-LOG.md
@@ -0,0 +1,156 @@
+# 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