| name | deployment-guide |
| description | Vercel frontend + Railway/Fly.io backend deployment, env vars, health checks. Use when deploying a Next.js frontend to Vercel or a NestJS backend to Railway or Fly.io, or troubleshooting a deployment issue. |
Deployment Guide
Frontend — Vercel
Setup
npm i -g vercel
vercel login
vercel link
vercel
vercel --prod
vercel.json
{
"framework": "nextjs",
"buildCommand": "pnpm run build",
"installCommand": "pnpm install --frozen-lockfile",
"outputDirectory": ".next",
"regions": ["iad1"],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
},
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store, max-age=0" }
]
},
{
"source": "/_next/static/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
],
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/api/:path*"
}
]
}
Vercel Environment Variables
vercel env add NEXT_PUBLIC_API_URL production
vercel env add NEXT_PUBLIC_API_URL preview
vercel env add NEXT_PUBLIC_API_URL development
vercel env pull .env.local
next.config.ts for Production
import type { NextConfig } from 'next'
const config: NextConfig = {
output: 'standalone',
reactStrictMode: true,
images: {
domains: ['cdn.example.com', 'avatars.githubusercontent.com'],
formats: ['image/avif', 'image/webp'],
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
],
},
]
},
async redirects() {
return [
{ source: '/old-path', destination: '/new-path', permanent: true },
]
},
}
export default config
Backend — Railway
Setup
npm i -g @railway/cli
railway login
railway init
railway up
railway open
railway logs
railway run pnpm run migration:run
railway.json
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "apps/api/Dockerfile"
},
"deploy": {
"startCommand": "node dist/main",
"healthcheckPath": "/health/liveness",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
Required Environment Variables (Railway)
DATABASE_URL = ${{Postgres.DATABASE_URL}}
REDIS_URL = ${{Redis.REDIS_URL}}
NODE_ENV = production
PORT = ${{PORT}}
JWT_SECRET = <generate: openssl rand -base64 32>
CORS_ORIGINS = https://your-app.vercel.app
LOG_LEVEL = info
Backend — Fly.io (Alternative)
Setup
curl -L https://fly.io/install.sh | sh
fly auth login
fly launch --name my-api --region iad --no-deploy
fly deploy
fly scale count 2
fly scale memory 512
fly logs
fly ssh console
fly.toml
app = "my-api"
primary_region = "iad"
[build]
dockerfile = "apps/api/Dockerfile"
[env]
NODE_ENV = "production"
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
[http_service.concurrency]
type = "connections"
hard_limit = 200
soft_limit = 150
[[vm]]
memory = "512mb"
cpu_kind = "shared"
cpus = 1
[checks]
[checks.health]
grace_period = "30s"
interval = "15s"
method = "GET"
path = "/health/liveness"
port = 8080
timeout = "5s"
type = "http"
Health Check Endpoints
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
) {}
@Get('liveness')
liveness() {
return { status: 'ok', timestamp: new Date().toISOString() }
}
@Get('readiness')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database', { timeout: 3000 }),
])
}
@Get()
@HealthCheck()
full() {
return this.health.check([
() => this.db.pingCheck('database'),
])
}
}
Database Migrations on Deploy
import { NestFactory } from '@nestjs/core'
import { MigrationModule } from './migration/migration.module'
async function runMigrations() {
const app = await NestFactory.createApplicationContext(MigrationModule)
const dataSource = app.get(DataSource)
console.log('Running migrations...')
const migrations = await dataSource.runMigrations()
console.log(`Ran ${migrations.length} migrations`)
await app.close()
process.exit(0)
}
runMigrations().catch(err => {
console.error('Migration failed:', err)
process.exit(1)
})
{
"scripts": {
"migration:run": "ts-node src/cli.ts",
"start:prod": "node dist/main"
}
}
Zero-Downtime Deployments
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableShutdownHooks()
await app.listen(process.env.PORT ?? 3000)
console.log(`API running on port ${process.env.PORT ?? 3000}`)
}
Environment Checklist
Production Environment Variables Checklist:
□ NODE_ENV=production
□ DATABASE_URL (managed service with connection pooling)
□ REDIS_URL (if using caching/sessions)
□ JWT_SECRET (min 32 chars, random)
□ CORS_ORIGINS (exact frontend domains, no wildcards)
□ LOG_LEVEL=info (not debug)
□ PORT (injected by platform)
Frontend:
□ NEXT_PUBLIC_API_URL (production backend URL)
□ No secrets in NEXT_PUBLIC_* vars (they're exposed to browser)
Forbidden Patterns
- Never deploy with
NODE_ENV=development in production
- Never use the same JWT secret across environments
- Never skip health check endpoints — platforms use them for traffic routing
- Never run database migrations during app startup — run them as a separate step
- Never put
NEXT_PUBLIC_* values that are secrets — they're bundled into the browser
- Never deploy without verifying the build passes CI first
- Never use
0.0.0.0 as CORS_ORIGINS in production — specify exact origins