도커 실전 강좌 #6 클라우드 배포 — Fly.io / Railway / ECS — 트랙 마무리

8 분 소요

도커 트랙의 마지막 글입니다. #1~3에서 컨테이너 이미지를 만들고, #4~5에서 CI로 빌드해 푸시했습니다. 이번 글은 그 이미지를 실제 운영 인프라에 올리는 단계입니다.

도커 실전 강좌에서 이번 글의 위치:

세 가지 옵션을 비교한 다음, Fly.io와 Railway의 흐름을 짧게 짚고, ECS는 AWS 실전 트랙으로 연결합니다. 마지막에 도커 24편을 회수하며 마무리합니다.

셋의 갈림길 #

먼저 한 표로.

Fly.ioRailwayAWS ECS Fargate
위치edge (전 세계 30+ 리전)US/EU 리전AWS 전 리전
인프라 모델Firecracker VM (개당)컨테이너 (Nomad)컨테이너 (관리형)
디플로이 단위App + MachineServiceTask + 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 설치 + 로그인.

Fly CLI
brew install flyctl
fly auth login

2. fly launch로 시작.

fly launch는 디렉터리를 보고 자동으로 적절한 fly.toml을 만들어 줍니다. Dockerfile이 있으면 그걸 우선 사용합니다.

앱 생성
cd my-fastapi-app
fly launch
# - 앱 이름 선택
# - 리전 선택 (가장 가까운 곳 추천됨)
# - Postgres 같이 만들지 묻기 → Yes 면 같이 만들고 DATABASE_URL 자동 주입

생성되는 fly.toml:

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 = 512

auto_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 list

secrets set은 자동으로 앱을 재배포해서 새 환경변수를 적용합니다. 시크릿은 빌드 시점이 아니라 런타임에만 들어갑니다 — 이미지에는 안 박힘 (#5의 주제).

4. 배포.

배포
fly deploy
# 또는 이미 푸시된 이미지를 쓸 때
fly deploy --image ghcr.io/me/app:sha-a1b2c3d

Fly가 이미지를 받아 새 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에서 자동 배포는 단순:

.github/workflows/deploy-fly.yml
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 설치 + 로그인.

Railway CLI
brew install railwayapp/railway/railway
railway login

2. 프로젝트 생성과 연결.

웹 콘솔에서 프로젝트를 만들면 GitHub 저장소 연결을 묻습니다. 연결하면 main 푸시마다 자동 빌드/배포. 도커파일이 있으면 그걸 우선 사용 (없으면 Nixpacks 자동 감지).

CLI만으로 한다면:

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 설정.

railway.json
{
  "$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 — 위 자원들을 담는 그릇.

배포 흐름 한 줄 요약:

  1. ECR에 이미지 푸시 (CI가 aws ecr get-login-passworddocker push).
  2. Task Definition의 image 필드를 새 SHA 태그로 갱신 (revision +1).
  3. Service가 새 revision으로 rolling 디플로이 — 새 Task 떠서 healthcheck 통과 후 옛 Task 종료.

단순한 워크플로우 예:

.github/workflows/deploy-ecs.yml — 골자
- 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 디플로이의 원리는 같습니다.

rolling 디플로이의 흐름
초기:        [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.iofly secrets set (envrypted at rest, 컨테이너에 환경변수로 주입)
Railway콘솔의 Variables, 또는 railway variables set
ECSSecrets Manager / Parameter Store + Task Definition의 secrets 섹션
KubernetesSecret 리소스 또는 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 — 의 마지막 화에서 도커가 항상 등장했습니다. 이제 그 도커가 이쪽 트랙에서 처음부터 끝까지 다뤄졌으니, 모든 트랙의 배포 문제를 같은 도구로 풀 수 있게 됐습니다.

X