개발자를 위한 Docker: 이미지, 컨테이너, Compose
Docker와 컨테이너에 대해 들어보셨겠지만, 실제로 사용하려고 하면 이미지, 컨테이너, 볼륨, Compose 같은 용어에 압도되곤 합니다. 아니면 이미 Docker를 사용 중이지만 이미지가 너무 크고, 빌드가 느리고, 여러 서비스를 관리하는 게 귀찮게 느껴질 수도 있습니다. 이 가이드는 혼란을 정리해 드립니다. 개발자를 위한 Docker의 필수 요소를 다루며, 컨테이너화된 애플리케이션을 효율적으로 빌드하고, 실행하고, 관리하는 데 실제로 알아야 할 내용에 초점을 맞춥니다.
왜 Docker인가? 해결하는 문제
Docker 이전에는 앱을 배포하려면 정확한 환경을 맞춰야 했습니다. 특정 OS 버전, 라이브러리, 구성 등이었죠. "내 컴퓨터에서는 되는데"라는 말이 진부해진 건 사실이었기 때문입니다. Docker는 애플리케이션을 의존성과 함께 컨테이너라는 단일 이식 가능한 단위로 패키징하여 이 문제를 해결합니다. 컨테이너는 가볍고 격리되어 있으며 Docker가 설치된 어디서든 일관되게 실행됩니다.
이미지 vs. 컨테이너: 핵심 차이
이미지는 애플리케이션 코드, 런타임, 라이브러리, 설정을 포함하는 읽기 전용 템플릿입니다. 컨테이너는 이미지의 실행 인스턴스입니다. 이미지를 클래스, 컨테이너를 객체라고 생각하세요. 동일한 이미지에서 여러 컨테이너를 실행할 수 있으며, 각각 서로 격리됩니다.
이미지가 빌드되는 방식
이미지는 지침이 담긴 텍스트 파일인 Dockerfile에서 빌드됩니다. 다음은 Node.js 앱을 위한 최소 예제입니다:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
각 지침은 레이어를 생성합니다. 레이어는 캐시되므로 소스 코드만 변경하면 Docker는 의존성에 대한 캐시된 레이어를 재사용하여 재빌드가 빨라집니다.
빌드 및 실행
이미지를 빌드하고 컨테이너를 실행합니다:
docker build -t my-app .
docker run -p 3000:3000 my-app
-p 플래그는 호스트의 포트 3000을 컨테이너의 포트 3000에 매핑합니다.
효율적인 Docker 이미지를 위한 모범 사례
큰 이미지는 빌드, 푸시, 배포를 느리게 만듭니다. 이미지를 가볍게 유지하려면 다음 관행을 따르세요:
- 가능하면 공식 slim 또는 alpine 베이스 이미지를 사용하세요. 예를 들어
node:18-alpine은node:18보다 훨씬 작습니다. - 멀티 스테이지 빌드를 활용하세요 빌드 시 의존성과 런타임을 분리합니다. 이렇게 하면 최종 이미지가 최소화됩니다.
- RUN 명령을 결합하세요 레이어를 줄이고 같은 레이어에서 패키지 관리자 캐시를 정리합니다.
- .dockerignore 파일을 사용하세요 불필요한 파일(예: node_modules, .git)을 빌드 컨텍스트에서 제외합니다.
다음은 Go 애플리케이션을 위한 멀티 스테이지 Dockerfile입니다:
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/main .
# Final stage
FROM alpine:latest
COPY --from=builder /app/main /main
CMD ["/main"]
최종 이미지는 컴파일된 바이너리와 Alpine만 포함하며 Go 툴체인은 포함하지 않습니다.
볼륨으로 데이터 관리하기
컨테이너는 일시적입니다. 제거되면 데이터도 사라집니다. 볼륨은 컨테이너 수명 주기 외부에 데이터를 유지합니다. 데이터베이스, 업로드 또는 재시작 후에도 유지되어야 하는 모든 상태에 사용하세요.
docker run -v my-data:/var/lib/postgresql/data postgres
이렇게 하면 Docker가 관리하는 명명된 볼륨 my-data가 생성됩니다. 개발 시에는 소스 코드를 컨테이너에 바인드 마운트하여 라이브 리로딩을 할 수도 있습니다:
docker run -v $(pwd):/app -p 3000:3000 my-app
Docker Compose: 여러 컨테이너 오케스트레이션
실제 애플리케이션은 종종 여러 서비스가 필요합니다: 웹 서버, 데이터베이스, 캐시. 각각을 docker run으로 시작하는 것은 지루하고 오류가 발생하기 쉽습니다. Docker Compose를 사용하면 단일 YAML 파일로 멀티 컨테이너 앱을 정의하고 실행할 수 있습니다.
실용적인 docker-compose.yml
다음은 PostgreSQL과 Redis를 사용하는 Node.js 앱을 위한 Compose 파일입니다:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
postgres_data:
docker compose up를 실행하여 모든 서비스를 시작합니다. Compose는 기본 네트워크를 생성하므로 컨테이너가 서비스 이름(예: db, cache)으로 서로 통신할 수 있습니다.
자주 쓰는 Compose 명령어
docker compose up -d– 분리 모드(백그라운드)로 시작합니다.docker compose logs -f web– 서비스의 로그를 따릅니다.docker compose exec web sh– 실행 중인 컨테이너에서 셸을 엽니다.docker compose down– 컨테이너, 네트워크, 볼륨을 중지하고 제거합니다(볼륨이 외부가 아닌 경우).
Docker Compose vs. Kubernetes 사용 시기
Docker Compose는 로컬 개발, 테스트, 소규모 배포에 이상적입니다. Kubernetes는 대규모 프로덕션을 위해 클러스터 전반에 걸쳐 컨테이너를 오케스트레이션하는 데 탁월합니다. 많은 개발자에게 Compose는 일상 작업에 충분하며, 자동 확장, 자가 치유, 고급 네트워킹이 필요할 때 Kubernetes로 넘어갈 수 있습니다.
| 기능 | Docker Compose | Kubernetes |
|---|---|---|
| 사용 사례 | 로컬 개발, 소규모 배포 | 프로덕션 클러스터 |
| 복잡성 | 낮음 | 높음 |
| 확장 | 수동 | 자동 |
| 학습 곡선 | 완만함 | 가파름 |
FAQ
이미지와 컨테이너의 차이점은 무엇인가요?
이미지는 앱과 의존성을 포함하는 읽기 전용 템플릿입니다. 컨테이너는 이미지의 실행 인스턴스입니다. 하나의 이미지에서 여러 컨테이너를 실행할 수 있습니다.
Docker 이미지 크기를 줄이려면 어떻게 하나요?
더 작은 베이스 이미지(예: Alpine)를 사용하고, 멀티 스테이지 빌드를 적용하고, RUN 명령을 결합하고, .dockerignore 파일로 불필요한 파일을 제외하세요.
프로덕션에서 Docker Compose를 사용할 수 있나요?
네, 소규모 배포에는 가능합니다. 더 크고 동적인 환경에는 Kubernetes 또는 Docker Swarm이 더 적합합니다.
Docker 워크플로를 최적화할 준비가 되셨나요? 컨테이너화된 웹 서버를 디버깅하고 모니터링하려면 Nginx Log Analyzer를 확인해 보세요.