Ch 02. Docker로 Dart 서버 배포하기
"내 컴퓨터에서는 되는데 서버에서는 안 돼요." 개발자라면 한 번쯤 경험해 본 상황입니다. Docker는 이 문제를 컨테이너로 해결합니다. 애플리케이션과 실행 환경을 함께 패키징하여 어디서나 동일하게 실행됩니다.
Dart 공식 Docker 이미지가 있어서 Dart 서버를 컨테이너화하기 쉽습니다.
공식 Dart Docker 이미지
Docker Hub에서 dart 이미지를 확인합니다.
# 태그 확인docker pull dart:stable# 버전 지정docker pull dart:3.3
dart:stable은 최신 안정 버전을 가리킵니다. 프로덕션에서는 dart:3.3처럼 버전을 고정하는 것이 좋습니다.
멀티스테이지 빌드
Dart 서버의 Docker 이미지를 만들 때 멀티스테이지 빌드를 사용합니다. "빌드 스테이지"에서 AOT 컴파일하고, "실행 스테이지"에는 컴파일된 바이너리만 복사합니다. 결과 이미지에 Dart SDK가 필요 없으므로 크기가 훨씬 작아집니다.
# 새 파일: todo_server/Dockerfile# ── 빌드 스테이지 ──────────────────────────────FROM dart:stable AS builderWORKDIR /app# 의존성 먼저 복사 (캐시 활용)COPY pubspec.yaml pubspec.lock ./RUN dart pub get# 소스 복사COPY . .# dart_frog 빌드 (서버 번들링)RUN dart pub global activate dart_frog_cliRUN dart_frog build# AOT 컴파일RUN dart compile exe build/bin/server.dart -o build/server# ── 실행 스테이지 ──────────────────────────────FROM debian:bookworm-slimWORKDIR /app# 필요한 공유 라이브러리 복사 (AOT 바이너리 의존)COPY --from=builder /runtime/ /# 컴파일된 바이너리만 복사COPY --from=builder /app/build/server /app/server# 8080 포트 노출EXPOSE 8080# 서버 실행CMD ["/app/server"]
공식 Dart Docker 이미지는 /runtime/ 경로에 AOT 실행에 필요한 공유 라이브러리를 제공합니다. COPY --from=builder /runtime/ /로 이 라이브러리를 포함시켜야 AOT 바이너리가 실행됩니다.
.dockerignore 설정
불필요한 파일이 이미지에 포함되지 않도록 합니다.
# 새 파일: todo_server/.dockerignore
.dart_tool/
.git/
build/
coverage/
*.md
test/
이미지 빌드 및 실행
cd todo_server# 이미지 빌드docker build -t todo-server:latest .# 이미지 크기 확인docker images todo-server# 컨테이너 실행docker run -p 8080:8080 todo-server:latest# 환경 변수와 함께 실행docker run \ -p 8080:8080 \ -e JWT_SECRET=my-super-secret \ -e DB_PATH=/data/todos.db \ -v $(pwd)/data:/data \ todo-server:latest
-v $(pwd)/data:/data는 로컬의 data/ 폴더를 컨테이너의 /data/에 마운트합니다. SQLite 파일이 컨테이너 안에만 있으면 컨테이너를 삭제할 때 데이터가 사라지므로, 볼륨 마운트로 영구 보관합니다.
환경 변수 처리
서버 코드에서 환경 변수를 읽는 방법을 확인합니다.
// 수정: todo_server/lib/config.dart
import 'dart:io';
class Config {
static String get jwtSecret =>
Platform.environment['JWT_SECRET'] ?? 'dev-secret-change-in-production';
static String get dbPath =>
Platform.environment['DB_PATH'] ?? 'todos.db';
static int get port =>
int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080;
}
개발 환경에서는 기본값이 사용되고, 프로덕션 Docker 환경에서는 환경 변수로 재정의합니다.
docker-compose 설정
여러 서비스를 함께 실행할 때 docker-compose가 편리합니다.
# 새 파일: docker-compose.ymlversion: '3.8'services: todo-server: build: context: ./todo_server dockerfile: Dockerfile ports: - "8080:8080" environment: - JWT_SECRET=${JWT_SECRET:-dev-secret} - DB_PATH=/data/todos.db - PORT=8080 volumes: - todo-data:/data restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3volumes: todo-data:
.env 파일에 환경 변수를 저장하면 docker-compose가 자동으로 읽습니다.
# 새 파일: .envJWT_SECRET=my-production-secret-key-at-least-32-chars
# 서비스 시작docker-compose up -d# 로그 확인docker-compose logs -f todo-server# 서비스 중지docker-compose down# 볼륨 포함 삭제 (데이터도 삭제됨)docker-compose down -v
헬스 체크 엔드포인트 추가
docker-compose의 healthcheck가 /health 경로를 호출합니다. 이 엔드포인트를 서버에 추가합니다.
// 새 파일: todo_server/routes/health.dart
import 'package:dart_frog/dart_frog.dart';
Response onRequest(RequestContext context) {
return Response.json(body: {
'status': 'ok',
'timestamp': DateTime.now().toIso8601String(),
});
}
헬스 체크 엔드포인트가 응답하지 않으면 Docker가 컨테이너를 자동으로 재시작합니다. 실제 서비스에서 중요한 패턴입니다.
이미지 최적화 결과
멀티스테이지 빌드 전후 크기를 비교합니다.
# 싱글 스테이지 (Dart SDK 포함)# dart:stable 이미지: ~800MB# 멀티스테이지 (바이너리만)docker images todo-server# REPOSITORY TAG SIZE# todo-server latest ~80MB
멀티스테이지 빌드로 이미지 크기를 10분의 1 이하로 줄일 수 있습니다. 작은 이미지는 배포 속도를 높이고 보안 취약점 노출을 줄입니다.
이번 챕터 정리
- 공식
dart:stableDocker 이미지로 Dart 서버를 컨테이너화했습니다. - 멀티스테이지 빌드로 최종 이미지를 AOT 바이너리만 포함하도록 최소화했습니다.
- 환경 변수로 민감한 설정(JWT 시크릿, DB 경로)을 분리했습니다.
docker-compose로 서비스 실행, 볼륨 마운트, 헬스 체크를 구성했습니다.
다음 챕터에서는 GitHub Actions로 테스트와 배포를 자동화합니다.