도커 실전 강좌 #6 클라우드 배포 — Fly.io / Railway / ECS — 트랙 마무리
도커 트랙의 마지막 글입니다. #1~3에서 컨테이너 이미지를 만들고, #4~5에서 CI로 빌드해 푸시했습니다. 이번 글은 그 이미지를 실제 운영 인프라에 올리는 단계입니다.
도커 실전 강좌에서 이번 글의 위치:
- #1 FastAPI 컨테이너화
- #2 Django + PostgreSQL compose
- #3 React/Next.js 빌드 컨테이너
- #4 CI에서 이미지 빌드
- #5 레지스트리 푸시와 태그 전략
- #6 클라우드 배포 — Fly.io / Railway / ECS — 트랙 마무리 ← 이번 글
세 가지 옵션을 비교한 다음, Fly.io와 Railway의 흐름을 짧게 짚고, ECS는 AWS 실전 트랙으로 연결합니다. 마지막에 도커 24편을 회수하며 마무리합니다.
셋의 갈림길 #
먼저 한 표로.
| Fly.io | Railway | AWS ECS Fargate | |
|---|---|---|---|
| 위치 | edge (전 세계 30+ 리전) | US/EU 리전 | AWS 전 리전 |
| 인프라 모델 | Firecracker VM (개당) | 컨테이너 (Nomad) | 컨테이너 (관리형) |
| 디플로이 단위 | App + Machine | Service | Task + Service |
| 다중 리전 | 기본 지원 (anycast) | 일부 | 별도 설계 필요 |
| DB / Redis | 내장 (Postgres, Upstash) | 내장 (Postgres, Redis) | RDS / ElastiCache |
| 가격 모델 | 사용량 기반 (분당) | 사용량 기반 (월별) | 시간/메모리 기반 |
| 학습 곡선 | 낮음 | 가장 낮음 | 높음 |
| 락인 | 낮음 | 낮음 (도커 이미지) | 높음 (AWS 생태계) |
선택 기준 한 줄:
- 빠르게 띄우고 빠르게 옮길 수 있어야 한다 → Railway 또는 Fly.io. 도커 이미지만 있으면 옮기기 쉬움.
- 이미 AWS 위에서 운영 중 → ECS. 다른 AWS 서비스(RDS, S3, IAM)와 묶기 자연스러움.
- 글로벌 사용자 / 저지연 → Fly.io. edge가 기본.
이번 글은 처음 두 옵션을 깊이 다루고, ECS는 AWS 실전 #1 ECS 배포로 연결.
Fly.io — fly launch 흐름
#
Fly.io는 도커 이미지를 받아 Firecracker VM(=Machine)에 띄우는 모델입니다. 한 앱 안에 여러 Machine이 있고, 각 Machine이 한 컨테이너를 굽는 구조.
1. CLI 설치 + 로그인.
brew install flyctl
fly auth login2. fly launch로 시작.
fly launch는 디렉터리를 보고 자동으로 적절한 fly.toml을 만들어 줍니다. Dockerfile이 있으면 그걸 우선 사용합니다.
cd my-fastapi-app
fly launch
# - 앱 이름 선택
# - 리전 선택 (가장 가까운 곳 추천됨)
# - Postgres 같이 만들지 묻기 → Yes 면 같이 만들고 DATABASE_URL 자동 주입생성되는 fly.toml:
app = "my-fastapi-app"
primary_region = "nrt" # 도쿄
[build]
# Dockerfile 사용 (자동 감지)
[env]
PYTHONUNBUFFERED = "1"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop" # 트래픽 없을 때 자동 stop (비용 절감)
auto_start_machines = true # 첫 요청에 자동 start
min_machines_running = 0
[[http_service.checks]]
interval = "30s"
timeout = "5s"
grace_period = "10s"
method = "GET"
path = "/healthz"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 512auto_stop_machines = "stop"이 흥미로운 옵션입니다. 트래픽 없으면 자동으로 컨테이너 정지, 첫 요청 들어오면 자동 시작. cold start가 0.5~2초 정도 — 사이드 프로젝트나 trafficchurn 큰 환경에 유용합니다.
3. 시크릿 주입.
DATABASE_URL 같은 시크릿은 fly secrets로 주입. fly.toml에는 절대 박지 마세요.
fly secrets set DJANGO_SECRET_KEY=$(openssl rand -hex 32)
fly secrets set DATABASE_URL="postgres://..."
fly secrets listsecrets set은 자동으로 앱을 재배포해서 새 환경변수를 적용합니다. 시크릿은 빌드 시점이 아니라 런타임에만 들어갑니다 — 이미지에는 안 박힘 (#5의 주제).
4. 배포.
fly deploy
# 또는 이미 푸시된 이미지를 쓸 때
fly deploy --image ghcr.io/me/app:sha-a1b2c3dFly가 이미지를 받아 새 Machine을 만들고, 헬스체크 통과를 기다린 뒤 트래픽을 옮깁니다. rolling 전략이 기본 — zero-downtime.
5. 로그 / 상태 / 셸.
fly status
fly logs
fly ssh console # 컨테이너에 셸로 진입
fly machine restart
fly scale count 3 # Machine 3개로 수평 확장
fly scale memory 1024 # 메모리 변경CI에서 자동 배포는 단순:
name: Deploy to Fly.io
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}--remote-only는 Fly의 빌더에서 빌드 — GHA 러너에서 빌드하지 않아 워크플로우 시간이 짧아집니다. 단, Fly 빌더 사용량 한도가 있습니다. 더 큰 캐시가 필요하면 #4처럼 GHA에서 빌드 → 레지스트리 푸시 → flyctl deploy --image ... 흐름을 쓰세요.
Railway — railway up
#
Railway는 가장 빠른 시작이 강점. UI가 깔끔하고, 도커 + 환경변수 + Postgres가 같은 화면에서 한 번에 묶입니다.
1. CLI 설치 + 로그인.
brew install railwayapp/railway/railway
railway login2. 프로젝트 생성과 연결.
웹 콘솔에서 프로젝트를 만들면 GitHub 저장소 연결을 묻습니다. 연결하면 main 푸시마다 자동 빌드/배포. 도커파일이 있으면 그걸 우선 사용 (없으면 Nixpacks 자동 감지).
CLI만으로 한다면:
cd my-app
railway init # 새 프로젝트
railway up # 현재 디렉터리를 빌드해 배포3. 시크릿과 서비스 연결.
콘솔에서 Postgres 서비스를 추가하면 자동으로 DATABASE_URL 같은 환경변수를 다른 서비스에 주입합니다. CLI로도 가능.
railway variables set DJANGO_SECRET_KEY=$(openssl rand -hex 32)
railway variables # 현재 변수 확인
railway run -- python manage.py migrate # 환경변수 주입한 채 명령 실행4. healthcheck와 zero-downtime.
railway.json (또는 railway.toml)에서 healthcheck 설정.
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"healthcheckPath": "/healthz",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"numReplicas": 2
}
}Railway도 healthcheck 통과 후 트래픽을 옮기는 rolling 디플로이가 기본입니다. numReplicas: 2 면 zero-downtime.
Fly vs Railway 짧게.
Railway는 정말로 “도커 이미지를 그냥 실행한다” 에 가깝습니다. 다중 리전, edge, anycast 같은 영역은 Fly가 강합니다. 단순한 풀스택 앱이라면 Railway가 가장 빨리 띄우는 옵션.
ECS Fargate — 짧게 #
ECS는 AWS 트랙에서 깊이 다뤘기 때문에 여기서는 결만 짚습니다.
핵심 개념:
- Task Definition — 어떤 이미지를 어떤 자원으로 어떻게 띄울지 정의 (JSON).
- Task — Task Definition에서 만들어진 한 인스턴스. (= 컨테이너 한 묶음)
- Service — Task를 N 개 유지하는 매니저. ALB와 묶여 트래픽을 분배.
- Cluster — 위 자원들을 담는 그릇.
배포 흐름 한 줄 요약:
- ECR에 이미지 푸시 (CI가
aws ecr get-login-password후docker push). - Task Definition의
image필드를 새 SHA 태그로 갱신 (revision +1). - Service가 새 revision으로 rolling 디플로이 — 새 Task 떠서 healthcheck 통과 후 옛 Task 종료.
단순한 워크플로우 예:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/github-actions
aws-region: ap-northeast-2
- uses: aws-actions/amazon-ecr-login@v2
id: login-ecr
- name: Build, tag, push
run: |
docker build -t $ECR_REGISTRY/$REPO:sha-${{ github.sha }} .
docker push $ECR_REGISTRY/$REPO:sha-${{ github.sha }}
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- name: Update task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-def.json
container-name: web
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO }}:sha-${{ github.sha }}
- name: Deploy
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: my-service
cluster: my-cluster
wait-for-service-stability: true자세한 IAM 셋업, ALB / Target Group / 헬스체크, RDS 연동, 비용 최적화는 AWS 실전 #1 부터 6 편에서 다룹니다.
Zero-downtime의 공통 원리 #
플랫폼이 다르더라도 zero-downtime 디플로이의 원리는 같습니다.
초기: [v1] [v1] [v1] ← LB
v2 push → [v1] [v1] [v1]
[v2] ← 새 인스턴스 띄움
┌── healthcheck ──┐
│ /healthz 응답? │
└─────────────────┘
[v1] [v1] [v2] ← LB 등록, 옛 인스턴스 한 개 제거
[v1] [v2] [v2]
[v2] [v2] [v2] ← 완료각 단계에서 필요한 요소:
/healthz엔드포인트 — 앱이 실제로 받을 수 있는 상태인지 확인. DB 연결 가능한지까지 봐야 정확. 너무 빨리 200을 주면 워밍업 전에 트래픽이 들어와서 첫 요청이 깨집니다.- graceful shutdown — 옛 인스턴스를 죽일 때 SIGTERM을 받고, in-flight 요청을 끝낸 뒤 종료. PID 1이 신호를 정확히 받는 게 전제 (#2의
exec "$@", 고급 #6의 주제). - 충분한 인스턴스 수 — 1 개로는 zero-downtime이 안 됨. 적어도 2 개에서 rolling.
- DB 마이그레이션 호환성 — 새 코드가 도는 동안 옛 코드도 같이 돕니다. 마이그레이션은 항상 backward-compatible 하게 (예: 컬럼 추가는 nullable, 컬럼 삭제는 두 단계로).
시크릿 관리 — 플랫폼별 #
런타임 시크릿(DATABASE_URL, API 키)은 절대 이미지에 박지 말고 플랫폼의 시크릿 관리에 둡니다.
| 플랫폼 | 시크릿 위치 |
|---|---|
| Fly.io | fly secrets set (envrypted at rest, 컨테이너에 환경변수로 주입) |
| Railway | 콘솔의 Variables, 또는 railway variables set |
| ECS | Secrets Manager / Parameter Store + Task Definition의 secrets 섹션 |
| Kubernetes | Secret 리소스 또는 ExternalSecrets / SOPS |
공통 원칙:
- 빌드 시점에 박지 말 것 (
--build-arg가 아니라 런타임 환경변수). - 저장소(
.env.production)에도 절대 커밋 금지. - CI가 직접 시크릿을 다룰 일이 있다면 GHA의
secrets.*또는 OIDC로 클라우드 자격 일시 발급.
백엔드 + 프런트 + DB의 일반적 배치 #
도커 트랙의 산출물을 한 장의 그림으로:
사용자
│
▼
┌───────────────┐
│ CDN/LB │ (Cloudflare / ALB / Fly anycast)
└───┬───────┬───┘
│ │
┌────▼─┐ ┌──▼────────┐
│ Web │ │ API │ (Next.js / FastAPI , Django)
│ (#3) │ │ (#1, #2) │ ← 컨테이너로 배포
└──────┘ └─────┬─────┘
│
┌──────▼───┐
│ DB │ (RDS / Fly Postgres / Railway PG)
│ Postgres │
└──────────┘각 단계에서 도커 트랙이 다룬 것:
- Web 컨테이너 — #3의 standalone / 정적 export.
- API 컨테이너 — #1의 uv,멀티스테이지,non-root.
- DB — 프로덕션은 매니지드 서비스가 정석 (RDS / Fly Postgres / Railway PG). #2의 compose 패턴은 로컬,개발용.
- CI 빌드/푸시 — #4, #5.
- 배포 — 이번 글.
도커 트랙 24편의 회수 #
기초 6편에서 컨테이너의 기본 개념부터 잡고, 중급 6편에서 멀티스테이지/compose/환경변수, 고급 6편에서 BuildKit/보안/리소스/PID 1, 그리고 실전 6편에서 FastAPI / Django / Next.js / CI / 태그 / 배포까지 — 한 사이클이 닫혔습니다.
기초 #1의 첫 문장을 다시 떠올려 보면, “내 컴퓨터에서는 되는데” 를 풀려고 컨테이너가 등장했다고 했습니다. 24편 끝에서 같은 문장으로 답할 수 있습니다 — 이미지 한 개가 어디서든 같은 동작을 하고, CI가 빌드해 푸시하고, 클라우드가 그걸 받아 실행합니다. 그 안의 잔주름들 (PID 1, healthcheck, 멀티 아키, 시크릿, 태그)이 운영의 실제 쟁점입니다.
여기서 더 갈 수 있는 방향:
- Kubernetes — 한 컨테이너가 아니라 수십 개 서비스 를 운영하는 단계. ECS/Fly/Railway가 가려둔 추상이 K8s에서는 그대로 드러남. 큰 조직 / 멀티 팀 / 셀프 호스팅이 트리거.
- Service Mesh (Istio, Linkerd) — 컨테이너 간 통신에 mTLS,관측,정책을 얹는 계층.
- Container Native CI/CD — Tekton, ArgoCD 같은 GitOps 흐름. 별도 트랙에서 다룹니다.
이 트랙은 1인 ~ 작은 팀의 운영을 다루는 범위에서 끝나고, 위 항목들은 별도 트랙으로 자라납니다.
정리 #
- 클라우드 배포의 첫 갈림길은 Fly.io vs Railway vs ECS. 빠르게 띄우려면 Railway, edge 면 Fly, AWS 위면 ECS.
- 어디든 공통: 이미지를 받아 → 헬스체크 통과까지 새 인스턴스 굽고 → LB가 트래픽 옮긴 뒤 → 옛 인스턴스 종료. rolling 디플로이.
- zero-downtime의 4 요소:
/healthz엔드포인트 / graceful shutdown / 인스턴스 ≥ 2 / backward-compatible 마이그레이션. - 시크릿은 런타임에만. 플랫폼의 시크릿 매니저에. 이미지에는 절대 박지 말 것.
- 운영 매니페스트의
image:는 SHA 태그 (#5). 롤백이 한 줄 변경. - 도커 트랙 24편이 닫히는 지점. 더 큰 조직 / 셀프호스팅으로 가면 Kubernetes가 다음.
도커 트랙은 여기서 마무리합니다. 다른 트랙들 — 모던 파이썬 / Django / Go / TypeScript / React / Angular / AWS — 의 마지막 화에서 도커가 항상 등장했습니다. 이제 그 도커가 이쪽 트랙에서 처음부터 끝까지 다뤄졌으니, 모든 트랙의 배포 문제를 같은 도구로 풀 수 있게 됐습니다.