도커 기초 강좌 #6 .dockerignore와 빌드 컨텍스트 — 캐시 잘 쓰기
도커 기초 시리즈의 마지막 글입니다. 지금까지 작은 앱을 컨테이너화하고, 실행하고, 데이터를 살리고, 레지스트리에 올렸습니다. 이번 글은 한 주제만 깊게 — 이미지가 왜 비대해지고 빌드가 왜 느려지는가, 그리고 그 두 문제를 푸는 두 도구 .dockerignore와 레이어 캐시 입니다.
도커 기초 강좌 시리즈에서 이번 글의 위치:
- #1 컨테이너란
- #2 Dockerfile 첫 작성
- #3 이미지와 컨테이너
- #4 볼륨과 네트워크
- #5 레지스트리 — Docker Hub, GHCR, push/pull
- #6
.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 . .같은 명령으로 이미지에 그대로 박힐 수도 있음
여기서 두 가지를 분리해 두어야 합니다.
- 컨텍스트로 보내지는 것 — 도커 데몬이 받는 모든 파일
- 이미지에 들어가는 것 —
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 라는 파일을 만들고, 빌드 컨텍스트에서 빼고 싶은 패턴을 적습니다.
# 버전 관리
.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
.dockerignoreDockerfile 자체와 .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도 같은 패턴 #
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편을 통해 만든 흐름을 한곳에 모아둡니다.
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"].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 Compose —
web + db + cache를 한 파일로 정의 / 동시 기동 - healthcheck, depends_on, profiles — Compose의 운영 기능
- 빌드 캐시 깊이 — BuildKit, mount cache —
pip install/npm ci캐시를 빌드 간에 공유 - 로깅과 디버깅 — 여러 컨테이너의 로그를 한곳에서 보기
- 환경변수와 secrets — 비밀 값을 이미지에 박지 않고 다루는 패턴
기초에서 만든 한 컨테이너 위에, 운영 환경에 한 발 더 다가가는 내용입니다. 이번 시리즈에서 잡은 명령과 개념(레이어, 캐시, 볼륨, 네트워크) 위에 그대로 쌓입니다.
정리 #
기초 시리즈 6편을 한 줄씩 정리하면:
- #1 — 컨테이너 vs VM, 도커 생태계의 뼈대
- #2 —
FROM / RUN / COPY / CMD로 첫 Dockerfile - #3 —
build / run / ps / logs / exec / stop / rm의 일상 명령군 - #4 — bind mount / named volume, 사용자 정의 bridge 네트워크
- #5 — Docker Hub / GHCR,
tag / push / pull, 다이제스트 - #6 —
.dockerignore, 빌드 컨텍스트, 레이어 캐시 순서
도커 트랙은 4 시리즈입니다. 다음 시리즈는 도커 중급 — Compose와 멀티스테이지 빌드로 들어갑니다.