Docker & Kubernetes 완전 정복 3편: Dockerfile 작성과 이미지 빌드
Docker & Kubernetes Complete Guide Part 3: Dockerfile Writing and Image Build
서론: Dockerfile의 중요성
Docker 시리즈 3편에서는 컨테이너 이미지를 만드는 핵심 요소인 Dockerfile에 대해 심층적으로 다룹니다. Dockerfile은 애플리케이션을 컨테이너화하기 위한 청사진으로, 이를 잘 작성하는 것이 효율적이고 안전한 컨테이너 운영의 첫걸음입니다.
이 글에서는 Dockerfile의 기본 개념부터 주요 명령어, 멀티스테이지 빌드, 빌드 캐시 활용, 그리고 보안을 고려한 베스트 프랙티스까지 상세히 알아보겠습니다.
1. Dockerfile이란?
1.1 Dockerfile의 정의
Dockerfile은 Docker 이미지를 빌드하기 위한 텍스트 파일입니다. 이 파일에는 베이스 이미지 선택부터 애플리케이션 설치, 환경 설정, 실행 명령까지 이미지 생성에 필요한 모든 명령어가 순차적으로 정의됩니다.
Dockerfile의 주요 특징:
- 재현 가능성: 동일한 Dockerfile로 언제 어디서나 같은 이미지를 빌드할 수 있습니다
- 버전 관리: 텍스트 파일이므로 Git 등 버전 관리 시스템으로 변경 이력을 추적할 수 있습니다
- 자동화: CI/CD 파이프라인에서 자동으로 이미지를 빌드하고 배포할 수 있습니다
- 문서화: Dockerfile 자체가 애플리케이션의 실행 환경을 문서화하는 역할을 합니다
1.2 기본 구조
간단한 Node.js 애플리케이션을 위한 Dockerfile 예시:
# 베이스 이미지 지정
FROM node:20-alpine
# 작업 디렉토리 설정
WORKDIR /app
# 패키지 파일 복사 및 의존성 설치
COPY package*.json ./
RUN npm install
# 소스 코드 복사
COPY . .
# 환경 변수 설정
ENV NODE_ENV=production
# 포트 노출
EXPOSE 3000
# 실행 명령
CMD ["node", "server.js"]
2. Dockerfile 주요 명령어
2.1 FROM - 베이스 이미지 지정
FROM은 모든 Dockerfile의 첫 번째 명령어로, 새 이미지의 기반이 될 베이스 이미지를 지정합니다.
# 기본 형식
FROM <이미지>:<태그>
# 예시
FROM ubuntu:22.04
FROM python:3.11-slim
FROM node:20-alpine
FROM scratch # 빈 이미지에서 시작
베이스 이미지 선택 시 고려사항:
- Alpine 이미지: 매우 작은 크기(약 5MB), 보안성 높음, 일부 호환성 이슈 가능
- Slim 이미지: 불필요한 패키지 제거된 경량 버전
- 공식 이미지: Docker Hub에서 검증된 공식 이미지 사용 권장
2.2 RUN - 명령어 실행
RUN은 이미지 빌드 과정에서 명령을 실행하고 그 결과를 새 레이어로 저장합니다.
# Shell 형식
RUN apt-get update && apt-get install -y curl
# Exec 형식
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "curl"]
# 여러 명령을 하나의 RUN으로 결합 (레이어 최소화)
RUN apt-get update \
&& apt-get install -y \
curl \
vim \
git \
&& rm -rf /var/lib/apt/lists/*
2.3 COPY vs ADD - 파일 복사
COPY와 ADD 모두 파일을 이미지에 복사하지만, 용도가 다릅니다.
# COPY - 로컬 파일/디렉토리를 이미지로 복사
COPY src/ /app/src/
COPY package.json package-lock.json ./
# ADD - COPY 기능 + 추가 기능
# 1. URL에서 파일 다운로드
ADD https://example.com/file.tar.gz /tmp/
# 2. 압축 파일 자동 해제 (tar, gzip, bzip2, xz)
ADD archive.tar.gz /app/
권장사항: 대부분의 경우 COPY를 사용하세요. ADD는 URL 다운로드나 자동 압축 해제가 필요할 때만 사용합니다.
2.4 WORKDIR - 작업 디렉토리 설정
WORKDIR은 이후의 RUN, CMD, ENTRYPOINT, COPY, ADD 명령어가 실행될 작업 디렉토리를 설정합니다.
# 작업 디렉토리 설정
WORKDIR /app
# 존재하지 않으면 자동 생성됨
WORKDIR /app/src
# 상대 경로도 가능 (이전 WORKDIR 기준)
WORKDIR subdir # /app/src/subdir
2.5 ENV - 환경 변수 설정
ENV는 컨테이너 실행 시 사용할 환경 변수를 설정합니다.
# 단일 환경 변수
ENV NODE_ENV=production
# 여러 환경 변수 (한 줄)
ENV NODE_ENV=production PORT=3000
# 여러 환경 변수 (여러 줄)
ENV NODE_ENV=production \
PORT=3000 \
DB_HOST=localhost
2.6 EXPOSE - 포트 문서화
EXPOSE는 컨테이너가 사용할 포트를 문서화합니다. 실제 포트 바인딩은 docker run -p로 수행합니다.
# 단일 포트
EXPOSE 3000
# 여러 포트
EXPOSE 80 443
# UDP 포트
EXPOSE 53/udp
2.7 CMD vs ENTRYPOINT - 실행 명령
CMD와 ENTRYPOINT는 컨테이너 시작 시 실행할 명령을 정의합니다.
# CMD - 기본 실행 명령 (docker run에서 덮어쓰기 가능)
CMD ["node", "server.js"]
CMD ["npm", "start"]
# ENTRYPOINT - 고정 실행 명령
ENTRYPOINT ["python", "app.py"]
# CMD와 ENTRYPOINT 조합
ENTRYPOINT ["python"]
CMD ["app.py"] # docker run image other.py로 변경 가능
| 명령어 | 역할 | 덮어쓰기 |
|---|---|---|
| CMD | 기본 명령/인수 | docker run 인수로 덮어쓰기 가능 |
| ENTRYPOINT | 고정 실행 파일 | --entrypoint 옵션으로만 변경 가능 |
2.8 기타 유용한 명령어
# ARG - 빌드 시점 변수 (이미지에 저장되지 않음)
ARG VERSION=1.0
ARG BUILD_DATE
# LABEL - 메타데이터 추가
LABEL maintainer="dev@example.com"
LABEL version="1.0"
LABEL description="My application"
# USER - 실행 사용자 변경
USER node
USER 1001:1001
# VOLUME - 볼륨 마운트 포인트 선언
VOLUME /data
VOLUME ["/var/log", "/var/db"]
# HEALTHCHECK - 컨테이너 상태 확인
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
3. 멀티스테이지 빌드
3.1 멀티스테이지 빌드란?
멀티스테이지 빌드는 하나의 Dockerfile에서 여러 단계(stage)를 정의하여, 빌드에 필요한 도구와 최종 실행에 필요한 파일을 분리하는 기법입니다. 이를 통해 최종 이미지 크기를 크게 줄일 수 있습니다.
3.2 실전 예제: Go 애플리케이션
# 빌드 스테이지
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# 실행 스테이지
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 빌드 스테이지에서 바이너리만 복사
COPY --from=builder /app/main .
CMD ["./main"]
3.3 실전 예제: Node.js 애플리케이션
# 의존성 설치 스테이지
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 실행 스테이지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 프로덕션 의존성만 복사
COPY --from=deps /app/node_modules ./node_modules
# 빌드 결과물만 복사
COPY --from=builder /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
3.4 멀티스테이지 빌드의 장점
- 이미지 크기 감소: 빌드 도구 없이 실행에 필요한 파일만 포함
- 보안 강화: 빌드 시 사용된 소스 코드, 인증 정보 등이 최종 이미지에 포함되지 않음
- 빌드 캐시 활용: 각 스테이지를 독립적으로 캐시할 수 있음
4. .dockerignore 파일
4.1 .dockerignore란?
.dockerignore 파일은 Docker 빌드 컨텍스트에서 제외할 파일과 디렉토리를 지정합니다. .gitignore와 유사한 문법을 사용합니다.
4.2 .dockerignore 예시
# Git 관련
.git
.gitignore
# Node.js
node_modules
npm-debug.log
# 빌드 결과물 (멀티스테이지가 아닌 경우)
dist
build
# 환경 설정
.env
.env.local
*.env
# IDE
.vscode
.idea
*.swp
# 테스트
coverage
__tests__
*.test.js
# 문서
README.md
docs
# Docker 관련
Dockerfile*
docker-compose*
.docker
4.3 .dockerignore의 중요성
- 빌드 속도 향상: 불필요한 파일이 빌드 컨텍스트에 포함되지 않아 전송 시간 단축
- 이미지 크기 감소: COPY 명령어 사용 시 불필요한 파일 복사 방지
- 보안:
.env, 인증서 등 민감한 파일이 이미지에 포함되는 것을 방지
5. 이미지 빌드 (docker build)
5.1 기본 빌드 명령
# 현재 디렉토리의 Dockerfile로 빌드
docker build -t myapp:1.0 .
# 다른 Dockerfile 지정
docker build -f Dockerfile.prod -t myapp:prod .
# 빌드 인수 전달
docker build --build-arg VERSION=2.0 -t myapp:2.0 .
# 빌드 캐시 무시
docker build --no-cache -t myapp:latest .
# 특정 스테이지까지만 빌드
docker build --target builder -t myapp:builder .
5.2 빌드 컨텍스트
빌드 컨텍스트는 docker build 명령에 전달되는 파일과 디렉토리의 집합입니다. 마지막 인수(`.`)가 빌드 컨텍스트 경로를 지정합니다.
# 현재 디렉토리를 빌드 컨텍스트로 사용
docker build -t myapp .
# 특정 디렉토리를 빌드 컨텍스트로 사용
docker build -t myapp /path/to/context
# Git 저장소에서 직접 빌드
docker build -t myapp https://github.com/user/repo.git
# stdin에서 Dockerfile 읽기
docker build -t myapp - < Dockerfile
6. 빌드 캐시 활용
6.1 캐시 동작 원리
Docker는 빌드 시 각 명령어의 결과를 레이어로 캐시합니다. 이전 빌드와 동일한 명령어는 캐시된 레이어를 재사용합니다.
캐시가 무효화되는 조건:
- 명령어가 변경된 경우
- COPY/ADD의 소스 파일이 변경된 경우
- 상위 레이어의 캐시가 무효화된 경우
6.2 캐시 효율을 높이는 팁
# 나쁜 예: 소스 코드 변경 시 npm install도 다시 실행됨
COPY . .
RUN npm install
# 좋은 예: package.json이 변경되지 않으면 캐시 사용
COPY package*.json ./
RUN npm install
COPY . .
자주 변경되는 레이어는 Dockerfile 아래쪽에 배치하여 캐시 효율을 높입니다.
7. 이미지 태깅과 버전 관리
7.1 태그 전략
# 버전 태그
docker build -t myapp:1.0.0 .
docker build -t myapp:1.0 .
docker build -t myapp:1 .
# 환경별 태그
docker build -t myapp:prod .
docker build -t myapp:staging .
docker build -t myapp:dev .
# Git 커밋 해시 태그
docker build -t myapp:$(git rev-parse --short HEAD) .
# 날짜 태그
docker build -t myapp:$(date +%Y%m%d) .
7.2 태그 추가 및 변경
# 기존 이미지에 새 태그 추가
docker tag myapp:1.0.0 myapp:latest
docker tag myapp:1.0.0 registry.example.com/myapp:1.0.0
# 레지스트리에 푸시
docker push registry.example.com/myapp:1.0.0
8. Dockerfile 베스트 프랙티스
8.1 레이어 최소화
# 나쁜 예: 불필요한 레이어 생성
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 좋은 예: 하나의 RUN으로 결합
RUN apt-get update \
&& apt-get install -y curl vim \
&& rm -rf /var/lib/apt/lists/*
8.2 보안 고려사항
# 1. 비루트 사용자로 실행
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
USER nodejs
# 2. 신뢰할 수 있는 베이스 이미지 사용
FROM node:20-alpine@sha256:abc123...
# 3. 불필요한 패키지 설치 금지
RUN apt-get install --no-install-recommends -y curl
# 4. 민감한 정보를 ENV 대신 실행 시점에 전달
# 나쁜 예
ENV DATABASE_PASSWORD=secret123
# 좋은 예: docker run -e DATABASE_PASSWORD=xxx 사용
# 5. COPY 대신 특정 파일만 복사
COPY src/ /app/src/
COPY package.json /app/
8.3 이미지 크기 최적화
- 가능한 작은 베이스 이미지 사용 (Alpine, distroless)
- 멀티스테이지 빌드 활용
- 불필요한 파일 삭제 (캐시, 임시 파일)
- .dockerignore 적극 활용
8.4 완성된 Dockerfile 예시
# 프로덕션용 Node.js 애플리케이션 Dockerfile
# 멀티스테이지 빌드와 보안 베스트 프랙티스 적용
# 1. 의존성 설치 스테이지
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 2. 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 3. 프로덕션 스테이지
FROM node:20-alpine AS runner
LABEL maintainer="dev@example.com"
LABEL version="1.0"
WORKDIR /app
ENV NODE_ENV=production
# 비루트 사용자 생성
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
# 필요한 파일만 복사
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# 소유권 변경 및 사용자 전환
RUN chown -R nodejs:nodejs /app
USER nodejs
# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/server.js"]
결론
Dockerfile 작성은 컨테이너화의 핵심 기술입니다. 이 글에서 다룬 내용을 정리하면:
- 기본 명령어 이해: FROM, RUN, COPY, WORKDIR, ENV, EXPOSE, CMD, ENTRYPOINT의 역할과 사용법
- 멀티스테이지 빌드: 빌드와 실행 환경을 분리하여 이미지 크기를 최소화
- .dockerignore: 불필요한 파일을 빌드에서 제외
- 빌드 캐시: 레이어 순서를 최적화하여 빌드 속도 향상
- 베스트 프랙티스: 보안, 크기, 유지보수성을 고려한 Dockerfile 작성
다음 4편에서는 Docker Compose를 사용하여 여러 컨테이너를 효율적으로 관리하는 방법을 알아보겠습니다.