Cloudflare Containers로 엣지에서 컨테이너 실행하기

Cloudflare Containers로 엣지에서 컨테이너 실행하기

Cloudflare Workers를 써보신 분이라면 한 번쯤 이런 생각을 해보셨을 겁니다. “Workers는 정말 편한데, 내가 쓰는 Python 라이브러리나 FFmpeg 같은 도구는 못 쓰잖아…” 🤔

Workers는 V8 엔진 위에서 돌아가기 때문에 JavaScript와 WebAssembly만 실행할 수 있습니다. Go로 만든 CLI 도구를 돌리거나 Python 머신러닝 모델을 서빙하거나 영상 트랜스코딩을 하려면 결국 별도의 서버나 컨테이너 서비스가 필요했어요.

Cloudflare Containers는 바로 이 틈을 메워줍니다. Docker 컨테이너를 Cloudflare 엣지 네트워크에서 직접 실행할 수 있게 해주는 서비스예요. Workers가 가벼운 라우팅과 API 로직을 담당하고, 무거운 작업은 컨테이너에 넘기는 구조입니다.

이번 글에서는 Cloudflare Containers의 핵심 개념부터 실제 프로젝트 설정과 생명주기 관리까지 살펴보겠습니다.

Cloudflare Containers란?

Cloudflare Containers는 Cloudflare의 글로벌 네트워크에서 Linux 컨테이너를 실행할 수 있는 플랫폼입니다. 2025년 6월에 퍼블릭 베타로 공개되었어요.

기존 Workers와 가장 큰 차이는 실행 환경이에요. Workers는 V8 자바스크립트 엔진의 격리된 환경에서 실행되기 때문에 가볍고 빠르지만 사용할 수 있는 언어와 리소스에 제약이 있습니다. 반면 Containers는 완전한 Linux 환경을 제공하기 때문에 어떤 언어든 어떤 런타임이든 쓸 수 있어요.

WorkersContainers
실행 환경V8 격리 (JS/WASM)Linux 컨테이너 (모든 언어)
시작 시간1ms 미만수 초
리소스경량 (제한적 CPU/메모리)최대 4 vCPU, 12GiB 메모리
과금CPU 시간 기준활성 시간 10ms 단위
용도API 라우팅, 경량 로직무거운 연산, 풀 런타임

둘은 경쟁 관계가 아니라 함께 쓰도록 설계되었습니다. Worker가 요청을 받아서 인증이나 캐싱 같은 가벼운 작업을 처리한 뒤 무거운 연산이 필요하면 컨테이너에 넘기는 식이에요.

어떻게 동작하나?

Cloudflare Containers 아키텍처에서 핵심은 Durable Objects예요. 각 컨테이너 인스턴스에 Durable Object가 하나씩 붙어서 “프로그래머블 사이드카” 역할을 합니다.

요청이 들어오면 이런 흐름으로 처리돼요.

  1. 클라이언트 요청이 Worker에 도착합니다
  2. Worker가 바인딩을 통해 컨테이너 인스턴스를 가져옵니다
  3. Worker가 요청을 컨테이너로 전달합니다
  4. 컨테이너가 작업을 처리하고 응답을 반환합니다

코드로 보면 이렇습니다.

src/index.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    // 경로를 기반으로 컨테이너 인스턴스를 가져옴
    const id = env.MY_CONTAINER.idFromName(url.pathname);
    const container = env.MY_CONTAINER.get(id);
    // 요청을 컨테이너로 전달
    return container.fetch(request);
  },
};

여기서 흥미로운 점은 “Region: Earth”라는 개념인데요. 컨테이너가 특정 리전에 고정되는 게 아니라 요청이 들어온 위치에서 가장 가까운 Cloudflare 데이터 센터에서 부팅됩니다. 별도로 리전을 설정하지 않아도 전 세계 어디서든 낮은 지연시간을 확보할 수 있어요.

프로젝트 시작하기

Cloudflare Containers를 사용하려면 Workers Paid 플랜($5/월)이 필요합니다. 무료 플랜에서는 아쉽게도 컨테이너를 사용할 수 없어요.

먼저 공식 템플릿으로 프로젝트를 생성합니다.

bunx create-cloudflare@latest -- --template=cloudflare/templates/containers-template

npm을 쓰신다면 이렇게 하면 됩니다.

npx create-cloudflare@latest -- --template=cloudflare/templates/containers-template

프로젝트가 생성되면 주요 파일 구조는 이렇습니다.

프로젝트 구조
my-container-app/
├── Dockerfile          # 컨테이너 이미지 정의
├── wrangler.jsonc      # Cloudflare 설정
├── src/
│   └── index.js        # Worker + Container 클래스
└── package.json

설정 파일

wrangler.jsonc에서 컨테이너 관련 설정을 합니다. Wrangler를 이미 써보신 분이라면 익숙한 형식일 거예요.

wrangler.jsonc
{
  "name": "my-container-app",
  "main": "src/index.js",
  "containers": [
    {
      "class_name": "MyContainer",
      "image": "./Dockerfile",
      "max_instances": 80,
      "instance_type": "basic",
    },
  ],
}

class_name은 Worker 코드에서 정의할 Container 클래스 이름이고 image는 Dockerfile 경로입니다. max_instances로 동시에 실행할 수 있는 최대 인스턴스 수를 제한할 수 있어요.

instance_type은 컨테이너에 할당할 리소스를 결정해요.

  • dev — 256MiB 메모리, 1/16 vCPU, 2GB 디스크. 개발과 테스트용
  • basic — 1GiB 메모리, 1/4 vCPU, 4GB 디스크. 가벼운 워크로드에 적합
  • standard — 4GiB 메모리, 1/2 vCPU, 4GB 디스크. 일반적인 서비스용
  • standard-4 — 12GiB 메모리, 4 vCPU, 20GB 디스크. 고성능 연산이 필요할 때

Container 클래스 작성

Worker 코드 안에서 Container 클래스를 상속받아 동작을 정의합니다.

src/index.js
import { Container } from "cloudflare:workers";

export class MyContainer extends Container {
  // 컨테이너가 리스닝할 포트
  defaultPort = 8080;

  // 유휴 상태 5분 후 자동 수면
  sleepAfter = "5m";

  // 컨테이너에 전달할 환경 변수
  envVars = {
    NODE_ENV: "production",
  };

  // 생명주기 훅
  onStart() {
    console.log("컨테이너가 시작되었습니다");
  }

  onStop() {
    console.log("컨테이너가 중지되었습니다");
  }

  onError(error) {
    console.error("컨테이너 오류:", error);
  }
}

defaultPort는 컨테이너 내부에서 HTTP 서버가 리스닝하는 포트입니다. TLS는 Cloudflare가 자동으로 처리해주니까 컨테이너에서는 일반 HTTP만 열면 돼요.

Dockerfile 작성

컨테이너 이미지는 평범한 Dockerfile로 정의합니다. 한 가지 주의할 점은 반드시 linux/amd64 아키텍처를 타겟으로 빌드해야 한다는 거예요.

Dockerfile
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .

EXPOSE 8080
CMD ["node", "server.js"]

Node.js뿐 아니라 Python이나 Go, Rust 등 Docker 이미지로 만들 수 있으면 뭐든 실행 가능합니다. 이미 Docker로 컨테이너화된 앱이 있다면 거의 그대로 가져다 쓸 수 있어요.

수면과 기상

Cloudflare Containers에서 가장 눈에 띄는 기능은 자동 수면/기상(sleep/wake) 메커니즘이에요.

sleepAfter 값을 설정해두면 일정 시간 동안 요청이 없을 때 자동으로 수면 상태에 들어갑니다. 수면 중에는 과금이 멈추니까 비용을 크게 절약할 수 있어요. 새 요청이 들어오면 알아서 깨어나서 처리를 시작하고요.

export class MyContainer extends Container {
  sleepAfter = "10m"; // 10분 유휴 시 수면

  onStart() {
    // 깨어날 때 상태 복원
    const state = await this.ctx.storage.get("savedState");
    if (state) {
      this.loadState(state);
    }
  }

  onStop() {
    // 수면 전 상태 저장
    await this.ctx.storage.put("savedState", this.currentState);
  }
}

Durable Objects 스토리지를 활용하면 수면 전에 상태를 저장하고 깨어날 때 복원할 수 있습니다. 이렇게 하면 수면과 기상을 반복해도 사용자 경험에는 영향이 없죠.

제로 스케일링도 됩니다. 요청이 전혀 없으면 인스턴스가 0개까지 줄어들어서 트래픽이 없는 시간에는 비용이 아예 안 나와요. AWS ECS나 Google Cloud Run에서는 최소 인스턴스를 유지해야 하는 경우가 많은데 이 부분이 꽤 다릅니다.

Worker를 활용한 세 가지 패턴

Cloudflare Containers에서 Worker는 단순한 프록시가 아니라 여러 역할을 맡을 수 있습니다.

첫 번째는 API 게이트웨이 패턴이에요. Worker에서 인증이나 레이트 리미팅, 캐싱을 처리한 뒤 유효한 요청만 컨테이너로 전달합니다.

src/index.js
export default {
  async fetch(request, env) {
    // Worker에서 인증 처리
    const auth = request.headers.get("Authorization");
    if (!isValidToken(auth)) {
      return new Response("Unauthorized", { status: 401 });
    }

    // 인증된 요청만 컨테이너로 전달
    const id = env.MY_CONTAINER.idFromName("main");
    const container = env.MY_CONTAINER.get(id);
    return container.fetch(request);
  },
};

두 번째는 서비스 메시 패턴입니다. 여러 컨테이너 간 통신을 Worker가 중개하면서 라우팅 로직을 짤 수 있어요. Kubernetes 서비스 메시와 비슷한데 Envoy 같은 별도 프록시가 필요 없습니다.

세 번째는 오케스트레이터 패턴이에요. Worker가 컨테이너 생명주기를 직접 관리하면서 커스텀 스케줄링이나 헬스체크 로직을 구현합니다. Kubernetes 오퍼레이터를 대체할 수 있는 패턴이죠.

네트워킹

보안 측면에서 네트워킹이 꽤 잘 설계되어 있습니다.

컨테이너는 기본적으로 인터넷에 직접 노출되지 않아요. 오직 Worker를 통해서만 접근할 수 있기 때문에 Worker에서 인증이나 접근 제어를 확실히 처리하면 안전하게 보호할 수 있습니다.

TLS도 알아서 처리해줘요. 컨테이너에서는 평문 HTTP 포트만 열면 되고 Cloudflare가 클라이언트와의 통신에 TLS를 자동 적용합니다. 인증서를 직접 관리할 필요가 없습니다.

WebSocket도 지원하고요. 클라이언트에서 컨테이너까지 WebSocket 연결을 유지할 수 있어서 실시간 통신이 필요한 앱도 만들 수 있어요.

아웃바운드 트래픽도 제어할 수 있는데요. enableInternet: false로 설정하면 외부 통신이 Worker를 거치게 되어서 서비스 메시와 비슷한 패턴을 구현할 수 있습니다.

로컬 개발과 배포

로컬에서 개발하려면 Docker가 실행 중이어야 합니다. Wrangler가 Dockerfile을 빌드하고 로컬에서 컨테이너를 띄워줘요.

bunx wrangler dev

코드를 수정하면 R 키를 눌러서 컨테이너를 다시 빌드할 수 있습니다.

배포도 간단해요.

bunx wrangler deploy

처음 배포할 때는 이미지 업로드와 컨테이너 준비에 몇 분 걸릴 수 있어요. 그 뒤로는 일반 Workers 배포와 비슷한 속도로 진행됩니다.

배포된 컨테이너 상태를 확인하려면 이렇게 합니다.

bunx wrangler containers list

요금

Workers Paid 플랜($5/월)에 기본 사용량이 포함되어 있어요.

리소스무료 포함량초과 요금
메모리25 GiB-시간/월$0.0000025/GiB-초
CPU375 vCPU-분/월$0.000020/vCPU-초
디스크200 GB-시간/월$0.00000007/GB-초

과금은 컨테이너가 활성 상태인 동안만 10ms 단위로 발생합니다. 수면 상태에서는 과금이 멈추니까 sleepAfter를 적절히 설정하면 비용을 잘 관리할 수 있어요.

네트워크 이그레스(egress) 요금도 별도로 있는데요. 북미/유럽은 월 1TB까지 무료이고 한국을 포함한 아시아 지역은 500GB까지 무료입니다. 초과분은 한국 기준 GB당 $0.05가 붙어요.

참고로 Cloudflare가 공개한 비교 자료에 따르면 월 2,500만 건의 컨테이너 요청을 처리할 때 Cloudflare Containers는 약 $20, Google Cloud Run은 약 $23.75 정도라고 합니다. 가격 면에서도 나쁘지 않죠.

어떤 경우에 쓰면 좋을까?

Cloudflare Containers가 특히 빛을 발하는 경우가 몇 가지 있어요.

코드 샌드박싱이 대표적인데요. 사용자별로 격리된 컨테이너를 띄워서 코드를 안전하게 실행할 수 있습니다. LLM이 생성한 코드를 실행하는 AI 에이전트 같은 서비스에 딱 맞죠.

미디어 처리에도 잘 어울려요. FFmpeg를 컨테이너에 설치해서 영상이나 오디오 트랜스코딩을 엣지에서 처리할 수 있거든요. 사용자와 가까운 데이터 센터에서 처리되니까 업로드와 다운로드 속도도 빠릅니다.

기존 백엔드 서비스를 옮기는 데도 쓸만해요. Python이나 Go, Rust로 작성된 서비스를 Docker 이미지로 만들어서 그대로 올릴 수 있으니까요. Worker가 API 게이트웨이 역할을 하면서 기존 서비스를 점진적으로 엣지로 옮겨갈 수 있습니다.

반면 단순한 API 엔드포인트나 가벼운 데이터 변환 정도라면 굳이 컨테이너를 쓸 필요가 없습니다. Cloudflare Workers만으로 충분하고 시작 시간도 훨씬 빠르거든요.

마치며

Cloudflare Containers는 “서버리스의 간편함”과 “컨테이너의 유연함”을 동시에 잡으려는 시도입니다. Workers가 가벼운 로직을 맡고 Containers가 무거운 작업을 처리하는 조합이 꽤 매력적이에요.

특히 자동 수면/기상으로 제로 스케일링이 가능하다는 점, 리전 설정 없이 전 세계 엣지에서 실행된다는 점이 AWS의 컨테이너 서비스와 차별화되는 부분이죠. 아직 퍼블릭 베타라서 프로덕션에 바로 투입하기는 조심스러울 수 있지만 개인 프로젝트나 새로운 서비스를 시작할 때 한번 시도해볼 만합니다.

더 자세한 내용은 Cloudflare Containers 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord