railway-deployment
Complete guide to deploying applications on Railway, covering project setup, environment variable management, services and databases (Postgres, Redis, MySQL), persistent volumes, monorepo support, private networking between services, and scheduled cron jobs.
```bash
npm install -g @railway/cli
## Key Points
1. Connect your GitHub repo in the Railway dashboard.
2. Railway auto-detects your framework and builds accordingly.
3. Every push to the linked branch triggers a deploy.
4. PRs get ephemeral preview environments.
- Mount path: Where the volume appears in the container (e.g., `/data`)
- Size starts at 1 GB, expandable to 50 GB
1. **Not using variable references**: Manually copying database URLs between services instead of using `${{Postgres.DATABASE_URL}}`.
2. **Missing health checks**: Without a health check path, Railway can't detect unhealthy deployments.
3. **Hardcoding PORT**: Railway assigns the port. Always use `process.env.PORT`.
4. **Public service-to-service calls**: Use private networking (`.railway.internal`) for internal traffic.
5. **No watch paths in monorepos**: Every commit rebuilds every service, wasting build minutes.
- [ ] `railway.json` or Nixpacks configuration set per service
## Quick Example
```bash
NIXPACKS_BUILD_CMD=npm run build
NIXPACKS_START_CMD=npm start
NIXPACKS_NODE_VERSION=20
```
```
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
INTERNAL_API_URL=${{api.RAILWAY_PRIVATE_DOMAIN}}
```skilldb get deployment-patterns-skills/railway-deploymentFull skill: 434 linesDeploying to Railway
Project Setup
Via CLI
# Install Railway CLI
npm install -g @railway/cli
# Login
railway login
# Initialize a new project
railway init
# Link to existing project
railway link
# Deploy
railway up
Via GitHub Integration
- Connect your GitHub repo in the Railway dashboard.
- Railway auto-detects your framework and builds accordingly.
- Every push to the linked branch triggers a deploy.
- PRs get ephemeral preview environments.
Nixpacks Build System
Railway uses Nixpacks for auto-detection. It supports Node.js, Python, Go, Rust, Ruby, Java, PHP, and more. Override with a Dockerfile or nixpacks.toml:
# nixpacks.toml
[phases.setup]
nixPkgs = ['...', 'ffmpeg']
[phases.build]
cmds = ['npm run build']
[start]
cmd = 'npm start'
Or configure via environment variables:
NIXPACKS_BUILD_CMD=npm run build
NIXPACKS_START_CMD=npm start
NIXPACKS_NODE_VERSION=20
Anti-pattern: Fighting Nixpacks when your setup is complex. If auto-detection fails, just add a Dockerfile — Railway handles it natively.
Environment Variables
Setting Variables
# Via CLI
railway variables set DATABASE_URL="postgresql://..."
railway variables set NODE_ENV=production
railway variables set SESSION_SECRET="$(openssl rand -hex 32)"
# List all variables
railway variables list
# Remove a variable
railway variables delete OLD_VAR
Variable References
Railway supports referencing variables from other services:
DATABASE_URL=${{Postgres.DATABASE_URL}}
REDIS_URL=${{Redis.REDIS_URL}}
INTERNAL_API_URL=${{api.RAILWAY_PRIVATE_DOMAIN}}
This is configured in the dashboard under service variables. Reference syntax: ${{service-name.VARIABLE_NAME}}.
Shared Variables
Set variables at the project level to share across all services:
# Shared across all services in the project
APP_NAME=my-app
ENVIRONMENT=production
Built-in Variables
Railway provides these automatically:
RAILWAY_ENVIRONMENT # "production" or environment name
RAILWAY_PROJECT_ID # Project UUID
RAILWAY_SERVICE_ID # Service UUID
RAILWAY_PRIVATE_DOMAIN # Internal DNS: service.railway.internal
RAILWAY_PUBLIC_DOMAIN # Public URL (if exposed)
PORT # Assigned port (Railway sets this)
Anti-pattern: Hardcoding the port. Railway assigns PORT dynamically. Always read from process.env.PORT.
Services Architecture
A Railway project contains multiple services. Common patterns:
Web + Worker + Database
Project: my-app
├── web (GitHub repo → apps/web)
├── worker (GitHub repo → apps/worker)
├── Postgres (managed)
├── Redis (managed)
└── cron (GitHub repo → apps/cron)
Adding Services
# Add a service from the same repo (monorepo)
# Done via dashboard: New Service → GitHub Repo → Set root directory
# Add a database
# Dashboard: New Service → Database → PostgreSQL/Redis/MySQL
Configuring a Service
In the dashboard or via railway.json:
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "npm start",
"healthcheckPath": "/health",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
Databases
PostgreSQL
# Provision via dashboard: New → Database → PostgreSQL
# Railway sets DATABASE_URL automatically when referenced
Connection in your app:
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.RAILWAY_ENVIRONMENT === 'production'
? { rejectUnauthorized: false }
: false,
max: 20,
idleTimeoutMillis: 30000,
});
Redis
# Provision via dashboard: New → Database → Redis
# Railway sets REDIS_URL automatically
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL, {
maxRetriesPerRequest: 3,
retryDelayOnFailover: 100,
});
MySQL
# Provision via dashboard: New → Database → MySQL
# Railway sets MYSQL_URL automatically
Anti-pattern: Creating databases outside the Railway project. Use Railway-managed databases to get automatic private networking and variable injection.
Volumes (Persistent Storage)
# Create a volume via dashboard: Service → Volumes → Add Volume
# Mount path: /data
# Size: 10 GB (expandable)
Configuration in the dashboard:
- Mount path: Where the volume appears in the container (e.g.,
/data) - Size starts at 1 GB, expandable to 50 GB
Use Cases
// SQLite with persistent volume
import Database from 'better-sqlite3';
const db = new Database('/data/app.db');
// File uploads
app.use('/uploads', express.static('/data/uploads'));
// Application cache
const CACHE_DIR = '/data/cache';
Anti-pattern: Relying on the container filesystem for persistence. Without a volume, all data is lost on redeploy.
Monorepo Support
Root Directory Setting
For monorepos, set the root directory per service:
Project: my-monorepo
├── web → Root: apps/web
├── api → Root: apps/api
├── worker → Root: apps/worker
└── shared → (packages/shared, used by all)
Watch Paths
Configure which paths trigger a rebuild for each service:
Service: web
Watch paths: apps/web/**, packages/shared/**
Service: api
Watch paths: apps/api/**, packages/shared/**
This prevents unnecessary rebuilds when unrelated code changes. Configure in the dashboard under Service → Settings → Watch Paths.
Build Configuration per Service
// apps/web/railway.json
{
"build": {
"builder": "NIXPACKS",
"buildCommand": "cd ../.. && npx turbo run build --filter=web"
},
"deploy": {
"startCommand": "node dist/server.js"
}
}
Private Networking
Services within a project communicate over an internal network:
web.railway.internal:PORT
api.railway.internal:PORT
worker.railway.internal:PORT
Configuring Internal Communication
// In the web service, call the API service internally
const API_URL = process.env.RAILWAY_ENVIRONMENT === 'production'
? `http://${process.env.API_PRIVATE_DOMAIN}:${process.env.API_PORT}`
: 'http://localhost:4000';
const response = await fetch(`${API_URL}/internal/process`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
Set service references in environment variables:
API_PRIVATE_DOMAIN=${{api.RAILWAY_PRIVATE_DOMAIN}}
API_PORT=${{api.PORT}}
DNS Resolution
Private networking uses IPv6 internally. Ensure your application binds to :: (all interfaces):
// Express
app.listen(PORT, '::', () => {
console.log(`Listening on port ${PORT}`);
});
Anti-pattern: Using public URLs for service-to-service communication. This adds latency and egress costs. Always use private networking within a project.
Cron Jobs
Using the Railway Cron Service
// railway.json for cron service
{
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "node dist/cron.js",
"cronSchedule": "0 */6 * * *"
}
}
Cron Patterns
"*/5 * * * *" # Every 5 minutes
"0 * * * *" # Every hour
"0 0 * * *" # Daily at midnight
"0 0 * * 1" # Weekly on Monday
"0 0 1 * *" # Monthly on the 1st
Cron Service Setup
// dist/cron.js
async function run() {
console.log('Cron job started:', new Date().toISOString());
await cleanupExpiredSessions();
await sendScheduledEmails();
await aggregateAnalytics();
console.log('Cron job completed');
process.exit(0); // Exit after completion
}
run().catch((err) => {
console.error('Cron job failed:', err);
process.exit(1);
});
Anti-pattern: Running cron logic inside your web service with setInterval. It won't survive deploys and runs on every instance. Use a dedicated cron service.
Custom Domains
# Add custom domain via dashboard
# Service → Settings → Custom Domain → Add Domain
# DNS configuration:
# CNAME: www → your-service.up.railway.app
# A record: @ → Railway IP (shown in dashboard)
Railway auto-provisions TLS certificates via Let's Encrypt.
Deployment Workflow
# Deploy from local (useful for testing)
railway up
# Deploy from specific directory
railway up --path ./apps/web
# View logs
railway logs
# Open the deployed app
railway open
# Run one-off commands
railway run npm run migrate
railway run node scripts/seed.js
# Connect to database
railway connect postgres
Common Anti-Patterns
- Not using variable references: Manually copying database URLs between services instead of using
${{Postgres.DATABASE_URL}}. - Missing health checks: Without a health check path, Railway can't detect unhealthy deployments.
- Hardcoding PORT: Railway assigns the port. Always use
process.env.PORT. - Public service-to-service calls: Use private networking (
.railway.internal) for internal traffic. - No watch paths in monorepos: Every commit rebuilds every service, wasting build minutes.
Deployment Checklist
-
railway.jsonor Nixpacks configuration set per service - Environment variables configured with proper references
- Databases provisioned and connected via private networking
- Health check endpoint implemented
- Watch paths configured for monorepo services
- Volumes attached for persistent storage needs
- Cron jobs configured as separate services
- Custom domain added with correct DNS records
- Logging and monitoring verified via
railway logs
Install this skill directly: skilldb add deployment-patterns-skills
Related Skills
database-deployment
Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.
docker-deployment
Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.
fly-io-deployment
Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.
github-actions-cd
Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.
monitoring-post-deploy
Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.
netlify-deployment
Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.