Deployment Guide (Railway)

12 min readUpdated 29 Apr 2026

VeloCMS production runs two Railway services: one for the Next.js application and one for PocketBase. Both are deployed from the same GitHub repository via Railway's git-driven auto-deploy. A push to main triggers a build for both services in parallel. The Railway CLI provides a management interface for environment variables, manual redeploys, and log streaming.

Service layout

ServiceRuntimePortDeploy trigger
Next.js (web)Node.js 20 (Nixpacks)3000 (Railway injects PORT)Push to main
PocketBaseGo binary (Dockerfile)8090Push to main

Required environment variables

All environment variables are set in Railway via the Dashboard or the CLI. Never commit secrets to the repository. The .env.example file in the repository root mirrors the full list with placeholder values for reference.

Core production variables (set via railway variables --set)
# Mode
VELOCMS_MODE=multi

# PocketBase connection
POCKETBASE_URL=https://pb.velocms.org
POCKETBASE_ADMIN_EMAIL=<YOUR_ADMIN_EMAIL>
POCKETBASE_ADMIN_PASSWORD=<YOUR_ADMIN_PASSWORD>

# Public URLs
NEXT_PUBLIC_SITE_URL=https://velocms.org
NEXT_PUBLIC_PLATFORM_DOMAIN=velocms.org

# Stripe (live mode)
STRIPE_SECRET_KEY=rk_live_<YOUR_KEY>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_<YOUR_KEY>
STRIPE_PRICE_PRO=price_<YOUR_PRICE_ID>
STRIPE_PRICE_BUSINESS=price_<YOUR_PRICE_ID>
STRIPE_PRICE_AGENCY=price_<YOUR_PRICE_ID>
STRIPE_WEBHOOK_SECRET=whsec_<YOUR_PROD_SECRET>

# Email
RESEND_API_KEY=re_<YOUR_KEY>
RESEND_FROM_EMAIL=[email protected]

# Cloudflare
CLOUDFLARE_API_TOKEN=<YOUR_TOKEN>
CLOUDFLARE_ZONE_ID=<YOUR_ZONE_ID>
CLOUDFLARE_FALLBACK_ORIGIN=proxy.velocms.org

# AI
GEMINI_API_KEY=AI<YOUR_KEY>

# Media (R2)
CLOUDFLARE_R2_ACCOUNT_ID=<YOUR_ACCOUNT_ID>
CLOUDFLARE_R2_ACCESS_KEY_ID=<YOUR_KEY_ID>
CLOUDFLARE_R2_SECRET_ACCESS_KEY=<YOUR_SECRET>
CLOUDFLARE_R2_BUCKET_NAME=velocms-media
CLOUDFLARE_R2_PUBLIC_URL=https://media.velocms.org

# Encryption
ENCRYPTION_KEY=<64_hex_chars>  # 32 bytes for AES-256-GCM
bash

Railway CLI workflow

Railway CLI common commands
# Login
railway login

# List current variables for the web service
railway variables

# Set a variable without triggering a deploy
railway variables --set "GEMINI_API_KEY=AI..." --skip-deploys

# Set multiple variables at once
railway variables --set "KEY1=value1" --set "KEY2=value2" --skip-deploys

# Stream live logs from the web service
railway logs

# Trigger a manual redeploy (use sparingly — costs build minutes)
railway redeploy

# Open the Railway dashboard in the browser
railway open
bash

Deploy flow: step by step

  1. Run the full 4-gate cycle locally: npm run typecheck && npm run lint && npm run test && npm run build — all must pass
  2. For UI changes: run node scripts/visual-qa.mjs http://localhost:3000 and review screenshots
  3. Commit with a conventional commit message: git commit -m 'feat: description'
  4. Push to main: git push origin main
  5. Railway auto-deploys both services. Monitor in Railway Dashboard or with railway logs
  6. After deploy: run node scripts/production-smoke-test.mjs to verify key endpoints
  7. For UI changes: capture production screenshots and compare with local screenshots

Rolling back a bad deploy

Railway keeps the previous deployment available for instant rollback. Go to Railway Dashboard → your service → Deployments → click the previous deployment → Redeploy. This redeploys the last successful build without a new git push. Railway does not roll back environment variable changes — if the bad deploy was caused by a variable change, revert that variable manually via the CLI before rolling back the code.

PocketBase migrations in production

PocketBase migrations live in pb/pb_migrations/ and run automatically when the PocketBase container starts. For production schema changes, the workflow is: write the migration file, push to main, wait for Railway to deploy the new PocketBase container. The migration runs as part of the PocketBase startup sequence — zero manual SQL execution required.

For data migrations (backfilling values, applying transformations), use the apply-production-migrations.mjs script which authenticates as superuser and executes the migration logic programmatically. Always take a PocketBase backup before running a data migration in production.

Data migration in production
# 1. Take a manual backup first
POCKETBASE_URL=https://pb.velocms.org \
[email protected] \
POCKETBASE_ADMIN_PASSWORD=<PASSWORD> \
node scripts/backup-pb.mjs

# 2. Run the data migration
POCKETBASE_URL=https://pb.velocms.org \
[email protected] \
POCKETBASE_ADMIN_PASSWORD=<PASSWORD> \
node scripts/apply-production-migrations.mjs

# 3. Verify with smoke test
node scripts/production-smoke-test.mjs
bash

Custom domain wiring (Railway + Cloudflare)

The production domain setup uses Cloudflare for the apex domain (velocms.org, orange cloud proxy) and a gray-cloud wildcard (*.velocms.org) that resolves via Railway's wildcard custom domain. The wildcard gray cloud is required because Cloudflare's orange cloud proxy cannot pass through the subdomain for Railway's wildcard routing to work correctly (P-014).

RecordTypeValueCloudflare proxy
velocms.orgA/CNAMERailway service URLOrange cloud (proxied)
*.velocms.orgCNAMERailway wildcard domainGray cloud (DNS only)
proxy.velocms.orgCNAMERailway service URLGray cloud (DNS only)

Production healthcheck endpoints

EndpointExpected responseWhat it verifies
GET /200 HTMLNext.js is serving the landing page
GET /api/onboarding/status200 JSON or 401API layer + auth routing working
GET /sitemap.xml200 XMLISR and static generation working
GET /robots.txt200 textStatic file serving working
PocketBase /api/health200 JSON {code: 200}PocketBase container alive

Deployment checklist

  • [ ] 4-gate cycle passed: typecheck + lint + test + build
  • [ ] Visual QA passed for any UI changes (scripts/visual-qa.mjs)
  • [ ] Environment variables updated if new vars were added
  • [ ] .env.example updated to reflect new variables
  • [ ] PocketBase migration written and tested locally if schema changed
  • [ ] Manual backup taken if data migration is part of the deploy
  • [ ] Post-deploy: production smoke test passed
  • [ ] Post-deploy: Lighthouse run if performance-sensitive changes
  • [ ] wiki/hot.md updated if this was a sprint-level change

Monitoring and observability

Railway provides built-in log streaming and basic metrics (CPU, memory, network). For uptime monitoring, VeloCMS uses UptimeKuma configured via scripts/setup-uptime-kuma-monitors.mjs to check the landing page, PocketBase health, and API endpoints every 60 seconds. Error tracking runs through Sentry — the NEXT_PUBLIC_SENTRY_DSN and SENTRY_AUTH_TOKEN variables must be set in Railway for source-map-based stack traces in production.

Reference

File / ResourcePurpose
scripts/production-smoke-test.mjsPost-deploy smoke test — checks core endpoints
scripts/backup-pb.mjsManual backup trigger before risky deploys
scripts/apply-production-migrations.mjsRun data migrations against production PB
.env.exampleFull list of required environment variables with placeholder values
Railway DashboardDeployment history, rollback, environment variables UI