Dockerfile & Image Build

Dockerfile 작성, 이미지 빌드, 레이어 구조


📚 시리즈 네비게이션

이전현재다음
Docker BasicsDockerfile & ImageVolume & Network

시리즈 목차


🎯 Dockerfile이란?

이미지를 만들기 위한 명령어 스크립트.

# Dockerfile 예시
FROM nginx:latest
COPY index.html /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# 빌드
docker build -t my-nginx .
 
# 실행
docker run -d -p 8080:80 my-nginx

🏗️ 이미지 레이어 구조

Docker 이미지는 레이어 스택으로 구성됨.

flowchart TB
    subgraph Layers["Image Layers"]
        L4["Layer 4: CMD (실행 명령)"]
        L3["Layer 3: COPY index.html"]
        L2["Layer 2: EXPOSE 80"]
        L1["Layer 1: FROM nginx:latest<br/>(베이스 이미지)"]
        
        L4 --> L3 --> L2 --> L1
    end

특징:

  • 각 명령어가 새 레이어 생성
  • 레이어는 캐시됨 (변경 없으면 재사용)
  • 읽기 전용 (컨테이너 실행 시 쓰기 레이어 추가)

📝 Dockerfile 명령어

FROM

베이스 이미지 지정. 반드시 첫 줄에.

# 공식 이미지
FROM ubuntu:22.04
FROM node:18-alpine
FROM python:3.11-slim
 
# scratch (빈 이미지, Go 바이너리 등)
FROM scratch

이미지 선택 팁:

태그설명용량
latest최신 버전
alpineAlpine Linux 기반작음
slim불필요한 패키지 제거중간
bullseyeDebian 버전명

WORKDIR

작업 디렉토리 설정. (없으면 생성)

WORKDIR /app
 
# 이후 명령어는 /app에서 실행
COPY . .
RUN npm install

COPY vs ADD

# COPY: 로컬 파일 복사 (권장)
COPY package.json .
COPY src/ /app/src/
 
# ADD: COPY + 추가 기능 (URL, 압축 해제)
ADD https://example.com/file.tar.gz /app/
ADD archive.tar.gz /app/    # 자동 압축 해제

권장: 대부분 COPY 사용. ADD는 압축 해제 필요할 때만.

RUN

빌드 시 명령어 실행.

# 셸 형식
RUN apt-get update && apt-get install -y nginx
 
# exec 형식
RUN ["apt-get", "update"]
 
# 여러 명령어 (레이어 최소화)
RUN apt-get update && \
    apt-get install -y \
        nginx \
        vim \
        curl && \
    rm -rf /var/lib/apt/lists/*

CMD vs ENTRYPOINT

항목CMDENTRYPOINT
용도기본 명령어고정 명령어
docker run 인자덮어씀인자로 추가됨
여러 개 선언마지막만 유효마지막만 유효
# CMD: 기본 명령어 (덮어쓰기 가능)
CMD ["nginx", "-g", "daemon off;"]
 
# docker run my-nginx              → nginx -g daemon off;
# docker run my-nginx cat /etc/os  → cat /etc/os (CMD 무시)
# ENTRYPOINT: 고정 명령어
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
 
# docker run my-app                 → python app.py --port 8080
# docker run my-app --port 9000     → python app.py --port 9000

조합 패턴:

# 실행 파일 고정, 인자만 변경 가능
ENTRYPOINT ["python", "app.py"]
CMD ["--help"]
 
# docker run my-app           → python app.py --help
# docker run my-app --debug   → python app.py --debug

ENV

환경 변수 설정.

ENV NODE_ENV=production
ENV APP_HOME=/app \
    APP_PORT=3000
 
# 사용
WORKDIR $APP_HOME
EXPOSE $APP_PORT

ARG

빌드 시 인자. (이미지에 남지 않음)

ARG VERSION=latest
ARG BUILD_DATE
 
FROM node:${VERSION}
RUN echo "Build date: ${BUILD_DATE}"
docker build --build-arg VERSION=18 --build-arg BUILD_DATE=$(date) .
항목ARGENV
사용 시점빌드 시빌드 + 런타임
이미지에 저장
컨테이너에서 접근

EXPOSE

문서화 용도의 포트 선언. (실제 포트 오픈 아님)

EXPOSE 80
EXPOSE 443
EXPOSE 3000/tcp
EXPOSE 53/udp

실제 포트 매핑은 docker run -p로 해야 함.

USER

실행 사용자 변경.

# 사용자 생성
RUN useradd -r -u 1001 appuser
 
# 사용자 변경
USER appuser
 
# 이후 명령어는 appuser로 실행

VOLUME

볼륨 마운트 포인트 선언.

VOLUME /data
VOLUME ["/data", "/logs"]

HEALTHCHECK

컨테이너 상태 체크.

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost/ || exit 1

🎯 실습: Node.js 앱

프로젝트 구조

my-node-app/
├── Dockerfile
├── package.json
├── package-lock.json
└── src/
    └── index.js

package.json

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

src/index.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
 
app.get('/', (req, res) => {
  res.json({ message: 'Hello Docker!', timestamp: new Date() });
});
 
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Dockerfile

# 베이스 이미지
FROM node:18-alpine
 
# 작업 디렉토리
WORKDIR /app
 
# 의존성 파일 먼저 복사 (캐시 활용)
COPY package*.json ./
 
# 의존성 설치
RUN npm ci --only=production
 
# 소스 복사
COPY src/ ./src/
 
# 포트 문서화
EXPOSE 3000
 
# 실행
CMD ["npm", "start"]

빌드 및 실행

# 빌드
docker build -t my-node-app .
 
# 실행
docker run -d -p 3000:3000 --name app my-node-app
 
# 확인
curl http://localhost:3000
 
# 로그
docker logs app
 
# 정리
docker rm -f app

🎯 실습: Python Flask 앱

Dockerfile

FROM python:3.11-slim
 
WORKDIR /app
 
# 의존성 먼저
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
# 소스 복사
COPY app.py .
 
EXPOSE 5000
 
CMD ["python", "app.py"]

requirements.txt

flask==3.0.0

app.py

from flask import Flask, jsonify
import os
 
app = Flask(__name__)
 
@app.route('/')
def hello():
    return jsonify(message='Hello Docker!', host=os.uname().nodename)
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

⚡ 이미지 최적화

1. 레이어 최소화

# 나쁜 예 (레이어 3개)
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
 
# 좋은 예 (레이어 1개)
RUN apt-get update && \
    apt-get install -y nginx && \
    rm -rf /var/lib/apt/lists/*

2. 캐시 활용

# 나쁜 예: 소스 변경 시 npm install 다시 실행
COPY . .
RUN npm install
 
# 좋은 예: package.json 변경 없으면 캐시 사용
COPY package*.json ./
RUN npm install
COPY . .

3. 작은 베이스 이미지

# 큰 이미지 (~900MB)
FROM node:18
 
# 작은 이미지 (~170MB)
FROM node:18-alpine
 
# 더 작게 (~120MB)
FROM node:18-alpine
RUN npm ci --only=production

4. 멀티스테이지 빌드

빌드 도구는 최종 이미지에 불필요함.

flowchart LR
    subgraph Build["빌드 스테이지"]
        B1["FROM node:18-alpine AS builder"]
        B2["npm ci"]
        B3["npm run build"]
        B1 --> B2 --> B3
    end
    
    subgraph Run["실행 스테이지"]
        R1["FROM nginx:alpine"]
        R2["COPY --from=builder<br/>/app/dist"]
        R1 --> R2
    end
    
    Build -->|"빌드 결과만 복사"| Run
# 빌드 스테이지
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# 실행 스테이지 (빌드 결과만 복사)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Go 예시:

# 빌드
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
 
# 실행 (scratch = 빈 이미지)
FROM scratch
COPY --from=builder /app/main /main
CMD ["/main"]

5. .dockerignore

불필요한 파일 제외.

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
*.md
.env
.env.*

🏷️ 이미지 태깅 & 푸시

태깅

# 빌드 시 태그
docker build -t myapp:1.0.0 .
docker build -t myapp:latest .
 
# 기존 이미지에 태그 추가
docker tag myapp:1.0.0 myapp:stable
docker tag myapp:1.0.0 myregistry.com/myapp:1.0.0

Docker Hub 푸시

# 로그인
docker login
 
# 태그 (username 포함)
docker tag myapp:1.0.0 username/myapp:1.0.0
 
# 푸시
docker push username/myapp:1.0.0

프라이빗 레지스트리

# AWS ECR
aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.ap-northeast-2.amazonaws.com
docker tag myapp:1.0.0 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0.0
docker push 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0.0
 
# Harbor, GitLab Registry 등도 유사

📋 Dockerfile 체크리스트

항목확인
작은 베이스 이미지 사용 (alpine, slim)
.dockerignore 설정
의존성 파일 먼저 COPY (캐시 활용)
RUN 명령어 합치기 (레이어 최소화)
불필요한 파일 삭제 (apt cache 등)
멀티스테이지 빌드 고려
non-root 사용자 사용
HEALTHCHECK 설정

🔗 시리즈 네비게이션

시리즈 목차로 돌아가기


🔗 참고 자료