The 2026-07-06 prod outage was a missed manual migration: the 0002 job_events migration never ran against prod, so the worker crash-looped on every job. Make the deploy pipeline own it: - railway.json sets `npm run db:migrate` as the pre-deploy command. Railway runs it with the service's env before starting the new deployment; a failed migration fails the deploy and the old version keeps serving. - runMigrations() now serializes on a Postgres advisory lock so api and worker pre-deploys firing off the same push can't race Drizzle's journal writes. - DEPLOY.md: document the automatic path; keep the manual command for first-time setup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.2 KiB
Deploy — Railway + Neon + R2
The service is two long-lived processes (an api and a worker) plus managed Postgres and S3-compatible storage. The recommended OSS-friendly stack:
- Railway — hosts the
apiandworkerservices (one Railway service each, built from the Dockerfiles indocker/). - Neon — managed Postgres (the database and the pg-boss queue).
- Cloudflare R2 (or AWS S3) — blob storage for binary assets + bundles. R2 has no egress fees, friendly for forks/self-host.
1. Database (Neon)
-
Create a Neon project; copy the pooled connection string.
-
Migrations run automatically before every Railway deploy: the repo's
railway.jsonsetsnpm run db:migrateas the pre-deploy command, which Railway executes (with the service's env, soDATABASE_URLis present) before starting the new deployment. Re-running is a no-op for anything already applied, and concurrent runs (api + worker deploying off the same push) serialize on a Postgres advisory lock. A failed migration fails the deploy — the old version keeps serving.To apply migrations manually (first-time setup, or outside Railway):
DATABASE_URL='postgres://...neon.../ditto_site?sslmode=require' npm run db:migrate(Alternatively, apply a single migration by piping its SQL through
psql/the Neon CLI — but preferdb:migrate, which also records the journal entry so future runs stay consistent.)Current migrations:
0000core tables,0001signup tokens,0002job_events(structured clone-progress events behindGET /v1/clones/:id/events).
2. Blob storage (R2 / S3)
Create a bucket (e.g. ditto-site-artifacts) and an access key. Note the S3 API
endpoint (R2: https://<accountid>.r2.cloudflarestorage.com).
3. Railway services
Create two services in one Railway project, both deploying this repo:
| Service | Dockerfile | Scaling |
|---|---|---|
api |
docker/api.Dockerfile |
light; autoscale on CPU |
worker |
docker/worker.Dockerfile |
scale by queue depth (Playwright image) |
Set these variables on both services:
DATABASE_URL=postgres://...neon.../ditto_site?sslmode=require
S3_BUCKET=ditto-site-artifacts
S3_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com # omit for AWS S3
S3_REGION=auto # 'auto' for R2
S3_ACCESS_KEY_ID=…
S3_SECRET_ACCESS_KEY=…
API-only:
PORT=8787
PUBLIC_BASE_URL=https://api.yourdomain.com # so MCP-returned URLs are absolute
API_KEYS=key_live_xxx,key_live_yyy # require auth
RATE_LIMIT_PER_MINUTE=60
SIGNUP_ENABLED=true # optional: public API-key minting
SIGNUP_RATE_LIMIT_PER_HOUR=3
DEFAULT_SIGNUP_KEY_RATE_LIMIT=30
SIGNUP_DIRECT_ENABLED=false # recommended once verified email is configured
RESEND_API_KEY=re_...
SIGNUP_FROM_EMAIL=Ditto <hello@ditto.site> # must be a verified Resend sender/domain
SIGNUP_VERIFY_URL=https://www.ditto.site/api-key
SIGNUP_TOKEN_TTL_MINUTES=30
SIGNUP_CORS_ORIGINS=https://ditto.site,https://www.ditto.site # browser origins allowed to call signup routes
# SSRF is on by default; do NOT set SSRF_ALLOW_LOOPBACK in prod.
Worker-only:
CACHE_STALE_AFTER=24h
ARTIFACTS_DIR=/data/artifacts # only used if S3 is not configured
HARNESS_DIR=/data/harness # isolated build harness for verify
4. Scaling notes
- Capture workers parallelize freely (each clone = one headless Chromium, ~0.5–1 GB peak); scale horizontally by queue depth.
- Verify is the heavy step (
next build). Each worker container has its own filesystem, so itsHARNESS_DIRis naturally isolated; for multiple workers on one host, give each a distinctHARNESS_DIR. - Per-job wall-clock caps and retries are handled by pg-boss; a
compilerVersionbump invalidates the cache automatically.
5. CI
.github/workflows/ci.yml typechecks and runs the
test suite on every push/PR with a Postgres service container + Chromium installed,
so the DB and browser-gated tests run in CI. Heavy real-site benchmarks remain a
manual/nightly concern; benchmark lists live in
compiler/benchmarks/.