apps/api/Dockerfile is the artifact you ship — there is no separate “OSS” image. Whether you use Railway, Fly, Render, ECS, or plain Kubernetes, the moving parts are the same.
Architecture: two services, one image
The API container has two entry points and you run them as two separate services in production:
Both share the same image and the same env vars. Only the start command and the public-networking flag differ.
Why split: a queue backlog (Twitter rate limit, Instagram container processing) keeps the worker busy. If the worker shared a process with the HTTP listener, that backlog would starve
POST /v1/posts. Splitting lets you scale them independently and ensures the HTTP path stays responsive under queue pressure.
What you need
- A container host (Railway, Fly, Render, ECS, Kubernetes — anywhere Docker runs)
- Managed Postgres — Neon, RDS, Supabase, or a Postgres you operate
- Managed Redis — Upstash, ElastiCache, or a Redis you operate
- An S3 bucket configured per the media spec
- DNS control for the hostname your API will live on
- Your own platform OAuth credentials (see platform credentials)
Build the image
The repo-rootapps/api/Dockerfile builds a single image from the monorepo:
apps/api/. The Dockerfile copies pnpm-lock.yaml, every workspace package.json, and the packages/schemas/ source — none of which exist inside apps/api/.
Environment variables
Set these on both services (api and worker):
The dashboard hosts elsewhere (Vercel, Cloudflare Pages, your own static host) and needs
NEXT_PUBLIC_API_URL=https://api.your-domain.example on its environment.
Health check
The API exposesGET /health:
/health on the api service with a 30s timeout. The worker doesn’t bind a port — disable its health check.
Migrations
The api service runs migrations on boot (node dist/db/migrate.js && node dist/server.js). The worker does not run migrations — the api owns that responsibility so two replicas don’t race.
For a schema change in your fork:
Reference: Railway
Step-by-step setup for our production deploy (api + worker on Railway, Neon for Postgres, Upstash for Redis) lives inDEPLOY.md in the repo. The two non-obvious things to know if you mirror it:
- Root Directory: blank /
/on both services. Not/apps/api. The Dockerfile needs the monorepo root as its build context. - Same
KEK_MASTERon both services. They’re separate Railway services, so it’s two env-var copies — but the value must be identical or the worker can’t decrypt tokens the api wrote.
Rollback
Container platforms handle rollback by redeploying a previous image. Migrations are forward-only — if you need to roll back across a schema change, restore the database from a point-in-time snapshot first, then redeploy the previous image.Smoke test after deploy
queued → published within a few seconds. If it stays queued, the worker isn’t consuming — see troubleshooting.
Self-host parity
There is no feature gate between the OSS image andapi.letmepost.dev. Same code, same API surface, same error envelope. The hosted tier’s only differentiation is managed OAuth apps (you don’t have to register your own with each platform) and managed infrastructure.
