서론: 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 - 파일 복사

COPYADD 모두 파일을 이미지에 복사하지만, 용도가 다릅니다.

# 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 - 실행 명령

CMDENTRYPOINT는 컨테이너 시작 시 실행할 명령을 정의합니다.

# 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를 사용하여 여러 컨테이너를 효율적으로 관리하는 방법을 알아보겠습니다.