4.8 KiB
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 traefik-network external 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:
docker exec <postgres_container> 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:
- Take the application offline.
- Write a one-off migration script that decrypts every
Channel.accessTokenandChannel.refreshTokenwith the old key and re-encrypts them with the new key. - Update
TOKEN_ENCRYPTION_KEYin.env. - 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.