개발자를 위한 Docker: 컨테이너, Compose, 그리고 실전 워크플로우
Docker 개념, Dockerfile 모범 사례, 그리고 Docker Compose를 활용한 멀티 서비스 개발 환경 구성에 대한 실용적인 가이드입니다.
"내 컴퓨터에서는 잘 됐는데"라는 말은 수많은 배포 실패의 시작점이었습니다. Docker는 애플리케이션과 모든 의존성을 컨테이너로 패키징하여 이 문제를 해결합니다. 컨테이너는 격리된 재현 가능한 환경으로, 노트북에서부터 프로덕션 서버까지 어디서나 동일하게 실행됩니다.
컨테이너 vs. 가상 머신
| 가상 머신 | 컨테이너 | |
|---|---|---|
| 격리 방식 | 전체 OS | 프로세스 수준 |
| 시작 시간 | 분 단위 | 초 단위 |
| 크기 | GB 수준 | MB 수준 |
| 오버헤드 | 높음 (하이퍼바이저) | 거의 없음 |
| 사용 목적 | 완전한 OS 격리 | 앱 패키징 |
컨테이너는 호스트 커널을 공유하지만 파일시스템, 프로세스, 네트워킹은 격리합니다. 바로 그 덕분에 컨테이너가 매우 가볍습니다.
Docker 핵심 개념
Image — 컨테이너의 읽기 전용 청사진입니다. 클래스 정의라고 생각하면 됩니다.
Container — Image의 실행 인스턴스입니다. 클래스에서 생성된 객체라고 생각하면 됩니다.
Registry — Image를 저장하고 배포하는 시스템입니다. Docker Hub이 기본 공개 레지스트리이며, GitHub Container Registry와 AWS ECR이 자주 사용됩니다.
Volume — 컨테이너가 재시작되어도 유지되는 영구 저장소입니다. Volume 없이 컨테이너 내부에 쓴 데이터는 컨테이너가 종료되면 사라집니다.
좋은 Dockerfile 작성하기
# 특정 버전을 명시하세요 — 프로덕션에서 "latest"는 절대 사용 금지
FROM node:20-alpine
# 작업 디렉터리 설정
WORKDIR /app
# 의존성 파일을 먼저 복사 (레이어 캐싱 활용)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 애플리케이션 코드 복사
COPY . .
# 앱 빌드
RUN npm run build
# 보안을 위해 루트가 아닌 사용자로 실행
USER node
# 포트 문서화 (실제로 포트를 게시하지는 않음)
EXPOSE 3000
# 올바른 시그널 처리를 위해 exec 형식 사용
CMD ["node", "server.js"]
레이어 캐싱: 빠른 빌드의 핵심
Dockerfile의 각 명령어는 레이어를 생성합니다. Docker는 레이어를 캐시하며, 해당 레이어나 이전 레이어에 변경 사항이 없으면 캐시를 재사용합니다.
애플리케이션 코드보다 의존성 파일을 먼저 복사하세요. 의존성은 코드보다 훨씬 덜 변경됩니다. 모든 파일을 한 번에 복사하면 index.js에서 한 줄만 바꿔도 의존성 설치 캐시가 무효화되어 npm install을 처음부터 다시 실행하게 됩니다.
멀티 스테이지 빌드
멀티 스테이지 빌드를 사용하여 프로덕션 이미지를 가볍게 유지하세요:
# 스테이지 1: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 스테이지 2: 프로덕션 (개발 의존성 없음, 소스 코드 없음)
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
최종 이미지에는 빌드된 결과물만 포함되며, 소스 코드, 테스트 파일, 개발 의존성은 포함되지 않습니다.
로컬 개발을 위한 Docker Compose
Docker Compose는 멀티 컨테이너 애플리케이션을 오케스트레이션합니다. 하나의 docker-compose.yml 파일로 로컬 스택 전체를 정의할 수 있습니다:
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Docker Compose Generator를 사용하면 원하는 서비스를 선택하고 몇 초 만에 프로덕션 수준의 docker-compose.yml을 생성할 수 있습니다.
필수 Compose 명령어
# 모든 서비스를 백그라운드에서 시작
docker compose up -d
# 로그 확인 (실시간 출력 모드)
docker compose logs -f app
# 실행 중인 컨테이너 내에서 명령어 실행
docker compose exec app sh
# 컨테이너 중지 및 제거 (볼륨은 유지)
docker compose down
# 컨테이너와 볼륨 모두 중지 및 제거
docker compose down -v
# Dockerfile 변경 후 이미지 재빌드
docker compose up -d --build
환경 변수와 시크릿
Dockerfile이나 Compose 파일에 절대 자격증명을 하드코딩하지 마세요. .env 파일을 사용하세요:
# .env (.gitignore에 추가하세요!)
DATABASE_URL=postgresql://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
JWT_SECRET=your-super-secret-key
Compose는 프로젝트 디렉터리에서 .env 파일을 자동으로 불러옵니다. YAML 파일에서 ${VARIABLE_NAME} 형식으로 변수를 참조하세요.
Env Generator를 사용하면 합리적인 기본값이 포함된 .env 파일과 팀을 위한 .env.example 템플릿을 쉽게 생성할 수 있습니다.
Docker 네트워킹
Compose는 스택을 위한 기본 네트워크를 생성합니다. 서비스들은 서비스 이름을 호스트명으로 사용하여 서로 통신할 수 있습니다. 앱이 데이터베이스 호스트로 localhost 대신 db를 사용하는 이유가 바로 이 때문입니다.
app → "db:5432"에 연결
app → "redis:6379"에 연결
ports:로 명시적으로 매핑된 포트만 호스트 머신에서 접근할 수 있습니다.
헬스 체크와 시작 순서
depends_on은 컨테이너가 시작되기를 기다릴 뿐, 준비 완료 상태를 기다리지 않습니다. 데이터베이스 컨테이너는 몇 초 안에 시작되지만, Postgres가 실제로 연결을 수락하기까지 몇 초가 더 걸릴 수 있습니다. healthcheck와 condition: service_healthy를 함께 사용하여 서비스가 실제로 준비될 때까지 기다리세요.
프로덕션 고려사항
- 특정 이미지 태그 사용 —
node:latest가 아닌node:20.11.1-alpine처럼 명시하세요. - 이미지 취약점 스캔 —
docker scout cves myapp:latest를 실행하세요. - 리소스 제한 설정 — 하나의 컨테이너가 다른 컨테이너의 리소스를 독점하지 않도록 하세요.
- 가능한 경우 읽기 전용 파일시스템 사용 — Compose에서
read_only: true를 설정하세요. - 절대 루트로 실행하지 마세요 —
USER node또는 동등한 설정을 추가하세요. - 앱 앞에 리버스 프록시로 Nginx 구성 — Nginx Config Generator를 사용하여 검증된 설정을 바로 얻으세요.
빠른 참조
# Images
docker images # 로컬 이미지 목록
docker pull nginx:alpine # 이미지 다운로드
docker rmi myimage # 이미지 제거
# Containers
docker ps # 실행 중인 컨테이너
docker ps -a # 모든 컨테이너
docker stop <id> # 정상 종료
docker rm <id> # 종료된 컨테이너 제거
docker logs <id> -f # 로그 스트리밍
# 정리
docker system prune # 미사용 항목 모두 제거
docker volume prune # 미사용 볼륨 제거
Docker는 환경 관련 버그 전체를 없애주고, 새로운 개발자 온보딩을 매우 쉽게 만들어 줍니다. 여기서 기본기를 다지면 Kubernetes, CI/CD 파이프라인, 클라우드 배포로 나아갈 수 있는 탄탄한 토대를 갖추게 됩니다.