Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide38 min read

Railway

Railway is a platform for deploying applications and databases in the cloud with automatic provisioning and scaling.

Railway - Kompletny Przewodnik po Nowoczesnej Platformie Deployment

Czym jest Railway?

Railway to nowoczesna platforma infrastructure-as-a-service (IaaS), która pozwala deployować aplikacje, bazy danych i serwisy z jednego miejsca. Railway łączy prostotę Heroku z elastycznością i przystępną ceną - jest to idealne rozwiązanie dla developerów, którzy chcą skupić się na kodzie, a nie na infrastrukturze.

Railway wyróżnia się wyjątkowo prostym procesem deploymentu - wystarczy połączyć repozytorium GitHub, a platforma automatycznie wykryje framework, zbuduje aplikację i wdroży ją na produkcję. Każdy push do main branch automatycznie triggeruje nowy deployment, co czyni Railway idealnym narzędziem dla continuous deployment.

Platforma została założona w 2020 roku i szybko zdobyła popularność wśród startupów i indywidualnych developerów jako alternatywa dla Heroku (który znacząco podniósł ceny i usunął darmowy tier).

Dlaczego Railway?

Kluczowe zalety Railway

  1. Deploy w minutę - Z GitHub lub CLI, automatyczna detekcja frameworków
  2. Bazy danych jednym klikiem - PostgreSQL, MySQL, MongoDB, Redis natychmiast
  3. Automatyczne skalowanie - Horizontal i vertical scaling według potrzeb
  4. Preview Environments - Oddzielne środowiska dla każdego PR
  5. Zero konfiguracji - Nixpacks automatycznie wykrywa i buduje projekty
  6. Prosty pricing - Pay-as-you-go bez niespodzianek
  7. Monorepo support - Deploy wielu serwisów z jednego repo
  8. Private networking - Bezpieczna komunikacja między serwisami

Railway vs Heroku vs Vercel vs Render

CechaRailwayHerokuVercelRender
Free tier$5 credit❌ (usunięty)✅ (limited)✅ (limited)
Backend✅ Pełne wsparcie✅ Pełne❌ Serverless only✅ Pełne
Bazy danych✅ Wbudowane✅ Add-ons❌ Brak✅ Wbudowane
Docker✅ Natywne✅ Stack❌ Brak✅ Natywne
Preview envs✅ Tak❌ Brak✅ Tak✅ Tak
Auto-scaling✅ Tak$$ Drogie✅ Tak✅ Tak
Pricing modelUsage-basedDyno hoursInvocationsUsage-based
Sleep policy❌ Brak sleepingu✅ Free śpiN/A✅ Free śpi

Rozpoczęcie pracy z Railway

Instalacja CLI

Code
Bash
# macOS (Homebrew)
brew install railway

# npm
npm install -g @railway/cli

# curl (Linux/macOS)
curl -fsSL https://railway.app/install.sh | sh

# Weryfikacja instalacji
railway --version

Logowanie

Code
Bash
# Logowanie przez przeglądarkę
railway login

# Logowanie przez token (CI/CD)
export RAILWAY_TOKEN=your-token
railway login --token

Pierwszy deployment

Code
Bash
# Inicjalizacja projektu
railway init

# Wybierz:
# 1. Empty Project - czysty projekt
# 2. From Template - z gotowego szablonu

# Deploy z bieżącego katalogu
railway up

# Otwórz dashboard projektu
railway open

Deploy z GitHub

Code
Bash
# 1. Przejdź do railway.app
# 2. Kliknij "New Project"
# 3. Wybierz "Deploy from GitHub repo"
# 4. Autoryzuj GitHub i wybierz repo
# 5. Railway automatycznie wykryje framework i zdeployuje

# Każdy push do main = automatyczny deploy
git push origin main

Struktura projektu Railway

Projekt i serwisy

Code
TEXT
Railway Project
├── Service: API (Node.js)
│   ├── Deployments
│   ├── Variables
│   ├── Logs
│   └── Metrics
├── Service: Web (Next.js)
│   ├── Deployments
│   └── Variables
├── Service: PostgreSQL
│   └── Connection string (auto-exposed)
├── Service: Redis
│   └── Connection string (auto-exposed)
└── Service: Worker (Background jobs)
    └── Cron schedule

Environment Variables

Code
Bash
# Ustawianie zmiennych z CLI
railway variables set API_KEY=secret123
railway variables set DATABASE_URL=postgres://...

# Wyświetl zmienne
railway variables

# Usuń zmienną
railway variables delete API_KEY

# Reference do innych serwisów (w dashboard)
# ${{postgres.DATABASE_URL}} - automatycznie dostępne

railway.toml - konfiguracja projektu

railway.toml
TOML
# railway.toml

[build]
# Komenda budowania
builder = "NIXPACKS"
buildCommand = "npm run build"

[deploy]
# Komenda startowa
startCommand = "npm start"
# Health check endpoint
healthcheckPath = "/health"
# Timeout health checku (sekundy)
healthcheckTimeout = 300
# Liczba replik
numReplicas = 1
# Sleep po nieaktywności (tylko hobby)
sleepApplication = false
# Restart policy
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

[build.nixpacksPlan]
# Custom Nix packages
phases.setup.nixPkgs = ["...", "ffmpeg"]

# Environment variables dla build
[build.variables]
NODE_ENV = "production"

Bazy danych na Railway

Railway oferuje pełne zarządzane bazy danych jednym klikiem.

PostgreSQL

Code
Bash
# Dodaj PostgreSQL do projektu
railway add postgresql

# Automatycznie dostępne zmienne:
# DATABASE_URL=postgres://user:pass@host:port/db
# PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE

Przykład użycia z Prisma:

prisma/schema.prisma
Prisma
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}
TSsrc/db.ts
TypeScript
// src/db.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export async function getUsers() {
  return prisma.user.findMany()
}

export async function createUser(email: string, name?: string) {
  return prisma.user.create({
    data: { email, name }
  })
}

Migracje na Railway:

Code
Bash
# Uruchom migracje przed deploymentem
# W railway.toml:
[deploy]
startCommand = "npx prisma migrate deploy && npm start"

# Lub w package.json
"scripts": {
  "start": "npx prisma migrate deploy && node dist/index.js"
}

MySQL

Code
Bash
railway add mysql

# Zmienne:
# MYSQL_URL=mysql://user:pass@host:port/db
# MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD, MYSQLDATABASE
Code
TypeScript
// Drizzle ORM z MySQL
import { drizzle } from 'drizzle-orm/mysql2'
import mysql from 'mysql2/promise'

const connection = await mysql.createConnection(process.env.MYSQL_URL!)
const db = drizzle(connection)

// Użycie
const users = await db.select().from(usersTable)

MongoDB

Code
Bash
railway add mongodb

# Zmienne:
# MONGO_URL=mongodb://user:pass@host:port/db
Code
TypeScript
// Mongoose
import mongoose from 'mongoose'

await mongoose.connect(process.env.MONGO_URL!)

const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: String,
  createdAt: { type: Date, default: Date.now }
})

export const User = mongoose.model('User', UserSchema)

Redis

Code
Bash
railway add redis

# Zmienne:
# REDIS_URL=redis://default:pass@host:port
Code
TypeScript
// ioredis
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL!)

// Cache
await redis.set('user:1', JSON.stringify(user), 'EX', 3600)
const cached = await redis.get('user:1')

// Pub/Sub
const pub = new Redis(process.env.REDIS_URL!)
const sub = new Redis(process.env.REDIS_URL!)

sub.subscribe('notifications')
sub.on('message', (channel, message) => {
  console.log(`Received: ${message}`)
})

await pub.publish('notifications', 'Hello!')

// Queue (BullMQ)
import { Queue, Worker } from 'bullmq'

const queue = new Queue('emails', {
  connection: { url: process.env.REDIS_URL }
})

const worker = new Worker('emails', async (job) => {
  await sendEmail(job.data)
}, {
  connection: { url: process.env.REDIS_URL }
})

Docker na Railway

Railway natywnie wspiera Dockerfiles.

Podstawowy Dockerfile

Code
DOCKERFILE
# Dockerfile
FROM node:20-alpine

WORKDIR /app

# Cache dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy source
COPY . .

# Build
RUN npm run build

# Run
EXPOSE 3000
CMD ["npm", "start"]

Multi-stage build

Code
DOCKERFILE
# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS runner

WORKDIR /app

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodeuser

# Copy only necessary files
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER nodeuser

EXPOSE 3000
ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Python z Dockerfile

Code
DOCKERFILE
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source
COPY . .

# Run with gunicorn
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Go z Dockerfile

Code
DOCKERFILE
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Production stage
FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

Monorepo na Railway

Railway doskonale wspiera monorepo z wieloma serwisami.

Struktura monorepo

Code
TEXT
my-monorepo/
├── apps/
│   ├── api/
│   │   ├── package.json
│   │   ├── railway.toml
│   │   └── src/
│   ├── web/
│   │   ├── package.json
│   │   ├── railway.toml
│   │   └── src/
│   └── worker/
│       ├── package.json
│       ├── railway.toml
│       └── src/
├── packages/
│   ├── shared/
│   └── ui/
├── package.json
└── turbo.json

Konfiguracja serwisów

apps/api/railway.toml
TOML
# apps/api/railway.toml
[build]
buildCommand = "cd ../.. && npm run build --filter=api"

[deploy]
startCommand = "node dist/index.js"
apps/web/railway.toml
TOML
# apps/web/railway.toml
[build]
buildCommand = "cd ../.. && npm run build --filter=web"

[deploy]
startCommand = "npm start"

Root directory w dashboard

Code
TEXT
W Railway Dashboard:
1. Przejdź do serwisu
2. Settings → General → Root Directory
3. Ustaw: apps/api (dla API)
4. Ustaw: apps/web (dla frontendu)

Railway wykryje zmiany tylko w odpowiednim katalogu.

Turborepo + Railway

turbo.json
JSON
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false
    }
  }
}
package.json
JSON
// package.json (root)
{
  "scripts": {
    "build": "turbo run build",
    "build:api": "turbo run build --filter=api",
    "build:web": "turbo run build --filter=web"
  }
}

Preview Environments

Railway automatycznie tworzy środowiska preview dla każdego PR.

Konfiguracja

Code
TEXT
1. W Dashboard → Settings → Deployments
2. Włącz "Enable PR Deploys"
3. Każdy PR otrzyma unikalny URL

Format URL:
https://[service-name]-pr-[number].up.railway.app

Zmienne dla preview

Code
Bash
# Zmienne specyficzne dla preview environments
# Ustawiane w Dashboard → Environment: Preview

DATABASE_URL=${{postgres-preview.DATABASE_URL}}
API_URL=https://api-pr-${{ RAILWAY_PR_NUMBER }}.up.railway.app

Izolacja danych

Code
TEXT
Dla preview environments możesz:

1. Używać shared development database
2. Tworzyć osobne bazy dla każdego PR (drogie)
3. Używać seeded test data

Rekomendacja:
- Shared dev database dla prostych projektów
- Database branching (jak Neon) dla izolacji

Cron Jobs i Workers

Scheduled Tasks (Cron)

Code
Bash
# W Dashboard → Service → Settings → Cron
# Lub w railway.toml:

[deploy]
cronSchedule = "0 0 * * *"  # Codziennie o północy
TSworker/src/cron.ts
TypeScript
// worker/src/cron.ts
import { CronJob } from 'cron'

// Cleanup old sessions
const cleanupJob = new CronJob('0 0 * * *', async () => {
  console.log('Running cleanup...')
  await db.session.deleteMany({
    where: {
      expiresAt: { lt: new Date() }
    }
  })
  console.log('Cleanup complete')
})

cleanupJob.start()

Background Workers

TSworker/src/index.ts
TypeScript
// worker/src/index.ts
import { Worker, Queue } from 'bullmq'

const connection = { url: process.env.REDIS_URL }

// Email worker
const emailWorker = new Worker('emails', async (job) => {
  const { to, subject, body } = job.data

  await sendEmail({ to, subject, body })

  console.log(`Email sent to ${to}`)
}, { connection })

// Image processing worker
const imageWorker = new Worker('images', async (job) => {
  const { imageUrl, userId } = job.data

  const processed = await processImage(imageUrl)
  await uploadToS3(processed)
  await notifyUser(userId, 'Image processed!')

}, {
  connection,
  concurrency: 5  // Process 5 jobs simultaneously
})

console.log('Workers started')
TSapi/src/routes/images.ts
TypeScript
// api/src/routes/images.ts
import { Queue } from 'bullmq'

const imageQueue = new Queue('images', {
  connection: { url: process.env.REDIS_URL }
})

app.post('/api/images/process', async (req, res) => {
  const { imageUrl } = req.body
  const userId = req.user.id

  await imageQueue.add('process', { imageUrl, userId })

  res.json({ message: 'Processing started' })
})

Private Networking

Railway umożliwia bezpieczną komunikację między serwisami.

Internal URLs

Code
TEXT
Każdy serwis otrzymuje internal URL:
[service-name].railway.internal

Przykład:
api.railway.internal:3000
postgres.railway.internal:5432
redis.railway.internal:6379

Konfiguracja

TSapi/src/config.ts
TypeScript
// api/src/config.ts
export const config = {
  // Internal URL dla komunikacji między serwisami
  authServiceUrl: process.env.AUTH_SERVICE_URL || 'http://auth.railway.internal:3001',

  // External URL dla klientów
  publicApiUrl: process.env.PUBLIC_API_URL || 'https://api.example.com',

  // Database - zawsze internal
  databaseUrl: process.env.DATABASE_URL // postgres.railway.internal
}
TSapi/src/services/auth.ts
TypeScript
// api/src/services/auth.ts
import { config } from '../config'

export async function verifyToken(token: string) {
  // Wywołanie do internal auth service
  const response = await fetch(`${config.authServiceUrl}/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token })
  })

  return response.json()
}

Service Discovery

docker-compose.yml
YAML
# docker-compose.yml (dla local development)
services:
  api:
    environment:
      - AUTH_SERVICE_URL=http://auth:3001
      - DATABASE_URL=postgres://postgres:postgres@db:5432/app

  auth:
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/app

  db:
    image: postgres:15

Skalowanie

Vertical Scaling

Code
TEXT
Railway Dashboard → Service → Settings

Resources:
├── vCPU: 0.5 → 8 vCPUs
├── Memory: 512MB → 32GB
└── Disk: Persistent volumes

Autoscaling:
├── Min replicas: 1
├── Max replicas: 10
├── Target CPU: 70%
└── Target Memory: 80%

Horizontal Scaling

railway.toml
TOML
# railway.toml
[deploy]
numReplicas = 3

# Lub przez dashboard → Scale

Health Checks

railway.toml
TOML
# railway.toml
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 300
TSapi/src/routes/health.ts
TypeScript
// api/src/routes/health.ts
import express from 'express'

const router = express.Router()

router.get('/health', async (req, res) => {
  try {
    // Check database connection
    await db.$queryRaw`SELECT 1`

    // Check Redis
    await redis.ping()

    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'connected',
        redis: 'connected'
      }
    })
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    })
  }
})

export default router

Logowanie i Monitoring

Railway Logs

Code
Bash
# CLI
railway logs

# Z filtrowaniem
railway logs --filter "error"

# Follow (live)
railway logs -f

Structured Logging

TSlogger.ts
TypeScript
// logger.ts
import pino from 'pino'

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label })
  },
  timestamp: pino.stdTimeFunctions.isoTime
})

// Użycie
logger.info({ userId: user.id }, 'User logged in')
logger.error({ err, requestId }, 'Request failed')

Metrics

TSmetrics.ts
TypeScript
// metrics.ts
import { collectDefaultMetrics, Registry, Counter, Histogram } from 'prom-client'

const register = new Registry()
collectDefaultMetrics({ register })

// Custom metrics
export const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status'],
  registers: [register]
})

export const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'path'],
  buckets: [0.1, 0.5, 1, 2, 5],
  registers: [register]
})

// Endpoint dla Prometheus
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType)
  res.end(await register.metrics())
})

Integracja z zewnętrznymi serwisami

Code
TypeScript
// Sentry
import * as Sentry from '@sentry/node'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.RAILWAY_ENVIRONMENT || 'development',
  release: process.env.RAILWAY_GIT_COMMIT_SHA
})

// Datadog
import tracer from 'dd-trace'

tracer.init({
  service: process.env.DD_SERVICE || 'api',
  env: process.env.RAILWAY_ENVIRONMENT,
  version: process.env.RAILWAY_GIT_COMMIT_SHA
})

CI/CD z Railway

GitHub Actions

.github/workflows/deploy.yml
YAML
# .github/workflows/deploy.yml
name: Deploy to Railway

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Railway CLI
        run: npm install -g @railway/cli

      - name: Deploy
        run: railway up --service api
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

      - name: Run migrations
        run: railway run npx prisma migrate deploy
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Deploy z testami

Code
YAML
name: Test and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Railway
        run: |
          npm install -g @railway/cli
          railway up
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Rollback

Code
Bash
# Z CLI
railway rollback

# Wybierz poprzedni deployment z listy
# Lub przez dashboard → Deployments → Rollback

Integracje z frameworkami

Next.js

JSnext.config.js
JavaScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',  // Optymalizacja dla Railway
  experimental: {
    serverComponentsExternalPackages: ['@prisma/client']
  }
}

module.exports = nextConfig
railway.toml
TOML
# railway.toml
[build]
builder = "NIXPACKS"

[deploy]
startCommand = "node server.js"
healthcheckPath = "/api/health"

NestJS

TSmain.ts
TypeScript
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  // Health check
  app.get('/health', (req, res) => res.send('OK'))

  const port = process.env.PORT || 3000
  await app.listen(port, '0.0.0.0')

  console.log(`Application running on port ${port}`)
}

bootstrap()

FastAPI (Python)

PYmain.py
Python
# main.py
from fastapi import FastAPI
import os

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "healthy"}

@app.get("/")
def root():
    return {"message": "Hello from Railway!"}

if __name__ == "__main__":
    import uvicorn
    port = int(os.getenv("PORT", 8000))
    uvicorn.run(app, host="0.0.0.0", port=port)
railway.toml
TOML
# railway.toml
[build]
builder = "NIXPACKS"

[deploy]
startCommand = "uvicorn main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"

Go (Gin)

main.go
Go
// main.go
package main

import (
    "os"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "healthy"})
    })

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello from Railway!"})
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    r.Run(":" + port)
}

Django

PYsettings.py
Python
# settings.py
import os
import dj_database_url

DEBUG = os.getenv('DEBUG', 'False') == 'True'

ALLOWED_HOSTS = [
    '.railway.app',
    'localhost',
    '127.0.0.1'
]

# Database
DATABASES = {
    'default': dj_database_url.config(
        default=os.getenv('DATABASE_URL'),
        conn_max_age=600
    )
}

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# WhiteNoise for static files
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ...
]

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Code
TEXT
# Procfile (alternatywa dla railway.toml)
web: gunicorn myproject.wsgi --bind 0.0.0.0:$PORT

Custom Domains

Dodawanie domeny

Code
TEXT
1. Railway Dashboard → Service → Settings → Domains
2. Kliknij "Add Custom Domain"
3. Wpisz domenę: api.example.com
4. Dodaj rekordy DNS:

   CNAME api -> [your-service].up.railway.app

   Lub dla root domain:
   A record -> Railway IP

SSL/TLS

Code
TEXT
Railway automatycznie:
- Generuje certyfikaty Let's Encrypt
- Obsługuje HTTPS
- Redirect HTTP → HTTPS
- Odnawia certyfikaty

Wildcard domains

Code
TEXT
Dla *.example.com:
1. Dodaj CNAME *.example -> [service].up.railway.app
2. W Railway dodaj *.example.com jako custom domain
3. Railway obsłuży wszystkie subdomeny

Persistent Storage

Volumes

Code
TEXT
Railway wspiera persistent volumes dla:
- Plików uploadowanych przez użytkowników
- Cache
- Lokalnych baz danych (SQLite)

Dashboard → Service → Volumes → Add Volume

Mount path: /data
Size: 1GB - 100GB
Code
TypeScript
// Użycie volume
import fs from 'fs/promises'
import path from 'path'

const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'

export async function saveFile(file: Buffer, filename: string) {
  const filepath = path.join(UPLOAD_DIR, filename)
  await fs.mkdir(path.dirname(filepath), { recursive: true })
  await fs.writeFile(filepath, file)
  return filepath
}

Object Storage (S3-compatible)

Code
TypeScript
// Rekomendacja: Używaj S3/R2/MinIO dla plików
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const s3 = new S3Client({
  region: 'auto',
  endpoint: process.env.S3_ENDPOINT,  // Cloudflare R2, MinIO, etc.
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!
  }
})

export async function uploadFile(file: Buffer, key: string) {
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: file
  }))

  return `${process.env.S3_PUBLIC_URL}/${key}`
}

Cennik Railway

Plany

Code
TEXT
Trial:
├── Cena: $0
├── $5 credit jednorazowy
├── Pełny dostęp do platformy
├── Ograniczenie: 500 execution hours/miesiąc
└── Idealne do testowania

Hobby:
├── Cena: $5/miesiąc
├── Includes $5 usage
├── 8GB RAM per service
├── 8 vCPU per service
├── Unlimited deployments
└── Dla side projects

Pro:
├── Cena: $20/miesiąc per member
├── Includes $10 usage per member
├── 32GB RAM per service
├── 32 vCPU per service
├── Teams & collaboration
├── Priority support
└── Dla produkcji

Enterprise:
├── Custom pricing
├── Dedicated infrastructure
├── SLA guarantees
├── SSO/SAML
└── Advanced security

Usage Pricing

Code
TEXT
Compute:
├── vCPU: $0.000463/minute ($20/month for 1 vCPU 24/7)
└── Memory: $0.000231/GB/minute ($10/month for 1GB 24/7)

Network:
├── Egress: $0.10/GB (first 100GB free)
└── Ingress: Free

Databases:
├── PostgreSQL: compute + storage ($0.25/GB/month)
├── Redis: compute only
└── MongoDB: compute + storage

Kalkulacja kosztów

Code
TEXT
Przykład: Mały SaaS

API (Node.js):
├── 0.5 vCPU: ~$10/month
├── 512MB RAM: ~$5/month
└── Subtotal: ~$15/month

PostgreSQL:
├── 0.25 vCPU: ~$5/month
├── 256MB RAM: ~$2.50/month
├── 10GB storage: ~$2.50/month
└── Subtotal: ~$10/month

Redis:
├── 0.25 vCPU: ~$5/month
├── 256MB RAM: ~$2.50/month
└── Subtotal: ~$7.50/month

Total: ~$32.50/month (z $5 included w Hobby)
Actual cost: ~$27.50/month

Best Practices

Bezpieczeństwo

Code
TypeScript
// 1. Używaj zmiennych środowiskowych
const apiKey = process.env.API_KEY  // Nigdy hardcoded!

// 2. Waliduj input
import { z } from 'zod'

const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
})

// 3. Używaj HTTPS only
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(`https://${req.headers.host}${req.url}`)
  }
  next()
})

// 4. Rate limiting
import rateLimit from 'express-rate-limit'

app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}))

Optymalizacja

Code
DOCKERFILE
# 1. Multi-stage builds
FROM node:20-alpine AS builder
# ... build steps ...

FROM node:20-alpine AS runner
# ... tylko production dependencies ...

# 2. .dockerignore
node_modules
.git
.env*
*.md
tests/
Code
TypeScript
// 3. Connection pooling
import { PrismaClient } from '@prisma/client'

// Singleton pattern dla Prisma
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query'] : []
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Graceful Shutdown

Code
TypeScript
// Obsługa SIGTERM
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully')

  // Stop accepting new requests
  server.close()

  // Close database connections
  await prisma.$disconnect()
  await redis.quit()

  console.log('Cleanup complete')
  process.exit(0)
})

FAQ - Często zadawane pytania

Czy Railway ma darmowy tier?

Tak, Railway oferuje $5 jednorazowego kredytu na trial. Po jego wykorzystaniu możesz przejść na plan Hobby ($5/miesiąc) który zawiera $5 usage credit. Nie ma jednak permanentnie darmowego planu jak kiedyś Heroku.

Jak Railway wypada vs Heroku?

Railway jest generalnie tańszy i nowocześniejszy:

  • Brak "dyno sleeping" nawet na najtańszym planie
  • Natywne wsparcie dla Docker
  • Preview environments
  • Prostszy pricing (pay-as-you-go)
  • Lepsze developer experience

Heroku może być lepszy dla enterprise z wymaganiami compliance.

Czy mogę deployować statyczne strony?

Tak, ale Railway nie jest do tego zoptymalizowany. Dla statycznych stron lepsze są:

  • Vercel
  • Netlify
  • Cloudflare Pages

Railway świeci przy backendach i full-stack apps.

Jak debugować problemy z deploymentem?

  1. Sprawdź logi: railway logs
  2. Sprawdź build logs w dashboard
  3. Uruchom lokalnie z tymi samymi zmiennymi: railway run npm start
  4. Zweryfikuj Dockerfile/Nixpacks configuration
  5. Sprawdź health check endpoint

Czy Railway obsługuje WebSockets?

Tak, Railway w pełni obsługuje WebSockets bez dodatkowej konfiguracji. Pamiętaj o sticky sessions przy skalowaniu horyzontalnym.

Jak zrobić backup bazy danych?

Code
Bash
# PostgreSQL dump
railway run pg_dump $DATABASE_URL > backup.sql

# Lub przez plugin pg_dump w dashboard
# Settings → Backups → Create Backup

Podsumowanie

Railway to nowoczesna platforma deployment, która łączy prostotę Heroku z elastycznością i przystępną ceną:

  • Szybki start - Deploy w minutę z GitHub lub CLI
  • Bazy danych jednym klikiem - PostgreSQL, MySQL, MongoDB, Redis
  • Docker native - Pełne wsparcie dla kontenerów
  • Preview environments - Automatyczne środowiska dla PR
  • Skalowanie - Horizontal i vertical scaling
  • Prosty pricing - Pay-as-you-go bez niespodzianek

Railway jest idealny dla startupów, side projects i zespołów, które chcą skupić się na kodzie, a nie na infrastrukturze.


Railway - a complete guide to the modern deployment platform

What is Railway?

Railway is a modern infrastructure-as-a-service (IaaS) platform that lets you deploy applications, databases, and services from one place. Railway combines the simplicity of Heroku with flexibility and affordable pricing - it is an ideal solution for developers who want to focus on code, not infrastructure.

Railway stands out with an exceptionally simple deployment process - just connect your GitHub repository and the platform will automatically detect the framework, build the application, and deploy it to production. Every push to the main branch automatically triggers a new deployment, making Railway the perfect tool for continuous deployment.

The platform was founded in 2020 and quickly gained popularity among startups and individual developers as an alternative to Heroku (which significantly raised prices and removed its free tier).

Why Railway?

Key advantages of Railway

  1. Deploy in a minute - From GitHub or CLI, automatic framework detection
  2. One-click databases - PostgreSQL, MySQL, MongoDB, Redis instantly
  3. Automatic scaling - Horizontal and vertical scaling as needed
  4. Preview Environments - Separate environments for each PR
  5. Zero configuration - Nixpacks automatically detects and builds projects
  6. Simple pricing - Pay-as-you-go with no surprises
  7. Monorepo support - Deploy multiple services from one repo
  8. Private networking - Secure communication between services

Railway vs Heroku vs Vercel vs Render

FeatureRailwayHerokuVercelRender
Free tier$5 credit❌ (removed)✅ (limited)✅ (limited)
Backend✅ Full support✅ Full❌ Serverless only✅ Full
Databases✅ Built-in✅ Add-ons❌ None✅ Built-in
Docker✅ Native✅ Stack❌ None✅ Native
Preview envs✅ Yes❌ None✅ Yes✅ Yes
Auto-scaling✅ Yes$$ Expensive✅ Yes✅ Yes
Pricing modelUsage-basedDyno hoursInvocationsUsage-based
Sleep policy❌ No sleeping✅ Free sleepsN/A✅ Free sleeps

Getting started with Railway

CLI installation

Code
Bash
# macOS (Homebrew)
brew install railway

# npm
npm install -g @railway/cli

# curl (Linux/macOS)
curl -fsSL https://railway.app/install.sh | sh

# Verify installation
railway --version

Logging in

Code
Bash
# Log in through the browser
railway login

# Log in with a token (CI/CD)
export RAILWAY_TOKEN=your-token
railway login --token

First deployment

Code
Bash
# Initialize a project
railway init

# Choose:
# 1. Empty Project - a clean project
# 2. From Template - from a ready-made template

# Deploy from the current directory
railway up

# Open the project dashboard
railway open

Deploy from GitHub

Code
Bash
# 1. Go to railway.app
# 2. Click "New Project"
# 3. Select "Deploy from GitHub repo"
# 4. Authorize GitHub and select your repo
# 5. Railway will automatically detect the framework and deploy

# Every push to main = automatic deploy
git push origin main

Railway project structure

Projects and services

Code
TEXT
Railway Project
├── Service: API (Node.js)
│   ├── Deployments
│   ├── Variables
│   ├── Logs
│   └── Metrics
├── Service: Web (Next.js)
│   ├── Deployments
│   └── Variables
├── Service: PostgreSQL
│   └── Connection string (auto-exposed)
├── Service: Redis
│   └── Connection string (auto-exposed)
└── Service: Worker (Background jobs)
    └── Cron schedule

Environment variables

Code
Bash
# Set variables from CLI
railway variables set API_KEY=secret123
railway variables set DATABASE_URL=postgres://...

# Display variables
railway variables

# Delete a variable
railway variables delete API_KEY

# Reference to other services (in dashboard)
# ${{postgres.DATABASE_URL}} - automatically available

railway.toml - project configuration

railway.toml
TOML
# railway.toml

[build]
# Build command
builder = "NIXPACKS"
buildCommand = "npm run build"

[deploy]
# Start command
startCommand = "npm start"
# Health check endpoint
healthcheckPath = "/health"
# Health check timeout (seconds)
healthcheckTimeout = 300
# Number of replicas
numReplicas = 1
# Sleep after inactivity (hobby only)
sleepApplication = false
# Restart policy
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

[build.nixpacksPlan]
# Custom Nix packages
phases.setup.nixPkgs = ["...", "ffmpeg"]

# Environment variables for build
[build.variables]
NODE_ENV = "production"

Databases on Railway

Railway offers fully managed databases with a single click.

PostgreSQL

Code
Bash
# Add PostgreSQL to the project
railway add postgresql

# Automatically available variables:
# DATABASE_URL=postgres://user:pass@host:port/db
# PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE

Example usage with Prisma:

prisma/schema.prisma
Prisma
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}
TSsrc/db.ts
TypeScript
// src/db.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export async function getUsers() {
  return prisma.user.findMany()
}

export async function createUser(email: string, name?: string) {
  return prisma.user.create({
    data: { email, name }
  })
}

Migrations on Railway:

Code
Bash
# Run migrations before deployment
# In railway.toml:
[deploy]
startCommand = "npx prisma migrate deploy && npm start"

# Or in package.json
"scripts": {
  "start": "npx prisma migrate deploy && node dist/index.js"
}

MySQL

Code
Bash
railway add mysql

# Variables:
# MYSQL_URL=mysql://user:pass@host:port/db
# MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD, MYSQLDATABASE
Code
TypeScript
// Drizzle ORM with MySQL
import { drizzle } from 'drizzle-orm/mysql2'
import mysql from 'mysql2/promise'

const connection = await mysql.createConnection(process.env.MYSQL_URL!)
const db = drizzle(connection)

// Usage
const users = await db.select().from(usersTable)

MongoDB

Code
Bash
railway add mongodb

# Variables:
# MONGO_URL=mongodb://user:pass@host:port/db
Code
TypeScript
// Mongoose
import mongoose from 'mongoose'

await mongoose.connect(process.env.MONGO_URL!)

const UserSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: String,
  createdAt: { type: Date, default: Date.now }
})

export const User = mongoose.model('User', UserSchema)

Redis

Code
Bash
railway add redis

# Variables:
# REDIS_URL=redis://default:pass@host:port
Code
TypeScript
// ioredis
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL!)

// Cache
await redis.set('user:1', JSON.stringify(user), 'EX', 3600)
const cached = await redis.get('user:1')

// Pub/Sub
const pub = new Redis(process.env.REDIS_URL!)
const sub = new Redis(process.env.REDIS_URL!)

sub.subscribe('notifications')
sub.on('message', (channel, message) => {
  console.log(`Received: ${message}`)
})

await pub.publish('notifications', 'Hello!')

// Queue (BullMQ)
import { Queue, Worker } from 'bullmq'

const queue = new Queue('emails', {
  connection: { url: process.env.REDIS_URL }
})

const worker = new Worker('emails', async (job) => {
  await sendEmail(job.data)
}, {
  connection: { url: process.env.REDIS_URL }
})

Docker on Railway

Railway natively supports Dockerfiles.

Basic Dockerfile

Code
DOCKERFILE
# Dockerfile
FROM node:20-alpine

WORKDIR /app

# Cache dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy source
COPY . .

# Build
RUN npm run build

# Run
EXPOSE 3000
CMD ["npm", "start"]

Multi-stage build

Code
DOCKERFILE
# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS runner

WORKDIR /app

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodeuser

# Copy only necessary files
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER nodeuser

EXPOSE 3000
ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Python with Dockerfile

Code
DOCKERFILE
FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source
COPY . .

# Run with gunicorn
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Go with Dockerfile

Code
DOCKERFILE
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Production stage
FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

Monorepo on Railway

Railway has excellent support for monorepos with multiple services.

Monorepo structure

Code
TEXT
my-monorepo/
├── apps/
│   ├── api/
│   │   ├── package.json
│   │   ├── railway.toml
│   │   └── src/
│   ├── web/
│   │   ├── package.json
│   │   ├── railway.toml
│   │   └── src/
│   └── worker/
│       ├── package.json
│       ├── railway.toml
│       └── src/
├── packages/
│   ├── shared/
│   └── ui/
├── package.json
└── turbo.json

Service configuration

apps/api/railway.toml
TOML
# apps/api/railway.toml
[build]
buildCommand = "cd ../.. && npm run build --filter=api"

[deploy]
startCommand = "node dist/index.js"
apps/web/railway.toml
TOML
# apps/web/railway.toml
[build]
buildCommand = "cd ../.. && npm run build --filter=web"

[deploy]
startCommand = "npm start"

Root directory in the dashboard

Code
TEXT
In the Railway Dashboard:
1. Go to the service
2. Settings → General → Root Directory
3. Set: apps/api (for the API)
4. Set: apps/web (for the frontend)

Railway will detect changes only in the relevant directory.

Turborepo + Railway

turbo.json
JSON
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false
    }
  }
}
package.json
JSON
// package.json (root)
{
  "scripts": {
    "build": "turbo run build",
    "build:api": "turbo run build --filter=api",
    "build:web": "turbo run build --filter=web"
  }
}

Preview environments

Railway automatically creates preview environments for each PR.

Configuration

Code
TEXT
1. In Dashboard → Settings → Deployments
2. Enable "Enable PR Deploys"
3. Each PR will receive a unique URL

URL format:
https://[service-name]-pr-[number].up.railway.app

Variables for preview

Code
Bash
# Variables specific to preview environments
# Set in Dashboard → Environment: Preview

DATABASE_URL=${{postgres-preview.DATABASE_URL}}
API_URL=https://api-pr-${{ RAILWAY_PR_NUMBER }}.up.railway.app

Data isolation

Code
TEXT
For preview environments you can:

1. Use a shared development database
2. Create separate databases for each PR (expensive)
3. Use seeded test data

Recommendation:
- Shared dev database for simple projects
- Database branching (like Neon) for isolation

Cron jobs and workers

Scheduled tasks (Cron)

Code
Bash
# In Dashboard → Service → Settings → Cron
# Or in railway.toml:

[deploy]
cronSchedule = "0 0 * * *"  # Every day at midnight
TSworker/src/cron.ts
TypeScript
// worker/src/cron.ts
import { CronJob } from 'cron'

// Cleanup old sessions
const cleanupJob = new CronJob('0 0 * * *', async () => {
  console.log('Running cleanup...')
  await db.session.deleteMany({
    where: {
      expiresAt: { lt: new Date() }
    }
  })
  console.log('Cleanup complete')
})

cleanupJob.start()

Background workers

TSworker/src/index.ts
TypeScript
// worker/src/index.ts
import { Worker, Queue } from 'bullmq'

const connection = { url: process.env.REDIS_URL }

// Email worker
const emailWorker = new Worker('emails', async (job) => {
  const { to, subject, body } = job.data

  await sendEmail({ to, subject, body })

  console.log(`Email sent to ${to}`)
}, { connection })

// Image processing worker
const imageWorker = new Worker('images', async (job) => {
  const { imageUrl, userId } = job.data

  const processed = await processImage(imageUrl)
  await uploadToS3(processed)
  await notifyUser(userId, 'Image processed!')

}, {
  connection,
  concurrency: 5  // Process 5 jobs simultaneously
})

console.log('Workers started')
TSapi/src/routes/images.ts
TypeScript
// api/src/routes/images.ts
import { Queue } from 'bullmq'

const imageQueue = new Queue('images', {
  connection: { url: process.env.REDIS_URL }
})

app.post('/api/images/process', async (req, res) => {
  const { imageUrl } = req.body
  const userId = req.user.id

  await imageQueue.add('process', { imageUrl, userId })

  res.json({ message: 'Processing started' })
})

Private networking

Railway enables secure communication between services.

Internal URLs

Code
TEXT
Each service receives an internal URL:
[service-name].railway.internal

Example:
api.railway.internal:3000
postgres.railway.internal:5432
redis.railway.internal:6379

Configuration

TSapi/src/config.ts
TypeScript
// api/src/config.ts
export const config = {
  // Internal URL for service-to-service communication
  authServiceUrl: process.env.AUTH_SERVICE_URL || 'http://auth.railway.internal:3001',

  // External URL for clients
  publicApiUrl: process.env.PUBLIC_API_URL || 'https://api.example.com',

  // Database - always internal
  databaseUrl: process.env.DATABASE_URL // postgres.railway.internal
}
TSapi/src/services/auth.ts
TypeScript
// api/src/services/auth.ts
import { config } from '../config'

export async function verifyToken(token: string) {
  // Call to internal auth service
  const response = await fetch(`${config.authServiceUrl}/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token })
  })

  return response.json()
}

Service discovery

docker-compose.yml
YAML
# docker-compose.yml (for local development)
services:
  api:
    environment:
      - AUTH_SERVICE_URL=http://auth:3001
      - DATABASE_URL=postgres://postgres:postgres@db:5432/app

  auth:
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/app

  db:
    image: postgres:15

Scaling

Vertical scaling

Code
TEXT
Railway Dashboard → Service → Settings

Resources:
├── vCPU: 0.5 → 8 vCPUs
├── Memory: 512MB → 32GB
└── Disk: Persistent volumes

Autoscaling:
├── Min replicas: 1
├── Max replicas: 10
├── Target CPU: 70%
└── Target Memory: 80%

Horizontal scaling

railway.toml
TOML
# railway.toml
[deploy]
numReplicas = 3

# Or through dashboard → Scale

Health checks

railway.toml
TOML
# railway.toml
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 300
TSapi/src/routes/health.ts
TypeScript
// api/src/routes/health.ts
import express from 'express'

const router = express.Router()

router.get('/health', async (req, res) => {
  try {
    // Check database connection
    await db.$queryRaw`SELECT 1`

    // Check Redis
    await redis.ping()

    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'connected',
        redis: 'connected'
      }
    })
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    })
  }
})

export default router

Logging and monitoring

Railway logs

Code
Bash
# CLI
railway logs

# With filtering
railway logs --filter "error"

# Follow (live)
railway logs -f

Structured logging

TSlogger.ts
TypeScript
// logger.ts
import pino from 'pino'

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label })
  },
  timestamp: pino.stdTimeFunctions.isoTime
})

// Usage
logger.info({ userId: user.id }, 'User logged in')
logger.error({ err, requestId }, 'Request failed')

Metrics

TSmetrics.ts
TypeScript
// metrics.ts
import { collectDefaultMetrics, Registry, Counter, Histogram } from 'prom-client'

const register = new Registry()
collectDefaultMetrics({ register })

// Custom metrics
export const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status'],
  registers: [register]
})

export const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration',
  labelNames: ['method', 'path'],
  buckets: [0.1, 0.5, 1, 2, 5],
  registers: [register]
})

// Endpoint for Prometheus
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType)
  res.end(await register.metrics())
})

Integration with external services

Code
TypeScript
// Sentry
import * as Sentry from '@sentry/node'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.RAILWAY_ENVIRONMENT || 'development',
  release: process.env.RAILWAY_GIT_COMMIT_SHA
})

// Datadog
import tracer from 'dd-trace'

tracer.init({
  service: process.env.DD_SERVICE || 'api',
  env: process.env.RAILWAY_ENVIRONMENT,
  version: process.env.RAILWAY_GIT_COMMIT_SHA
})

CI/CD with Railway

GitHub Actions

.github/workflows/deploy.yml
YAML
# .github/workflows/deploy.yml
name: Deploy to Railway

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install Railway CLI
        run: npm install -g @railway/cli

      - name: Deploy
        run: railway up --service api
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

      - name: Run migrations
        run: railway run npx prisma migrate deploy
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Deploy with tests

Code
YAML
name: Test and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Railway
        run: |
          npm install -g @railway/cli
          railway up
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

Rollback

Code
Bash
# From CLI
railway rollback

# Choose a previous deployment from the list
# Or through dashboard → Deployments → Rollback

Framework integrations

Next.js

JSnext.config.js
JavaScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',  // Optimization for Railway
  experimental: {
    serverComponentsExternalPackages: ['@prisma/client']
  }
}

module.exports = nextConfig
railway.toml
TOML
# railway.toml
[build]
builder = "NIXPACKS"

[deploy]
startCommand = "node server.js"
healthcheckPath = "/api/health"

NestJS

TSmain.ts
TypeScript
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  // Health check
  app.get('/health', (req, res) => res.send('OK'))

  const port = process.env.PORT || 3000
  await app.listen(port, '0.0.0.0')

  console.log(`Application running on port ${port}`)
}

bootstrap()

FastAPI (Python)

PYmain.py
Python
# main.py
from fastapi import FastAPI
import os

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "healthy"}

@app.get("/")
def root():
    return {"message": "Hello from Railway!"}

if __name__ == "__main__":
    import uvicorn
    port = int(os.getenv("PORT", 8000))
    uvicorn.run(app, host="0.0.0.0", port=port)
railway.toml
TOML
# railway.toml
[build]
builder = "NIXPACKS"

[deploy]
startCommand = "uvicorn main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"

Go (Gin)

main.go
Go
// main.go
package main

import (
    "os"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "healthy"})
    })

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello from Railway!"})
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    r.Run(":" + port)
}

Django

PYsettings.py
Python
# settings.py
import os
import dj_database_url

DEBUG = os.getenv('DEBUG', 'False') == 'True'

ALLOWED_HOSTS = [
    '.railway.app',
    'localhost',
    '127.0.0.1'
]

# Database
DATABASES = {
    'default': dj_database_url.config(
        default=os.getenv('DATABASE_URL'),
        conn_max_age=600
    )
}

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# WhiteNoise for static files
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ...
]

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Code
TEXT
# Procfile (alternative to railway.toml)
web: gunicorn myproject.wsgi --bind 0.0.0.0:$PORT

Custom domains

Adding a domain

Code
TEXT
1. Railway Dashboard → Service → Settings → Domains
2. Click "Add Custom Domain"
3. Enter the domain: api.example.com
4. Add DNS records:

   CNAME api -> [your-service].up.railway.app

   Or for root domain:
   A record -> Railway IP

SSL/TLS

Code
TEXT
Railway automatically:
- Generates Let's Encrypt certificates
- Handles HTTPS
- Redirects HTTP → HTTPS
- Renews certificates

Wildcard domains

Code
TEXT
For *.example.com:
1. Add CNAME *.example -> [service].up.railway.app
2. In Railway add *.example.com as a custom domain
3. Railway will handle all subdomains

Persistent storage

Volumes

Code
TEXT
Railway supports persistent volumes for:
- Files uploaded by users
- Cache
- Local databases (SQLite)

Dashboard → Service → Volumes → Add Volume

Mount path: /data
Size: 1GB - 100GB
Code
TypeScript
// Using a volume
import fs from 'fs/promises'
import path from 'path'

const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'

export async function saveFile(file: Buffer, filename: string) {
  const filepath = path.join(UPLOAD_DIR, filename)
  await fs.mkdir(path.dirname(filepath), { recursive: true })
  await fs.writeFile(filepath, file)
  return filepath
}

Object storage (S3-compatible)

Code
TypeScript
// Recommendation: Use S3/R2/MinIO for files
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const s3 = new S3Client({
  region: 'auto',
  endpoint: process.env.S3_ENDPOINT,  // Cloudflare R2, MinIO, etc.
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!
  }
})

export async function uploadFile(file: Buffer, key: string) {
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: file
  }))

  return `${process.env.S3_PUBLIC_URL}/${key}`
}

Railway pricing

Plans

Code
TEXT
Trial:
├── Price: $0
├── $5 one-time credit
├── Full platform access
├── Limit: 500 execution hours/month
└── Ideal for testing

Hobby:
├── Price: $5/month
├── Includes $5 usage
├── 8GB RAM per service
├── 8 vCPU per service
├── Unlimited deployments
└── For side projects

Pro:
├── Price: $20/month per member
├── Includes $10 usage per member
├── 32GB RAM per service
├── 32 vCPU per service
├── Teams & collaboration
├── Priority support
└── For production

Enterprise:
├── Custom pricing
├── Dedicated infrastructure
├── SLA guarantees
├── SSO/SAML
└── Advanced security

Usage pricing

Code
TEXT
Compute:
├── vCPU: $0.000463/minute ($20/month for 1 vCPU 24/7)
└── Memory: $0.000231/GB/minute ($10/month for 1GB 24/7)

Network:
├── Egress: $0.10/GB (first 100GB free)
└── Ingress: Free

Databases:
├── PostgreSQL: compute + storage ($0.25/GB/month)
├── Redis: compute only
└── MongoDB: compute + storage

Cost calculation

Code
TEXT
Example: Small SaaS

API (Node.js):
├── 0.5 vCPU: ~$10/month
├── 512MB RAM: ~$5/month
└── Subtotal: ~$15/month

PostgreSQL:
├── 0.25 vCPU: ~$5/month
├── 256MB RAM: ~$2.50/month
├── 10GB storage: ~$2.50/month
└── Subtotal: ~$10/month

Redis:
├── 0.25 vCPU: ~$5/month
├── 256MB RAM: ~$2.50/month
└── Subtotal: ~$7.50/month

Total: ~$32.50/month (with $5 included in Hobby)
Actual cost: ~$27.50/month

Best practices

Security

Code
TypeScript
// 1. Use environment variables
const apiKey = process.env.API_KEY  // Never hardcoded!

// 2. Validate input
import { z } from 'zod'

const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
})

// 3. Use HTTPS only
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(`https://${req.headers.host}${req.url}`)
  }
  next()
})

// 4. Rate limiting
import rateLimit from 'express-rate-limit'

app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}))

Optimization

Code
DOCKERFILE
# 1. Multi-stage builds
FROM node:20-alpine AS builder
# ... build steps ...

FROM node:20-alpine AS runner
# ... only production dependencies ...

# 2. .dockerignore
node_modules
.git
.env*
*.md
tests/
Code
TypeScript
// 3. Connection pooling
import { PrismaClient } from '@prisma/client'

// Singleton pattern for Prisma
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query'] : []
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Graceful shutdown

Code
TypeScript
// SIGTERM handling
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully')

  // Stop accepting new requests
  server.close()

  // Close database connections
  await prisma.$disconnect()
  await redis.quit()

  console.log('Cleanup complete')
  process.exit(0)
})

FAQ - frequently asked questions

Does Railway have a free tier?

Yes, Railway offers a one-time $5 credit on the trial plan. After using it up, you can switch to the Hobby plan ($5/month) which includes $5 in usage credit. However, there is no permanently free plan like Heroku used to offer.

How does Railway compare to Heroku?

Railway is generally cheaper and more modern:

  • No "dyno sleeping" even on the cheapest plan
  • Native Docker support
  • Preview environments
  • Simpler pricing (pay-as-you-go)
  • Better developer experience

Heroku may be a better fit for enterprises with compliance requirements.

Can I deploy static sites?

Yes, but Railway is not optimized for that. For static sites, better options include:

  • Vercel
  • Netlify
  • Cloudflare Pages

Railway shines with backends and full-stack apps.

How do I debug deployment issues?

  1. Check logs: railway logs
  2. Check build logs in the dashboard
  3. Run locally with the same variables: railway run npm start
  4. Verify the Dockerfile/Nixpacks configuration
  5. Check the health check endpoint

Does Railway support WebSockets?

Yes, Railway fully supports WebSockets without any additional configuration. Keep in mind sticky sessions when scaling horizontally.

How do I back up the database?

Code
Bash
# PostgreSQL dump
railway run pg_dump $DATABASE_URL > backup.sql

# Or through the pg_dump plugin in the dashboard
# Settings → Backups → Create Backup

Summary

Railway is a modern deployment platform that combines the simplicity of Heroku with flexibility and affordable pricing:

  • Quick start - Deploy in a minute from GitHub or CLI
  • One-click databases - PostgreSQL, MySQL, MongoDB, Redis
  • Docker native - Full container support
  • Preview environments - Automatic environments for PRs
  • Scaling - Horizontal and vertical scaling
  • Simple pricing - Pay-as-you-go with no surprises

Railway is ideal for startups, side projects, and teams that want to focus on code, not infrastructure.