도커 첫걸음 4편 — Dockerfile과 docker build: 내 앱을 이미지로 만들고, 빠르게, 맥과 서버 모두에서
남이 만든 이미지를 실행하는 데서 한 걸음 더 — 내 파이썬 웹 앱을 이미지로 만듭니다. FROM·WORKDIR·COPY·RUN·EXPOSE·CMD 여섯 줄로 된 Dockerfile을 한 줄씩 읽고, 빌드 컨텍스트와 .dockerignore, 태그 붙이는 법을 익힙니다. 코드 한 줄 고쳤는데 90초씩 기다리는 이유(레이어 캐시)를 시뮬레이터로 확인하고, 이미지를 작게 만드는 slim 베이스와 멀티 스테이지 빌드, 그리고 Apple 실리콘 맥에서 만든 이미지가 x86 서버에서 안 도는 문제를 buildx 멀티 플랫폼 빌드로 해결하는 법까지 다룹니다.
3편에서는 PostgreSQL, nginx, 주피터처럼 이미 누가 만들어 둔 이미지를 실행했습니다. 그런데 우리 팀이 만든 웹 앱은 Docker Hub에 없습니다. 팀원이 한 줄로 띄우게 하려면 우리가 직접 이미지를 만들어야 합니다. 그 레시피가 Dockerfile이고, 레시피로 밀키트를 만드는 명령이 docker build입니다.
RUN과 CMD의 차이가 가장 헷갈립니다.RUN은 빌드할 때 한 번 실행되어 그 결과(설치된 라이브러리)가 이미지에 굳습니다. CMD는 이미지에 "나중에 이걸 실행해"라고 적어만 두고, docker run 할 때마다 실행됩니다. 라이브러리 설치는 RUN, 서버 시작은 CMD입니다.
🔑
--host 0.0.0.0이 없으면 접속이 안 됩니다. 많은 개발 서버(uvicorn, Flask, Vite, Next.js dev 등)는 기본으로 127.0.0.1(자기 자신)에서만 요청을 받습니다. 컨테이너 안에서 "자기 자신"은 컨테이너뿐이라, -p로 통로를 뚫어도 바깥 요청을 거절합니다. 컨테이너 안 서버는 반드시 0.0.0.0에서 듣게 하세요. "run은 되는데 브라우저가 안 열려요"의 두 번째 원인입니다(첫 번째는 -p 누락).
3. 빌드하고 실행하기
bash
docker build -t hello-api:0.1 .
-t hello-api:0.1 — 만들어질 이미지의 이름과 태그(버전).
마지막의 . — 빌드 컨텍스트. "현재 폴더의 파일들을 재료로 쓰라"는 뜻입니다. 이 점을 빼먹는 실수가 아주 흔합니다.
빌드가 끝나면 이미지가 생겼는지 보고, 3편에서 배운 대로 실행합니다.
bash
docker images hello-api
docker run -d --name api -p 8000:8000 hello-api:0.1
curl http://localhost:8000
{"message":"도커 안에서 인사합니다"}가 나오면 성공입니다. 브라우저에서 http://localhost:8000/docs를 열면 FastAPI가 만들어 주는 API 문서도 볼 수 있습니다. 내 컴퓨터에는 파이썬도 FastAPI도 설치하지 않았는데 말이죠.
코드를 고친 뒤에는 다시 빌드하고, 옛 컨테이너를 지우고, 새로 띄웁니다.
bash
docker build -t hello-api:0.2 .
docker rm -f api
docker run -d --name api -p 8000:8000 hello-api:0.2
💡
매번 이 세 줄을 치는 게 번거롭다면 정상입니다. 개발 중에는 5편의 Docker Compose가 이걸 docker compose up --build 한 줄로 줄여 주고, 소스 폴더를 볼륨으로 연결해 재빌드 없이 코드 변경이 바로 반영되게 할 수도 있습니다.
4. .dockerignore — 넣지 말아야 할 것 빼기
COPY . .는 폴더 안의 모든 것을 복사합니다. .git 폴더, 가상환경(.venv), node_modules, 그리고 무엇보다 비밀번호가 든 .env 파일까지요. 이미지가 불필요하게 커지고, 비밀이 이미지에 박혀 레지스트리로 퍼질 수 있습니다. .gitignore처럼 .dockerignore 파일로 막습니다.
비밀은 이미지에 넣지 않습니다. API 키나 DB 비밀번호를 Dockerfile의 ENV에 적거나 .env를 COPY하면, 이미지를 받은 누구나 docker history나 docker inspect로 꺼내 볼 수 있습니다. 비밀은 실행할 때-e나 Compose의 env_file로 넣습니다. 이미지는 공유되는 것이고, 비밀은 공유되면 안 되는 것입니다.
5. 레이어와 캐시 — 코드 한 줄에 90초를 기다리는 이유
도커 이미지는 Dockerfile의 줄마다 레이어(층)가 하나씩 쌓인 구조입니다. 도커는 빌드할 때 각 줄의 입력이 지난번과 같으면 새로 만들지 않고 이전 결과를 재사용합니다. 이게 빌드 캐시입니다. 두 번째 빌드가 순식간에 끝나는 이유죠.
규칙이 하나 있습니다. 어떤 줄의 입력이 바뀌면 그 줄과 그 뒤의 모든 줄이 다시 실행됩니다. 그래서 줄 순서가 중요합니다. 위 Dockerfile이 requirements.txt를 먼저 복사하고 설치한 뒤 소스 코드를 나중에 복사한 이유가 이것입니다. 순서를 바꾸면 어떻게 되는지 직접 비교해 보세요.
📐
캐시 친화적 순서의 원칙 — 잘 안 바뀌는 것을 위로, 자주 바뀌는 것을 아래로.
· 파이썬: requirements.txt(또는 pyproject.toml·uv.lock) COPY → 설치 → 소스 COPY
· Node: package.json·package-lock.json COPY → npm ci → 소스 COPY
· 시스템 패키지(apt-get install)는 그보다도 위에
6. 이미지를 작게 — slim 베이스와 멀티 스테이지
이미지가 작으면 받는 시간도, 저장 공간도, 보안 취약점이 숨을 곳도 줄어듭니다. 크기는 이렇게 확인합니다.
bash
docker images
① 베이스 이미지 고르기. 같은 파이썬 3.13이라도 태그에 따라 크기가 크게 다릅니다.
태그
특징
언제 쓰나
python:3.13
컴파일러 등 개발 도구가 다 든 전체판. 가장 크다 (1GB 안팎)
C 확장을 빌드해야 하는데 뭘 설치할지 모를 때
python:3.13-slim
필수만 남긴 데비안. 수백 MB 대신 백몇십 MB 수준
대부분의 경우 기본 추천
python:3.13-alpine
가장 작다. 대신 glibc가 아닌 musl 기반
일부 파이썬 패키지(과학 계산 등)의 설치가 까다로워 초보자에겐 비추천
② 멀티 스테이지 빌드. 빌드할 때만 필요한 도구(컴파일러, npm 개발 의존성)를 최종 이미지에서 빼는 방법입니다. 프런트엔드 앱이 대표적입니다. Node로 빌드해서 나온 정적 파일만 nginx 이미지에 옮겨 담습니다.
dockerfile
# 1단계: 빌드 전용 (최종 이미지에 안 들어감)
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 2단계: 실행용 — 빌드 결과물만 복사
FROM nginx:1.29-alpine
COPY --from=build /app/dist /usr/share/nginx/html
node_modules 수백 MB와 Node 런타임은 1단계에만 있고, 최종 이미지에는 nginx와 빌드된 HTML·JS만 남습니다. 결과 이미지는 수십 MB 수준이 됩니다.
7. Apple 실리콘 맥 ↔ x86 서버 — 협업에서 가장 흔한 함정
팀에서 도커를 쓰기 시작하면 거의 반드시 만나는 문제입니다.
💥
증상
M 시리즈 맥에서 빌드해 올린 이미지를 회사 서버(인텔·AMD)에서 실행하니 exec format error. 반대로 맥에서 x86 전용 이미지를 받으면 no matching manifest for linux/arm64.
🔧
원인
이미지 안의 프로그램은 CPU 종류(아키텍처)에 맞게 만들어진다. Apple 실리콘 맥은 arm64, 대부분의 서버와 윈도우 PC는 amd64(x86-64). 도커는 기본적으로 빌드한 컴퓨터의 아키텍처로 이미지를 만든다.
✅
해결
① 서버용이면 --platform linux/amd64로 빌드 ② 팀 공용이면 두 아키텍처를 한 태그에 담는 멀티 플랫폼 빌드 ③ 가장 좋은 건 CI(GitHub Actions 등)에서 빌드하게 하는 것 (5편)
--push가 필요한 이유는, 여러 아키텍처가 섞인 이미지는 로컬 이미지 목록에 한 번에 담기 어려워 레지스트리로 곧장 보내는 것이 표준이기 때문입니다. 레지스트리 로그인과 공유는 5편에서 다룹니다.
💡
남의 이미지를 x86로 강제 실행하기. 맥에서 arm64 버전이 없는 오래된 이미지를 써야 한다면 docker run --platform linux/amd64 이미지이름으로 실행합니다. 에뮬레이션이라 느리지만 대부분 동작합니다. 공식 이미지는 요즘 거의 다 두 아키텍처를 모두 제공합니다.
8. 태그 붙이는 습관
협업에서 태그는 "어떤 버전의 이미지인가"를 가리키는 주소입니다.
태그 방식
예시
장단점
latest
hello-api:latest
편하지만 가리키는 대상이 계속 바뀐다. "어제 되던 게 오늘 안 돼요"의 원인
시맨틱 버전
hello-api:1.4.2
사람이 읽기 쉽고 배포·롤백 기준으로 좋다
git 커밋 해시
hello-api:3f9a2c1
어떤 코드로 만든 이미지인지 정확히 추적된다. CI에서 자동으로 붙이기 좋다
이미지 하나에 태그를 여러 개 붙일 수 있습니다. 보통 버전과 커밋 해시를 함께 붙입니다.
bash
docker tag hello-api:0.2 hello-api:$(git rev-parse --short HEAD)
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home app
USER app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
마치며
🧠
4편 요약
· Dockerfile 기본 여섯 줄: FROM → WORKDIR → COPY(의존성 목록) → RUN(설치) → COPY(소스) → CMD
· docker build -t 이름:태그 . — 마지막 점(빌드 컨텍스트)을 잊지 말 것
· 컨테이너 안 서버는 0.0.0.0에서 듣게, 비밀은 이미지에 넣지 말고 실행할 때
· 잘 안 바뀌는 것은 위로 — 캐시가 빌드 시간을 90초에서 1초로 줄인다
· Apple 실리콘 맥은 arm64, 서버는 amd64 — --platform 또는 buildx 멀티 플랫폼
이제 이미지를 만들 수 있습니다. 하지만 실제 앱은 혼자 돌지 않습니다. API 서버는 DB가 필요하고, 캐시가 필요하고, 팀원도 똑같이 띄울 수 있어야 합니다. 마지막 편에서는 여러 컨테이너를 파일 하나로 묶는 Docker Compose와, 이미지를 레지스트리로 공유하는 협업 흐름을 완성합니다.