# Deploying TouchBase A small, opinionated runbook for deploying TouchBase to a single self-hosted host. v1 assumes one practice, one host, no orchestration. When that stops being enough, the path forward is straightforward — Postgres goes managed, the app+worker images go to a container registry, and the same compose file or any orchestrator runs them. ## Prerequisites - A Linux host with Docker + Docker Compose v5+ (or `docker compose` plugin v2+) - A real domain pointed at the host's public IP (e.g. `book.your-practice.com`) - Ports 80 and 443 open inbound (Caddy needs both for ACME) - Outbound to: Postgres (if external), Resend SMTP (smtp.resend.com:587), Stripe API (api.stripe.com:443) - `git`, `pnpm`, and `node 22+` for migrations on first install (only needed once) ## What runs in production | Service | Image | Command | Port | Notes | |---|---|---|---|---| | `postgres` | `postgres:16-alpine` | (default) | 5432 (internal) | Volume `pgdata` | | `app` | `touchbase/app:latest` | `pnpm start` | 3000 (internal) | Healthchecked at `/api/health` | | `worker` | `touchbase/app:latest` | `pnpm worker` | — | Long-lived; pg-boss handlers | | `caddy` | `caddy:2-alpine` | (default) | 80, 443 (host) | Auto-TLS via ACME | Mailpit is **not** in the prod profile — production uses real SMTP (Resend). Stop the dev `mailpit` container if you brought it up. ## First-time setup ```bash # On the host git clone /opt/touchbase cd /opt/touchbase # 1. Create a real .env (see "Required env vars" below) cp .env.example .env $EDITOR .env # 2. Bring up just Postgres so we can run migrations docker-compose up -d postgres ./scripts/db-bootstrap.sh # idempotent: ensures touchbase_test exists + extensions # in prod you only need the prod DB; trim the script if desired # 3. Run migrations from the host (one-time) pnpm install --frozen-lockfile pnpm exec prisma migrate deploy # 4. (Optional) seed an admin user — replace the seed entirely for prod, or # do it manually via psql / a one-off script. # Minimal admin row: docker exec -i touchbase-postgres-1 psql -U touchbase -d touchbase_dev <` | Must be a verified Resend sender domain | Optional (only when payments are wired): | Var | Notes | |---|---| | `STRIPE_SECRET_KEY` | `sk_live_…` for prod, `sk_test_…` for staging | | `STRIPE_PUBLISHABLE_KEY` | matched env to secret | | `STRIPE_WEBHOOK_SECRET` | from your Stripe webhook configuration in the Stripe Dashboard, NOT the CLI | Optional (tweakable): | Var | Default | Notes | |---|---|---| | `REMINDER_LEAD_MIN` | `1440` (24h) | Minutes before each appointment to send the reminder email. Set on **both** the app and worker containers; producer-side scheduling uses the value, handler-side fires when it does. | If `STRIPE_*` vars are absent, the app skips the deposit branch entirely — bookings proceed straight to CONFIRMED with no payment. This means **don't accidentally launch without them set if you intend to require deposits**. ## Migrations Migrations live in `prisma/migrations/`. Apply on each deploy that adds them: ```bash docker-compose --profile prod exec app pnpm exec prisma migrate deploy ``` Prisma's `migrate deploy` is non-destructive (no resets, no prompts). It applies any unapplied migrations in order and is safe to run on every deploy. ## Where things log Production stdout/stderr from each container is captured by Docker: ```bash docker-compose --profile prod logs -f app docker-compose --profile prod logs -f worker docker-compose --profile prod logs -f caddy docker-compose --profile prod logs -f postgres ``` For long-term retention, point Docker at a logging driver (json-file with rotation, journald, or a remote sink). Out of scope here. ## Backups ```bash # Daily, e.g. via host cron: docker exec touchbase-postgres-1 pg_dump -U touchbase touchbase_dev \ | gzip > /var/backups/touchbase-$(date +%F).sql.gz # Encrypted off-host (recommended): ... | age -r | aws s3 cp - s3://your-backups/touchbase-$(date +%F).sql.gz.age ``` Test restore quarterly. The exclusion-constraint migration depends on the `btree_gist` extension — ensure your restore target has it (the `db/init` script does, plus `pgcrypto`). ## Rollback If a deploy breaks production: ```bash # Roll the app + worker back to the previous image tag docker-compose --profile prod down app worker git checkout docker-compose --profile prod build app worker docker-compose --profile prod up -d app worker ``` Postgres data is unaffected (it's in a volume). **Migrations are not auto-rolled-back** — Prisma doesn't generate down-migrations. If a migration is the breaking change, write a corrective migration in code and apply it forward; only resort to manual SQL for incidents. ## Healthcheck ```bash curl -sf https://book.your-practice.com/api/health | jq # { # "status": "ok", # "version": "dev", # "time": "2026-…", # "checks": { "app": "ok", "db": "ok" } # } ``` 503 with `checks.db` populated = app can't reach Postgres. The Docker HEALTHCHECK in the app service watches this every 30s. ## Common operations | Task | Command | |---|---| | See all bookings (admin) | https://book.your-practice.com/admin/bookings | | Run a one-off SQL query | `docker exec -it touchbase-postgres-1 psql -U touchbase -d touchbase_dev` | | Check pg-boss queue | `docker exec touchbase-postgres-1 psql -U touchbase -d touchbase_dev -c "SELECT name, state, COUNT(*) FROM pgboss.job GROUP BY 1, 2;"` | | Force a reminder to fire now | `UPDATE pgboss.job SET start_after = now() WHERE name='booking-reminder' AND state='created';` | | Make a user admin | `UPDATE "User" SET role='ADMIN' WHERE email='someone@example.com';` | | Restart just the worker | `docker-compose --profile prod restart worker` | ## What's not yet automated - **Image registry**: `touchbase/app:latest` is local-build only. For multi-host or CI deploys, push to a registry (GHCR, ECR, etc.) and pin tags by commit SHA in compose. - **Secret management**: `.env` on the host is fine for one-host. Beyond that, use Docker secrets, SOPS-encrypted env files, or your platform's secret store. - **Observability**: stdout logs only. Add Sentry/GlitchTip + Pino structured logs when the practice has appetite. - **CI**: there isn't one. Add a GitHub Actions workflow to run `pnpm test`, `pnpm lint`, `pnpm exec tsc --noEmit`, and `docker build` on every PR; tag-based release builds. These are all "next step" items, not v1 blockers.