# Deployment and Operations --- ## Production Stack The production setup is a single-host Docker Compose deployment using **Traefik** as a reverse proxy. There is no Kubernetes, cloud-managed infrastructure, or horizontal scaling configuration. **Services** (`infrastructure/docker-compose.yml`): | Service | Image | Role | |---|---|---| | `migrate` | backend Dockerfile | Runs `prisma migrate deploy` once before API starts | | `api` | backend Dockerfile | NestJS HTTP API on port 3001 | | `worker` | backend Dockerfile | BullMQ queue processor (`node dist/worker.js`) | | `frontend` | frontend Dockerfile | Next.js on port 3000 | | `postgres` | postgres:16-alpine | Database — bound to `127.0.0.1:5432` (not public) | | `redis` | redis:7-alpine | BullMQ queues — `noeviction` policy, AOF persistence | **SSL / routing**: Traefik handles TLS termination with automatic Let's Encrypt certificates. Both API (`/api` prefix) and frontend run on the same domain — Traefik routes by path prefix. The `proxy` external network (the host's existing Traefik network) must exist before deploy. **Build**: Multi-stage Dockerfile (`node:22-alpine`). Builder compiles TypeScript; runner installs prod-only deps. The Prisma CLI is copied from builder stage so the `migrate` service can run schema migrations. --- ## Secrets Management All secrets are passed as environment variables from `infrastructure/.env` (based on `.env.example`). There is no secrets vault, no encrypted secret store, and no runtime secret injection. The `.env` file must be present on the host before `docker compose up`. Secrets that must be set: | Variable | Notes | |---|---| | `TOKEN_ENCRYPTION_KEY` | Exactly 32 characters. AES-256 key for YouTube OAuth tokens. See rotation note below. | | `JWT_SECRET` / `JWT_REFRESH_SECRET` | Generate with `openssl rand -base64 48` | | `POSTGRES_PASSWORD` / `REDIS_PASSWORD` | Strong random passwords | | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | From Google Cloud Console | --- ## Health Check `GET /api/v1/health` returns `{ "status": "ok" }`. This is a **shallow** check — it only confirms the Node process is responding; it does not verify database connectivity or Redis availability. Docker healthcheck polls this endpoint every 15 s; the `frontend` service waits for the API to be healthy before starting. --- ## Logging No structured logging is configured. The application uses NestJS's default logger (`console.log`/`console.error`). In production, logs go to stdout and are captured by Docker's logging driver (default: `json-file`). There is no log aggregation, no Sentry integration, and no OpenTelemetry instrumentation. --- ## Job Failure Handling BullMQ jobs do not have automatic retry configured except where noted: | Queue | Retry | Failed job retention | |---|---|---| | `youtube-sync` | None | Last 5 failed jobs kept (`removeOnFail: { count: 5 }`); completed jobs removed | | `render` | None | BullMQ default (kept until manually cleared) | | `lint` | None | BullMQ default | | `bulk-metadata` | None | BullMQ default | | `import` | None | BullMQ default | | `conflict-detection` | None | Last 10 failed jobs kept (`removeOnFail: 10`); last 10 completed kept (`removeOnComplete: 10`). Registered as a BullMQ repeatable by `ConflictDetectionScheduler` when `CONFLICT_DETECTION_ENABLED=true`. | There is no dead-letter queue and no mechanism to notify users when a background job fails. A failed sync job means the user's video was not pushed to YouTube — the UI will continue to show "push pending" with no error indication. Failed jobs can be inspected directly in Redis or via a BullMQ dashboard (none is currently deployed). --- ## Rate Limiting There is no rate limiting on any API endpoint. `@nestjs/throttler` is not installed. All routes are unthrottled. --- ## Database Backups No backup strategy is configured. PostgreSQL data lives in the `postgres_data` Docker volume on the host. There is no automated backup job, no pg_dump schedule, and no offsite backup. To back up manually: ```bash docker exec pg_dump -U $POSTGRES_USER $POSTGRES_DB > backup.sql ``` --- ## TOKEN_ENCRYPTION_KEY Rotation YouTube OAuth tokens are AES-256 encrypted in the database using `TOKEN_ENCRYPTION_KEY`. **Changing this key breaks all channel connections** — all stored tokens become unreadable and every connected channel must be fully re-authenticated by its owner. There is no tooling for key rotation. If rotation is required (e.g. key compromise), the procedure is: 1. Take the application offline. 2. Write a one-off migration script that decrypts every `Channel.accessToken` and `Channel.refreshToken` with the old key and re-encrypts them with the new key. 3. Update `TOKEN_ENCRYPTION_KEY` in `.env`. 4. Bring the application back online. Until such a script is written, **key rotation = forced re-authentication for all channel owners**. See also the [[01 - Technical Debt and Future Work]] backlog.