Developer Tools

개발자를 위한 Docker: 컨테이너, Compose, 그리고 실전 워크플로우

Docker 개념, Dockerfile 모범 사례, 그리고 Docker Compose를 활용한 멀티 서비스 개발 환경 구성에 대한 실용적인 가이드입니다.

9분 읽기

파란 조명이 있는 서버실

"내 컴퓨터에서는 잘 됐는데"라는 말은 수많은 배포 실패의 시작점이었습니다. 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가 실제로 연결을 수락하기까지 몇 초가 더 걸릴 수 있습니다. healthcheckcondition: service_healthy를 함께 사용하여 서비스가 실제로 준비될 때까지 기다리세요.

프로덕션 고려사항

  1. 특정 이미지 태그 사용node:latest가 아닌 node:20.11.1-alpine처럼 명시하세요.
  2. 이미지 취약점 스캔docker scout cves myapp:latest를 실행하세요.
  3. 리소스 제한 설정 — 하나의 컨테이너가 다른 컨테이너의 리소스를 독점하지 않도록 하세요.
  4. 가능한 경우 읽기 전용 파일시스템 사용 — Compose에서 read_only: true를 설정하세요.
  5. 절대 루트로 실행하지 마세요USER node 또는 동등한 설정을 추가하세요.
  6. 앱 앞에 리버스 프록시로 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 파이프라인, 클라우드 배포로 나아갈 수 있는 탄탄한 토대를 갖추게 됩니다.