포스트

[이제와서 시작하는 Docker 마스터하기 - 고급편 #5] Docker 이미지 최적화

[이제와서 시작하는 Docker 마스터하기 - 고급편 #5] Docker 이미지 최적화

“이제와서 시작하는 Docker 마스터하기” Docker 이미지의 크기와 빌드 시간은 배포 속도와 비용에 직접적인 영향을 미칩니다. 이번 편에서는 이미지를 최적화하는 다양한 기법을 실습과 함께 알아보겠습니다.

이미지 크기가 중요한 이유

  1. 빠른 배포: 작은 이미지는 다운로드가 빠릅니다
  2. 저장 공간 절약: 레지스트리와 호스트의 디스크 공간 절약
  3. 보안: 불필요한 패키지가 없어 공격 표면이 줄어듭니다
  4. 성능: 컨테이너 시작 시간이 단축됩니다

현재 이미지 분석하기

1. 이미지 크기 확인

1
2
3
4
5
6
7
8
# 이미지 크기 확인
docker images

# 상세 레이어 정보
docker history <image-name>

# 레이어별 크기 확인
docker history <image-name> --no-trunc --format "table {{.CreatedBy}}\t{{.Size}}"

2. Dive 도구로 분석

1
2
3
4
# Dive 설치 및 실행
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest <image-name>

기본 최적화 기법

1. 적절한 베이스 이미지 선택

1
2
3
4
5
6
7
8
9
10
11
12
# 나쁜 예: 큰 베이스 이미지
FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3

# 좋은 예: 특화된 이미지
FROM python:3.11-slim

# 더 좋은 예: Alpine 기반
FROM python:3.11-alpine

# 최고의 예: Distroless
FROM gcr.io/distroless/python3-debian11

2. 레이어 최소화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 나쁜 예: 많은 레이어
FROM alpine:3.18
RUN apk update
RUN apk add python3
RUN apk add py3-pip
RUN pip install flask
RUN pip install gunicorn

# 좋은 예: 하나의 레이어로 통합
FROM alpine:3.18
RUN apk update && \
    apk add --no-cache python3 py3-pip && \
    pip install --no-cache-dir flask gunicorn && \
    rm -rf /var/cache/apk/*

3. 불필요한 파일 제거

1
2
3
4
5
6
7
8
9
10
11
12
13
# 패키지 캐시 제거
RUN apt-get update && \
    apt-get install -y python3 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# pip 캐시 제거
RUN pip install --no-cache-dir -r requirements.txt

# 빌드 도구 제거
RUN apk add --no-cache --virtual .build-deps gcc musl-dev && \
    pip install --no-cache-dir -r requirements.txt && \
    apk del .build-deps

멀티 스테이지 빌드

1. Node.js 애플리케이션 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 빌드 스테이지
FROM node:18-alpine AS builder

WORKDIR /app

# 의존성만 먼저 복사 (캐시 활용)
COPY package*.json ./
RUN npm ci --only=production

# 개발 의존성 포함 설치 (빌드용)
COPY . .
RUN npm ci && npm run build

# 프로덕션 스테이지
FROM node:18-alpine

# dumb-init 설치 (PID 1 문제 해결)
RUN apk add --no-cache dumb-init

# 사용자 생성
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# 빌드 결과물만 복사
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --chown=nodejs:nodejs package.json ./

USER nodejs

EXPOSE 3000

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

2. Go 애플리케이션 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 빌드 스테이지
FROM golang:1.21-alpine AS builder

RUN apk add --no-cache ca-certificates git

WORKDIR /app

# 의존성 먼저 다운로드 (캐시 활용)
COPY go.mod go.sum ./
RUN go mod download

# 소스 코드 복사 및 빌드
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -o app .

# 실행 스테이지 (scratch)
FROM scratch

# CA 인증서 복사 (HTTPS 요청용)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 바이너리만 복사
COPY --from=builder /app/app /app

ENTRYPOINT ["/app"]

3. Java 애플리케이션 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 빌드 스테이지
FROM maven:3.8-openjdk-17 AS builder

WORKDIR /app

# 의존성 캐싱을 위한 별도 복사
COPY pom.xml .
RUN mvn dependency:go-offline

# 소스 코드 복사 및 빌드
COPY src ./src
RUN mvn package -DskipTests

# JRE 추출 (JLink)
RUN jlink \
    --add-modules java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=2 \
    --output /javaruntime

# 실행 스테이지
FROM debian:bullseye-slim

ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"

# JRE 복사
COPY --from=builder /javaruntime $JAVA_HOME

# 애플리케이션 JAR 복사
COPY --from=builder /app/target/*.jar app.jar

# 사용자 생성
RUN useradd -m -u 1001 appuser
USER appuser

ENTRYPOINT ["java", "-jar", "/app.jar"]

언어별 최적화 팁

Python 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 멀티 스테이지 빌드
FROM python:3.11-slim AS builder

# 빌드 의존성 설치
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# 가상 환경 생성
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 런타임 스테이지
FROM python:3.11-slim

# 가상 환경 복사
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

WORKDIR /app
COPY . .

CMD ["python", "app.py"]

Node.js 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM node:18-alpine

# node-prune 설치 (불필요한 파일 제거)
RUN apk add --no-cache curl && \
    curl -sf https://gobinaries.com/tj/node-prune | sh

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production && \
    node-prune && \
    rm -rf /usr/local/lib/node_modules/npm && \
    rm -rf ~/.npm

COPY . .

CMD ["node", "index.js"]

고급 최적화 기법

1. BuildKit 캐시 마운트

1
2
3
4
5
6
# syntax=docker/dockerfile:1
FROM python:3.11-slim

# 캐시 마운트 사용
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

2. 멀티 플랫폼 빌드

1
2
3
4
5
6
7
8
# BuildKit 활성화
docker buildx create --use

# 멀티 플랫폼 빌드
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:latest \
  --push .

3. 조건부 복사

1
2
3
4
5
6
7
8
# BuildKit 문법
# syntax=docker/dockerfile:1

FROM alpine

# 조건부 복사
ARG BUILD_ENV=production
COPY configs/${BUILD_ENV}.conf /etc/app/config.conf

이미지 크기 비교

실제 예시: Python Flask 앱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 최적화 전 (1.2GB)
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

# 최적화 후 (150MB)
FROM python:3.11-slim AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY app.py .
CMD ["python", "app.py"]

보안을 위한 최적화

1. 최소 권한 사용자

1
2
3
4
5
6
7
8
9
10
11
12
FROM alpine:3.18

# 사용자 생성
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# 필요한 디렉토리 생성 및 권한 설정
RUN mkdir -p /app && \
    chown -R appuser:appgroup /app

USER appuser
WORKDIR /app

2. 읽기 전용 파일시스템

1
2
3
4
5
6
7
FROM alpine:3.18

# 임시 디렉토리는 쓰기 가능하게
VOLUME ["/tmp", "/var/tmp"]

# 앱 실행
USER nobody

자동화된 최적화

1. CI/CD에서 이미지 스캔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# .github/workflows/docker.yml
name: Docker Build

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build image
        run: docker build -t myapp:$ .
      
      - name: Scan image size
        run: |
          SIZE=$(docker images myapp:${{ github.sha }} --format "{{.Size}}")
          echo "Image size: $SIZE"
          
          # 크기 제한 체크 (500MB)
          SIZE_MB=$(docker images myapp:${{ github.sha }} --format "{{.Size}}" | sed 's/MB//')
          if (( $(echo "$SIZE_MB > 500" | bc -l) )); then
            echo "Image too large!"
            exit 1
          fi

2. 이미지 최적화 도구

1
2
3
4
5
6
7
# docker-slim 사용
docker run -it --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  dslim/docker-slim build myapp:latest

# 결과 확인
docker images | grep myapp

실습: 이미지 크기 줄이기

최적화 전후 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# compare-images.sh

echo "=== 이미지 크기 비교 ==="

# 최적화 전 빌드
docker build -f Dockerfile.original -t myapp:original .
ORIGINAL_SIZE=$(docker images myapp:original --format "{{.Size}}")

# 최적화 후 빌드
docker build -f Dockerfile.optimized -t myapp:optimized .
OPTIMIZED_SIZE=$(docker images myapp:optimized --format "{{.Size}}")

echo "Original: $ORIGINAL_SIZE"
echo "Optimized: $OPTIMIZED_SIZE"

# 레이어 분석
echo -e "\n=== 레이어 분석 ==="
docker history myapp:optimized

체크리스트

이미지 최적화 체크리스트

  • 적절한 베이스 이미지 선택 (Alpine, Distroless)
  • 멀티 스테이지 빌드 사용
  • 레이어 수 최소화
  • 패키지 캐시 정리
  • 불필요한 파일 제거 (.git, tests, docs)
  • 빌드 도구 제거
  • 정적 링크 바이너리 사용 (Go, Rust)
  • 압축 가능한 파일 압축
  • .dockerignore 파일 활용
  • 캐시 마운트 사용

모범 사례 정리

1. 캐싱 최대화

1
2
3
4
# 변경이 적은 것부터 복사
COPY package*.json ./
RUN npm ci
COPY . .

2. 레이어 재사용

1
2
3
4
5
6
7
8
9
# 베이스 이미지를 공통으로 사용
ARG BASE_IMAGE=node:18-alpine
FROM ${BASE_IMAGE} AS base

FROM base AS development
# 개발 설정

FROM base AS production
# 프로덕션 설정

3. 빌드 시간 단축

1
2
3
4
5
6
7
8
9
10
11
# 병렬 빌드
# syntax=docker/dockerfile:1
FROM alpine AS build1
RUN command1

FROM alpine AS build2
RUN command2

FROM alpine
COPY --from=build1 /output1 .
COPY --from=build2 /output2 .

마무리

Docker 이미지 최적화는 지속적인 과정입니다. 작은 이미지는 빠른 배포, 낮은 비용, 향상된 보안을 가져다줍니다. 프로젝트의 특성에 맞는 최적화 전략을 선택하고 지속적으로 개선해 나가세요. 다음 편에서는 Docker와 CI/CD 통합에 대해 알아보겠습니다.

다음 편 예고

  • CI/CD 파이프라인 구축
  • 자동화된 빌드와 배포
  • 테스트 자동화
  • GitOps 실습

자동화된 배포 파이프라인을 구축해봅시다! 🚀

📚 Docker 마스터하기 시리즈

🐳 기초편 (입문자용 - 5편)

  1. Docker란 무엇인가?
  2. Docker 설치 및 환경 설정
  3. 첫 번째 컨테이너 실행하기
  4. Docker 이미지 이해하기
  5. Dockerfile 작성하기

💼 실전편 (중급자용 - 6편)

  1. Docker 네트워크 기초
  2. Docker 볼륨과 데이터 관리
  3. Docker Compose 입문
  4. 멀티 컨테이너 애플리케이션
  5. Docker Hub 활용하기
  6. Docker 보안 베스트 프랙티스

🚀 고급편 (전문가용 - 9편)

  1. Docker 로그와 모니터링
  2. Docker로 Node.js 애플리케이션 배포
  3. Docker로 Python 애플리케이션 배포
  4. Docker로 데이터베이스 운영
  5. Docker 이미지 최적화 ← 현재 글
  6. Docker와 CI/CD
  7. Docker Swarm 기초
  8. 문제 해결과 트러블슈팅
  9. Docker 생태계와 미래
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.