도커 기초 강좌 #6 .dockerignore와 빌드 컨텍스트 — 캐시 잘 쓰기

7 분 소요

도커 기초 시리즈의 마지막 글입니다. 지금까지 작은 앱을 컨테이너화하고, 실행하고, 데이터를 살리고, 레지스트리에 올렸습니다. 이번 글은 한 주제만 깊게 — 이미지가 왜 비대해지고 빌드가 왜 느려지는가, 그리고 그 두 문제를 푸는 두 도구 .dockerignore레이어 캐시 입니다.

도커 기초 강좌 시리즈에서 이번 글의 위치:

빌드 컨텍스트 — 도커가 빌드 시작 전에 하는 일 #

docker build -t myapp .을 칠 때, 마지막의 점(.) 이 빌드 컨텍스트입니다. 도커는 빌드를 시작하기 전에 이 디렉터리 전체를 통째로 데몬에게 보내요.

빌드 컨텍스트의 흐름
host                              docker daemon
┌──────────────────┐              ┌─────────────────┐
│  ./hello-docker  │              │                 │
│  ├ app.py        │  tar 로 묶어 │                 │
│  ├ requirements  │ ───────────▶ │   build         │
│  ├ Dockerfile    │              │                 │
│  ├ ...           │              │                 │
│  └ .git/         │              └─────────────────┘
└──────────────────┘

빌드 출력에서 자주 보는 이 줄이 그 단계입니다.

빌드 출력의 한 줄
=> [internal] load build context           500ms
=> => transferring context: 152MB

이 152MB가 클수록:

  • 빌드가 시작되기까지 시간이 걸림
  • 도커 데몬이 메모리 / 디스크에 임시로 들고 있어야 함
  • 변경 감지(캐시 키 계산)가 더 들어감
  • 무엇보다 — 이 안에 들어간 파일이 COPY . . 같은 명령으로 이미지에 그대로 박힐 수도 있음

여기서 두 가지를 분리해 두어야 합니다.

  1. 컨텍스트로 보내지는 것 — 도커 데몬이 받는 모든 파일
  2. 이미지에 들어가는 것COPY / ADD로 명시적으로 가져온 것만

.dockerignore는 이 중 첫 단계를 깎습니다.

.git, node_modules, .venv — 무엇을 빼야 하나 #

작은 디렉터리도 한 번 보면 놀라요.

컨텍스트 크기 보기
du -sh .
# 152M    .

du -sh .git node_modules .venv build dist *.log
# 80M     .git
# 60M     node_modules     # 컨테이너 안에서 다시 깔 거임
# 8M      .venv            # 마찬가지
# 4M      build
# 1M      dist
# 200K    *.log

이 중 컨테이너에 필요한 건 거의 없습니다. 의존성은 RUN pip install / RUN npm ci가 컨테이너 안에서 다시 깔 테니, 호스트의 node_modules, .venv는 보낼 이유가 0입니다. .git도 보통 빠집니다 (빌드에 git 정보가 필요하면 --build-arg GIT_SHA=...로 명시적으로 주입하는 편이 깔끔).

.dockerignore 작성 #

.gitignore와 거의 같은 문법입니다. Dockerfile 옆에 .dockerignore 라는 파일을 만들고, 빌드 컨텍스트에서 빼고 싶은 패턴을 적습니다.

.dockerignore (파이썬/노드 공통 베이스)
# 버전 관리
.git
.gitignore

# 환경 / 비밀
.env
.env.*
*.key
*.pem

# 빌드 산출물
dist/
build/
out/
*.egg-info/

# 의존성 (컨테이너 안에서 다시 깔림)
node_modules/
.venv/
__pycache__/
*.pyc

# 에디터 / OS
.vscode/
.idea/
.DS_Store
Thumbs.db

# 로그
*.log
logs/

# 테스트 / 캐시
.pytest_cache/
.mypy_cache/
.ruff_cache/
coverage/
.coverage
htmlcov/

# 컨테이너 자체 파일
Dockerfile.dev
docker-compose*.yml
.dockerignore

Dockerfile 자체와 .dockerignore도 컨텍스트에 들어갑니다 — 굳이 이미지 안에 박을 이유가 없으니 무시 패턴에 넣어 두는 편이 단정합니다.

매칭 문법 한 번에 #

패턴의미
node_modules어느 위치든 이 이름의 파일/디렉터리
node_modules/디렉터리만
*.log모든 .log
**/*.log모든 깊이의 .log (도커는 기본이 재귀라 *.log와 사실상 동일)
dist/**dist 안의 모든 것
!important.log위 규칙들 중 이 파일은 예외로 포함

! 예외 규칙은 강력하지만 실수가 잦습니다. 단순한 무시 위주로 적는 게 안전합니다.

효과 확인 #

추가 전후로 컨텍스트 크기가 얼마나 줄었는지 빌드 출력에서 바로 보입니다.

추가 전
=> [internal] load build context           1.2s
=> => transferring context: 152MB

# .dockerignore 추가 후
=> [internal] load build context           120ms
=> => transferring context: 240kB

체감 차이가 큽니다. CI가 매번 컨텍스트를 빌드 머신에 끌어다 넣는다고 생각하면 분명한 절약입니다.

레이어 캐시 — 빌드를 빠르게 #

도커 빌드의 두 번째 핵심은 캐시입니다. Dockerfile의 한 줄(== 레이어 한 장) 마다 도커는 이런 판단을 합니다.

이 명령의 입력(이전 레이어 + 명령 자체 + 명령이 참조하는 파일들)이 마지막 빌드와 같은가? → 같으면 캐시 재사용, 다르면 이 레이어부터 다시 빌드.

이 모델이 빌드 속도에 결정적입니다. 그리고 — 한 레이어가 깨지면 그 아래 모든 레이어가 함께 깨져요. (위에서 아래로 누적되니까.)

캐시가 깨지는 지점 #

잘못된 순서 — 캐시가 자주 깨짐
FROM python:3.14-slim
WORKDIR /app

COPY . .                              # 코드와 의존성 명세가 같이 옴
RUN pip install -r requirements.txt   # 코드 한 글자만 바뀌어도 위가 깨져 다시 설치해야 함

이 Dockerfile은 코드 한 줄만 바꿔도 의존성을 처음부터 다시 설치합니다. COPY . .의 결과가 바뀌니 다음 RUN의 캐시 키도 바뀌기 때문입니다. 의존성이 큰 프로젝트라면 빌드가 매번 분 단위로 길어져요.

올바른 순서 — 의존성과 코드를 분리 #

권장 순서
FROM python:3.14-slim
WORKDIR /app

# 1) 의존성 명세만 먼저 복사
COPY requirements.txt .
# 2) 의존성 설치 — requirements.txt 가 안 바뀌면 캐시 재사용
RUN pip install --no-cache-dir -r requirements.txt

# 3) 그 다음에 코드 복사
COPY . .

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

이제 app.py만 바꾸면:

  • COPY requirements.txt . → 캐시 재사용
  • RUN pip install ...캐시 재사용 (의존성 설치를 건너뜀)
  • COPY . . → 다시 실행
  • 빌드가 수 초 안에 끝남

바뀌지 않는 것을 위에, 자주 바뀌는 것을 아래에 — 이게 Dockerfile 작성의 첫 번째 휴리스틱입니다. 베이스 이미지(거의 안 바뀜) → 시스템 의존성 → 언어 의존성 → 코드(자주 바뀜) 순서.

Node.js도 같은 패턴 #

Node 의존성 캐시
FROM node:20-slim
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
CMD ["node", "server.js"]

package.json / package-lock.json만 먼저 복사 → npm ci로 깔고 → 그 다음 코드. 같은 패턴입니다.

빌드 캐시의 부산물 — 이미지 크기 #

레이어 캐시는 빌드 속도뿐 아니라 이미지 크기 에도 영향을 줍니다. 한 레이어가 디스크에 굳혀지면, 그 안에서 만든 임시 파일도 같이 굳어 버립니다.

흔한 실수 — 임시 파일이 레이어에 박힘
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

이 세 줄은 세 개의 레이어가 됩니다. apt-get update가 만든 인덱스가 첫 레이어에 들어가고, 그 다음 apt-get clean은 다른 레이어에서 지워봤자 이미지 안엔 그대로 남습니다.

같은 일을 한 줄로 묶으면 한 레이어가 됩니다.

권장 패턴
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

&&로 묶고, 마지막에 캐시 디렉터리를 지워 한 레이어 안에서 청소까지 끝냅니다. 이미지 크기 차이가 분명합니다.

분리한 경우묶은 경우
이미지 크기~180MB~85MB

이 패턴은 거의 모든 베이스 이미지의 공식 Dockerfile에서 볼 수 있습니다.

--no-cache — 강제로 새로 빌드 #

캐시가 의도치 않게 옛 결과를 잡고 있을 때:

캐시 무시 빌드
docker build --no-cache -t myapp .

또는 베이스 이미지 자체를 새로 받고 싶을 때:

베이스도 새로 가져오기
docker build --pull -t myapp .
docker build --pull --no-cache -t myapp .   # 둘 다

apt-get update로 받은 인덱스가 오래돼 보안 패치를 못 받는 일이 생깁니다. CI의 정기 빌드에선 가끔 --no-cache 또는 --pull을 강제하는 옵션을 둬요.

시리즈를 마무리하며 — 한 컨테이너의 한 사이클 #

기초 시리즈 6편을 통해 만든 흐름을 한곳에 모아둡니다.

한곳에 모은 Dockerfile
FROM python:3.14-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

# 시스템 의존성 (한 레이어, 청소까지)
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 언어 의존성 (자주 안 바뀜 — 위로)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 코드 (자주 바뀜 — 아래로)
COPY app.py .

EXPOSE 8000
CMD ["python", "app.py"]
.dockerignore
.git
.env
.env.*
node_modules/
.venv/
__pycache__/
*.pyc
*.log
.pytest_cache/
.mypy_cache/
.ruff_cache/
.DS_Store
Dockerfile.dev
docker-compose*.yml
빌드 → 실행 → 푸시
# 1) 빌드 (CI가 자주 칠 명령)
docker build -t ghcr.io/curtis/myapp:1.0.0 -t ghcr.io/curtis/myapp:latest .

# 2) 로컬 확인
docker run --rm -p 8000:8000 ghcr.io/curtis/myapp:1.0.0

# 3) 운영용 데몬 모드 + 영속 데이터 + 자동 재시작
docker run -d --name myapp \
  --restart unless-stopped \
  --network mynet \
  -p 127.0.0.1:8000:8000 \
  -v myapp-data:/app/data \
  -e DB_HOST=pg \
  ghcr.io/curtis/myapp:1.0.0

# 4) 다른 머신에서 받기
docker pull ghcr.io/curtis/myapp:1.0.0

이 흐름이 손에 붙으면 — 한 컨테이너를 정의 → 빌드 → 운영 → 배포까지 한 흐름으로 이어갈 수 있습니다. 이게 도커 기초 시리즈의 도착점 입니다.

다음으로 — 도커 중급으로 #

기초 시리즈는 한 컨테이너를 잘 만들고 실행하는 단계까지였습니다. 다음 시리즈는 여러 컨테이너 + 더 깊은 빌드로 한 발 더 들어갑니다. 다룰 주제들:

  • 멀티스테이지 빌드 — 빌드 의존성과 런타임 의존성을 분리해 이미지 슬리밍
  • Docker Composeweb + db + cache를 한 파일로 정의 / 동시 기동
  • healthcheck, depends_on, profiles — Compose의 운영 기능
  • 빌드 캐시 깊이 — BuildKit, mount cachepip install / npm ci 캐시를 빌드 간에 공유
  • 로깅과 디버깅 — 여러 컨테이너의 로그를 한곳에서 보기
  • 환경변수와 secrets — 비밀 값을 이미지에 박지 않고 다루는 패턴

기초에서 만든 한 컨테이너 위에, 운영 환경에 한 발 더 다가가는 내용입니다. 이번 시리즈에서 잡은 명령과 개념(레이어, 캐시, 볼륨, 네트워크) 위에 그대로 쌓입니다.

정리 #

기초 시리즈 6편을 한 줄씩 정리하면:

  • #1 — 컨테이너 vs VM, 도커 생태계의 뼈대
  • #2FROM / RUN / COPY / CMD로 첫 Dockerfile
  • #3build / run / ps / logs / exec / stop / rm의 일상 명령군
  • #4 — bind mount / named volume, 사용자 정의 bridge 네트워크
  • #5 — Docker Hub / GHCR, tag / push / pull, 다이제스트
  • #6 — .dockerignore, 빌드 컨텍스트, 레이어 캐시 순서

도커 트랙은 4 시리즈입니다. 다음 시리즈는 도커 중급 — Compose와 멀티스테이지 빌드로 들어갑니다.

X