Skip to main content
Railway is the default deployment target for Refract. The stack deploys as two services — webapp and consumer — both built from Dockerfile.production and configured from environment variables set in your Railway project.

How it works

The production build compiles all workspace packages and bundles the frontend as static files served by the Express backend on port 3000. Two Railway services handle different responsibilities:
ServiceRoleEntry point
webappAPI, GraphQL, serves frontendnode apps/backend/dist/index.js
consumerBackground job consumernode apps/backend/dist/consumer.js
Both services run from the same Docker image. The webapp service runs migrations and seeds before starting via a Railway pre-deploy command.

Setup

1. Create a Railway project

Create a new project in Railway and note the project ID. Add two services — name them webapp and consumer. You’ll need the service IDs for each.

2. Add environment variables

In each Railway service, set all the variables your production config reads. At minimum:
# Application
NODE_ENV=production
BACKEND_URL=https://your-domain.up.railway.app
FRONTEND_URL=https://your-domain.up.railway.app
PORT=3000

# Database — use Railway's Postgres plugin or an external instance
PGHOST=...
PGPORT=5432
PGUSER=...
PGPASSWORD=...
PGDATABASE=...

# Redis — use Railway's Redis plugin or an external instance
REDIS_URL=redis://...

# Stripe
STRIPE_API_VERSION=2026-01-28.clover
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# SendGrid (if using Twilio SendGrid mailer)
SENDGRID_API_KEY=SG....

3. Configure Railway credentials locally

Set the Railway deployment environment variables in your local .env.development (or your CI environment):
RAILWAY_TOKEN=your_railway_api_token
RAILWAY_PROJECT_ID=your_project_id
RAILWAY_ENVIRONMENT=production            # or staging
RAILWAY_WEBAPP_SERVICE_ID=your_webapp_id
RAILWAY_CONSUMER_SERVICE_ID=your_consumer_id

4. Configure production.ts

In apps/backend/src/configuration/production.ts, set the deployment block with your service names and IDs:
deployment: {
  strategy: DeploymentStrategy.Railway,
  apiToken: process.env.RAILWAY_TOKEN,
  projectId: process.env.RAILWAY_PROJECT_ID,
  environmentId: process.env.RAILWAY_ENVIRONMENT,
  services: [
    {
      name: 'webapp',
      serviceId: process.env.RAILWAY_WEBAPP_SERVICE_ID,
      dockerfilePath: 'apps/backend/Dockerfile.production',
      healthcheckPath: '/health',
      preDeployCommand: 'node apps/backend/dist/tools/rds/sequelize/migrateUp.js && node apps/backend/dist/tools/rds/sequelize/seedUp.js',
    },
    {
      name: 'consumer',
      serviceId: process.env.RAILWAY_CONSUMER_SERVICE_ID,
      dockerfilePath: 'apps/backend/Dockerfile.production',
      startCommand: 'node apps/backend/dist/consumer.js',
    },
  ],
},

Deploying

Deploy to staging

make deploy-staging

Deploy to production

make deploy-production
Both commands run make deploy ENV=<environment> under the hood, which builds the deployment package inside the running backend container and triggers a Railway redeploy via the deploy script at apps/backend/scripts/deploy.ts.
Always deploy to staging and verify before deploying to production. The pre-deploy migration command runs automatically — a bad migration in production is painful to roll back.

Deploy a specific commit

make deploy ENV=production SHA=<commit-hash>
Useful for pinning a rollback or deploying a hotfix from a specific commit.

Fire-and-forget (async) deploy

make deploy ENV=staging ASYNC=true
Returns immediately without waiting for the deployment to complete. Useful in CI pipelines.

The production Dockerfile

Dockerfile.production is a multi-stage build that:
  1. Installs all workspace dependencies with pnpm install --frozen-lockfile
  2. Builds shared, all tooling-* packages, frontend, and backend in sequence
  3. Sets NODE_ENV=production and exposes port 3000
  4. Starts with node apps/backend/dist/index.js
The frontend’s dist/ output is copied into apps/frontend/dist at build time. The Express backend serves it as static files in production — no separate frontend hosting needed.

Rotating secrets

When rotating a secret (e.g. STRIPE_SECRET_KEY, SENDGRID_API_KEY):
  1. Add the new value to Railway’s environment variables for the affected service.
  2. Remove the old value.
  3. Trigger a redeploy: make deploy ENV=production.
  4. Verify the service is healthy via Railway’s logs before removing the old credential from Stripe/SendGrid.
If a credential was ever committed to version control, rotate it immediately — even if it was only in a branch. Railway env vars are the source of truth; .env.development is local only and gitignored.

Gotchas

  • PORT must be set in Railway — Railway dynamically assigns the external port and proxies to it. The backend reads PORT from the environment to bind Express.
  • The consumer service has no health check path. Railway will mark it healthy as soon as the process starts. If the consumer crashes immediately, check the Railway logs for startup errors.
  • Migrations run as part of the webapp pre-deploy command. If a migration fails, the deployment is aborted and the previous version keeps running.
  • RAILWAY_ENVIRONMENT must match an environment ID in your Railway project, not just a string like "production". Get the value from the Railway dashboard.

What’s next?

  • Updating Refract — pull upstream improvements into your project after deploying.
  • Configuration — how production.ts and environment variables fit together.
  • Support — if a deployment fails and you’re stuck.