Heroku 이후의 PaaS: Railway와 Fly.io로 Dockerfile 배포, DB 추가, 멀티 리전 비교

이 글의 핵심

Heroku 무료 플랜이 사라진 뒤 백엔드를 올릴 곳으로 많이 거론되는 두 서비스는 방향이 꽤 다릅니다. Railway는 설정 없이 푸시만으로 배포되는 단순함에, Fly.io는 microVM을 여러 리전에 띄우는 엣지 배포에 무게를 둡니다. 포트와 헬스체크, 모노레포 설정처럼 처음 막히는 지점을 짚고 어느 쪽이 내 서비스에 맞는지 판단할 기준을 제시합니다.

PaaS의 부활: Heroku 몰락과 Railway·Fly.io 부상

Heroku 시대의 종말 (2022)

Heroku(2007)는 PaaS(Platform as a Service)의 선구자였습니다. git push heroku main 한 줄로 배포가 끝나는 경험은 혁명적이었습니다. 하지만 2022년 8월, Salesforce는 무료 플랜 종료를 발표했습니다.

무료 플랜 종료의 충격:

  • 수백만 개의 취미·교육 프로젝트가 shutdown
  • 유료 전환 ($7/월~)은 개인 개발자에게 부담
  • AWS Elastic Beanstalk·Google App Engine은 복잡함

이 공백을 Railway(2020)와 Fly.io(2017)가 메웠습니다.

Railway vs Fly.io vs Heroku: 철학 비교

측면RailwayFly.ioHeroku
철학”Heroku 단순함 계승""Edge 네이티브 PaaS""원조 PaaS”
배포GitHub 자동 배포fly deploy CLIGit push
실행 환경DockerFirecracker microVMDyno (Container)
네트워크단일 리전Global Anycast리전 선택
DB 복제❌ (단일 인스턴스)✅ (멀티 리전)✅ (Postgres 유료)
무료 티어$5 크레딧/월3 VM + 3GB DB❌ (종료)
가격$0.000463/GB-sec$0.0000008/초 + $0.02/GB$7/dyno

Railway의 강점: “설정 제로, Heroku의 재림”

Railway는 Heroku의 단순함을 현대적으로 재현했습니다:

  • Nixpacks (Heroku Buildpack 오픈소스 대체): Node.js·Python·Go 자동 인식
  • GitHub 연동: Push하면 자동 배포
  • 환경변수 GUI: 클릭으로 설정
  • DB 템플릿: Postgres·Redis·MongoDB 원클릭 추가

Fly.io의 강점: “Edge 네이티브, 글로벌 배포”

Fly.io는 Firecracker microVM + Anycast 네트워크로 차별화:

  • Firecracker: AWS Lambda가 쓰는 경량 VM (5ms 시작)
  • Global Anycast: 사용자가 가장 가까운 리전으로 자동 라우팅
  • 멀티 리전 DB: Postgres primary + read replica를 여러 리전에 자동 배포
  • WireGuard VPN: 리전 간 private 네트워크

Fly.io 네트워크 아키텍처:

사용자(서울) → Fly Anycast IP → 서울 리전 VM → DB Primary(도쿄)
사용자(뉴욕) → Fly Anycast IP → 뉴욕 리전 VM → DB Replica(뉴욕)

Railway 시작

npm install -g @railway/cli
railway login
railway init
railway up

또는 Railway Dashboard에서 GitHub repo 연결 → 자동 배포.

Railway 배포 (GitHub)

  1. railway.app → New Project → Deploy from GitHub
  2. Repo 선택 → Railway 빌더(Railpack, 구 서비스는 Nixpacks)가 언어·프레임워크 자동 인식
  3. 환경변수 설정 (Dashboard)
  4. Git push → 자동 배포

Railway 환경변수

# CLI
railway variables set DATABASE_URL="postgres://..."
railway variables

# Dashboard
Project → Variables → Add Variable

환경변수는 빌드 시 + 런타임 모두 주입.

Railway DB 추가

railway add --database postgres

Dashboard에서도 New → Database → Postgres 클릭. 즉시 프로비저닝 + DATABASE_URL 환경변수 생성.

Railway Dockerfile

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

루트에 Dockerfile이 있으면 자동 감지 빌더 대신 Dockerfile로 빌드합니다.

Railway 커스텀 도메인

Dashboard → Settings → Domains → Add Custom Domain → CNAME 설정.

Railway에서 자주 막히는 지점: 포트, 헬스체크, 모노레포, 백업

Railway의 “설정 제로”는 앱이 몇 가지 관례를 지킨다는 전제 위에 있습니다. 첫 배포에서 가장 흔한 실패는 빌드가 아니라 시작 단계입니다. Railway는 PORT 환경변수를 주입하고 그 포트로 트래픽을 보내는데, 로컬 습관대로 app.listen(3000)처럼 하드코딩하면 빌드는 초록색인데 도메인에 접속하면 502가 나옵니다. process.env.PORT를 읽고, 컨테이너 안에서는 localhost가 아니라 0.0.0.0(또는 ::)에 바인딩해야 합니다.

헬스체크는 선택이지만 켜두는 편이 낫습니다. 설정하면 Railway가 새 배포의 헬스체크 엔드포인트가 200을 돌려줄 때까지 기존 배포로 트래픽을 유지하므로, 마이그레이션 실패나 환경변수 누락으로 뜨자마자 죽는 버전이 사용자에게 노출되지 않습니다. 다만 헬스체크는 배포 시점에만 확인되고 이후 상시 모니터링은 아니라서, 런타임 장애 감시는 별도 도구가 필요합니다.

# railway.toml
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 100
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10

모노레포는 서비스마다 Root Directory를 지정하거나, 루트에서 워크스페이스 명령으로 빌드합니다. 주의할 점은 기본 설정에서 아무 파일이 바뀌어도 모든 서비스가 재배포된다는 것입니다. 서비스별 Watch Paths(예: /apps/api/**, /packages/shared/**)를 지정해 두지 않으면 프론트엔드 문구 하나 고쳤는데 API까지 재시작되는 일이 생깁니다.

[build]
buildCommand = "npm run build --workspace=api"
watchPatterns = ["apps/api/**", "packages/shared/**"]

[deploy]
startCommand = "npm run start --workspace=api"

DB 백업은 플랜과 볼륨 설정에 따라 제공되는 자동 백업과 별개로, 이관·장애 대비용 논리 덤프를 직접 떠두는 습관이 필요합니다. railway connect postgres는 psql 셸을 열어줄 뿐 로컬 포트를 포워딩하지 않으므로, 덤프는 Variables의 DATABASE_PUBLIC_URL(외부 접속용 프록시 주소)을 대상으로 실행합니다. 로컬 pg_dump 메이저 버전이 서버보다 낮으면 “server version mismatch”로 실패하니 버전부터 맞추세요.

pg_dump "$DATABASE_PUBLIC_URL" -Fc -f backup.dump
pg_restore -d "$NEW_DATABASE_URL" --no-owner backup.dump

Fly.io 시작

# macOS/Linux
curl -L https://fly.io/install.sh | sh

# Windows
powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"

fly auth login
fly launch    # 프로젝트 초기화 + fly.toml 생성
fly deploy

fly.toml

app = "my-app"
primary_region = "nrt"  # Tokyo

[build]

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 1

[[vm]]
  size = "shared-cpu-1x"
  memory = "256mb"
  • primary_region: 주 리전 (nrt=도쿄, icn=서울, sjc=실리콘밸리 등)
  • auto_stop_machines: 트래픽 없으면 자동 중단 → 비용 절약

Fly.io 환경변수

fly secrets set DATABASE_URL="postgres://..."
fly secrets list

fly deploy 시 자동 주입.

Fly Postgres

fly postgres create --name my-db --region nrt
fly postgres attach my-db

DATABASE_URL 자동 생성. 추가 replica:

fly machine clone --region sjc --app my-db

Read replica를 여러 리전에 자동 배치.

Fly 멀티 리전 배포

fly scale count 3 --region nrt,sjc,ams

3개 리전에 VM 분산 배치. Anycast IP로 유저가 가장 가까운 리전에 자동 라우팅.

Fly Dockerfile

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

fly deploy가 Dockerfile 자동 인식 + buildkit으로 빌드.

Fly 커스텀 도메인

fly certs create example.com
fly certs create www.example.com

DNS에 A/AAAA 레코드 추가 → 자동 SSL 발급.

Railway vs Fly.io 요약

항목RailwayFly.io
배포 방식GitHub push 자동fly deploy
BuildpackNixpacks (Heroku 호환)fly launch가 Dockerfile 생성
DBPostgres·Redis·MySQL 원클릭fly postgres create
멀티 리전단일 리전Anycast + multi-region VM
가격 (무료)$5 크레딧3 VM + 3GB Postgres
스케일링수동 (Dashboard)auto-scale + auto-stop
네트워크표준 HTTP/HTTPS6PN private network
로그Dashboard 실시간fly logs
모니터링기본 메트릭Prometheus·Grafana 연동

Railway 실전 예제

# Node.js 앱
git clone https://github.com/my-org/api
cd api
railway init
railway add --plugin postgres
railway variables set NODE_ENV=production
railway up

# 도메인 연결
# Dashboard → Domains → api.example.com

Fly.io 실전 예제

# Go 앱
fly launch --name my-go-api --region nrt
fly secrets set API_KEY=xyz123
fly postgres create --name my-pg --region nrt
fly postgres attach my-pg
fly deploy
fly scale count 2 --region nrt,sjc
fly status

환경변수 비교

  • Railway: Dashboard GUI + railway variables
  • Fly: fly secrets (암호화 저장)

둘 다 .env 파일 업로드 불가 → CLI나 Dashboard로만 설정.

로그 & 모니터링

# Railway
railway logs

# Fly.io
fly logs
fly status
fly dashboard       # Grafana 링크

Fly는 Prometheus endpoint 제공 → 자체 모니터링 스택 연동 가능.

CI/CD 통합

Railway: GitHub Actions

- name: Deploy to Railway
  uses: bervProject/railway-deploy@main
  with:
    railway_token: ${{ secrets.RAILWAY_TOKEN }}
    service: api

Fly.io: GitHub Actions

- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
  env:
    FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

비용 예측 (2026년 기준)

Railway

  • Compute: $0.000463/GB-hour
  • 1GB RAM 24/7 = ~$10/월
  • DB: 별도 (같은 요금)

Fly.io

  • shared-cpu-1x (256MB): $1.94/월
  • dedicated-cpu-1x (2GB): $62/월
  • Postgres 1GB: $0.02/GB-hour = ~$15/월

무료 티어로 취미 프로젝트는 충분, 프로덕션은 $20-50/월 예상.

트러블슈팅

Railway 배포 실패

  • 빌드 로그 확인 → 누락된 build 스크립트, 로컬에선 통과하던 TypeScript 에러(대개 lockfile/Node 버전 차이)
  • railway logs 실시간 확인
  • Dockerfile로 전환

Fly 멀티 리전 DB 복제 안 됨

  • fly postgres attach가 primary만 연결 → replica는 수동 DNS 설정
  • fly postgres connect + pgbouncer 활용

Railway 메모리 OOM

  • Dashboard → Settings → RAM 증설
  • 가격 재계산

Fly 6PN private 네트워크 안 보임

  • fly deploy 후 fly wireguard create + VPN 연결
  • 또는 fly proxy 터널

체크리스트

  • Railway 또는 Fly CLI 설치
  • GitHub repo 또는 Dockerfile 준비
  • 환경변수 설정 (DATABASE_URL 등)
  • DB 프로비저닝 (Postgres/Redis)
  • 커스텀 도메인 + SSL
  • 로그·메트릭 확인
  • 멀티 리전 or 오토스케일 설정 (Fly)
  • 비용 알림 설정

마무리

Railway와 Fly.io는 Heroku가 남긴 “push to deploy” 경험을 현대적으로 계승한 PaaS입니다. Railway는 단순함·Buildpack 자동 인식·GitHub 통합으로 “Heroku의 직계”, Fly.io는 Firecracker microVM·Anycast·멀티 리전 복제로 “글로벌 edge PaaS”입니다. Vercel/Netlify는 정적·서버리스에 강하지만, Railway/Fly는 정말 백엔드 서버(DB·WebSocket·gRPC·장기 실행)를 컨테이너로 배포할 때 최선입니다. 2026년 현재 OSS·스타트업·인디 해커들이 가장 많이 쓰는 백엔드 배포 플랫폼이며, 무료 티어로 충분히 체험 가능하니 지금 바로 첫 배포를 시도해보세요.

자주 묻는 질문 (FAQ)

Q. Railway에서 빌드는 성공했는데 도메인에 접속하면 502가 나오는 이유는 무엇인가요?

A. 가장 흔한 원인은 앱이 Railway가 주입하는 PORT 환경변수가 아니라 app.listen(3000)처럼 하드코딩된 포트로 뜨는 경우입니다. process.env.PORT를 읽고, 컨테이너 안에서는 localhost가 아니라 0.0.0.0(또는 ::)에 바인딩해야 합니다. healthcheckPath를 설정해 두면 새 배포가 200을 돌려줄 때까지 기존 배포로 트래픽이 유지되어 이런 실패가 사용자에게 바로 노출되지 않습니다.


같이 보면 좋은 글