[도커 30강] 06강. Dockerfile 기초: FROM, RUN, COPY, CMD
도커를 어느 정도 써본 뒤 가장 먼저 부딪히는 벽이 있습니다. docker run으로 임시 테스트는 잘 되는데, 팀에 공유하거나 CI에서 자동 빌드를 하려면 결국 “내가 어떤 환경을 만들었는지”를 문서가 아니라 코드로 남겨야 한다는 점입니다. 그 역할을 하는 것이 Dockerfile입니다.
이번 강의에서는 Dockerfile의 가장 기본이자 핵심인 FROM, RUN, COPY, CMD를 중심으로, 단순 문법 소개가 아니라 왜 이 순서로 쓰는지, 어떤 실수를 줄여주는지, 실무에서 어떻게 재현성을 확보하는지까지 연결해서 설명하겠습니다. Dockerfile은 길게 쓰는 기술이 아니라, 의도를 분명히 쓰는 기술입니다.
핵심 개념
FROM은 이미지 빌드의 출발점입니다. 어떤 OS/런타임 위에서 시작할지 결정하므로, 보안/용량/호환성에 직접 영향을 줍니다.RUN은 이미지 빌드 시점에 실행되는 명령입니다. 패키지 설치, 디렉터리 생성, 권한 설정처럼 “이미지 내부 상태를 만드는 작업”에 사용합니다.COPY는 로컬의 파일을 이미지 안으로 가져옵니다. 애플리케이션 코드와 설정 파일을 포함시키는 단계이며, 캐시 전략과 빌드 속도에 큰 영향을 줍니다.CMD는 컨테이너 실행 시 기본으로 수행할 명령입니다. 빌드 단계에서 실행되는RUN과 완전히 다른 시점이라는 점을 명확히 구분해야 합니다.- Dockerfile은 위에서 아래로 읽히고 각 줄이 레이어가 됩니다. 그래서 같은 명령이라도 배치 순서가 빌드 시간, 캐시 효율, 운영 안정성에 차이를 만듭니다.
기본 사용
예제 1) 가장 작은 Dockerfile 구성
mkdir -p ~/docker100/lesson06-basic && cd ~/docker100/lesson06-basic
cat > Dockerfile <<'EOF'
FROM python:3.12-slim
RUN pip install --no-cache-dir flask==3.0.3
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
EOF
설명:
FROM python:3.12-slim은 파이썬 런타임이 포함된 가벼운 기반 이미지를 선택합니다.RUN에서 Flask를 설치하면, 해당 패키지가 이미지 안에 고정되어 이후 어디서 실행해도 같은 버전을 사용하게 됩니다.COPY는 호스트 파일을/app경로로 복사해 실행 대상 코드를 이미지에 포함합니다.CMD는 컨테이너가 시작될 때 실행할 기본 명령입니다.
확인 포인트:
- 이 Dockerfile은 “파이썬 런타임 + Flask + app.py 실행”이라는 의도를 그대로 코드화한 최소 단위입니다.
- README보다 Dockerfile이 더 신뢰할 수 있는 이유는, 실제 실행 가능한 형태로 환경 정의가 남기 때문입니다.
예제 2) 빌드와 실행을 통해 RUN/CMD 차이 확인
cat > app.py <<'EOF'
print("Hello from Dockerfile lesson 06")
EOF
docker build -t docker100-lesson06:v1 .
docker run --rm docker100-lesson06:v1
설명:
docker build시점에 실행되는 것은 Dockerfile의RUN입니다.docker run시점에 실행되는 것은 Dockerfile의CMD입니다.- 초보자가 가장 자주 헷갈리는 부분이 “RUN에 앱 실행 명령을 넣는 실수”입니다. 앱 시작은 실행 시점 작업이므로
CMD또는ENTRYPOINT로 분리해야 합니다.
실무 팁:
- 빌드 로그에서
RUN단계가 찍히고, 컨테이너 로그에서CMD결과가 보이면 시점 구분을 제대로 이해한 것입니다. - 팀 교육 시 “빌드 타임 vs 런타임”을 먼저 잡아두면 이후 Compose, CI 파이프라인 학습 속도가 빨라집니다.
예제 3) COPY 순서가 캐시에 주는 영향 체험
mkdir -p src
cat > requirements.txt <<'EOF'
flask==3.0.3
EOF
cat > src/app.py <<'EOF'
from flask import Flask
app = Flask(__name__)
@app.get('/')
def hello():
return 'lesson06'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
EOF
cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY src/ /app/
CMD ["python", "/app/app.py"]
EOF
docker build -t docker100-lesson06:v2 .
설명:
- 의존성 파일(
requirements.txt)을 먼저 복사하고 설치하면, 코드만 바뀔 때 패키지 설치 레이어를 재사용할 수 있습니다. - 반대로 코드를 먼저
COPY하면 작은 코드 변경에도RUN pip install이 다시 실행되어 빌드가 느려집니다. - 이 패턴은 나중에 7강(레이어 캐시 최적화)에서 더 깊게 다루지만, 지금부터 습관을 들이면 빌드 시간을 크게 줄일 수 있습니다.
예제 4) CMD 덮어쓰기와 디버깅 실행
docker run --rm docker100-lesson06:v2
docker run --rm docker100-lesson06:v2 python -c "print('CMD override test')"
설명:
- 두 번째 명령처럼 컨테이너 실행 시 추가 명령을 주면 Dockerfile의
CMD는 덮어써집니다. - 운영에서 긴급 진단이 필요할 때 기본 실행 대신 임시 명령을 넣어 빠르게 점검할 수 있습니다.
- 다만 서비스 실행 이미지에서 디버깅 명령을 남발하면 운영 일관성이 깨지므로, 목적을 분명히 하고 사용해야 합니다.
자주 하는 실수
실수 1) 최신 태그만 믿고 FROM을 모호하게 지정
- 원인:
FROM python:latest처럼 광범위 태그를 사용해, 며칠 뒤 같은 Dockerfile인데 다른 결과가 나옴. - 해결:
python:3.12-slim처럼 명시적 버전을 사용하고, 팀 규칙으로 베이스 이미지 업데이트 주기를 관리합니다.
실수 2) RUN과 CMD 역할을 뒤섞음
- 원인: 앱 실행 명령을
RUN python app.py로 작성해 빌드 중에만 실행되고 컨테이너 시작 시 아무 일도 안 일어남. - 해결: 빌드 시점 설정은
RUN, 컨테이너 시작 명령은CMD로 분리합니다.
실수 3) COPY를 너무 이른 단계에 둬서 캐시를 매번 깨뜨림
- 원인:
COPY . /app를 초반에 두고 의존성 설치를 뒤에 둠. - 해결: 변경 빈도가 낮은 파일(의존성 목록)을 먼저
COPY하고 설치한 뒤, 변경 빈도가 높은 앱 코드를 마지막에 복사합니다.
실수 4) 컨테이너 시작 실패를 코드 문제로만 판단
- 원인:
CMD ["python", "app.py"]인데 실제 파일 경로가/app/src/app.py로 달라서 파일을 못 찾음. - 해결:
WORKDIR,COPY경로,CMD경로를 한 세트로 검증하고, 필요하면docker run --rm -it 이미지 sh로 내부 경로를 직접 확인합니다.
실무 패턴
실무에서 Dockerfile 품질은 “돌아간다”에서 끝나지 않습니다. “같은 결과가 반복되고, 빌드가 빠르고, 팀원이 읽기 쉬운가”가 훨씬 중요합니다.
첫째, 의도를 단계별로 분리합니다. FROM은 런타임 선택, RUN은 환경 준비, COPY는 앱 반입, CMD는 실행이라는 역할 구분이 명확하면 코드 리뷰가 쉬워집니다.
둘째, 변경 빈도 기반 정렬을 합니다. 자주 안 바뀌는 의존성 설치 레이어를 위쪽에, 자주 바뀌는 애플리케이션 코드를 아래쪽에 두면 캐시 효율이 좋아집니다. 팀에서 빌드 시간이 2~3분만 줄어도 CI 전체 대기 시간이 크게 줄어듭니다.
셋째, 절대경로 중심으로 작성합니다. 상대경로를 무심코 섞으면 로컬에서는 되는데 CI에서는 깨지는 일이 자주 생깁니다. WORKDIR /app를 명시하고 COPY, CMD도 /app/... 기준으로 맞추면 사고가 줄어듭니다.
넷째, 이미지 재현성 점검 루틴을 둡니다. 같은 Git 커밋에서 두 번 빌드했을 때 핵심 동작이 일치하는지, 컨테이너 시작 로그가 동일한지 확인합니다. Dockerfile이 배포 단위의 계약서라는 관점이 중요합니다.
다섯째, 문제 발생 시 확인 순서를 고정합니다.
docker image inspect로 태그/레이어 확인docker run --rm 이미지로 기본 CMD 동작 확인- CMD override로 경로/의존성 최소 진단
이 순서를 팀 내 플레이북으로 통일하면 장애 대응 속도가 빨라집니다.
오늘의 결론
한 줄 요약: Dockerfile의 본질은 명령어 나열이 아니라, 빌드 시점과 실행 시점을 분리해 재현 가능한 실행 환경을 코드로 고정하는 것이다.
FROM, RUN, COPY, CMD 네 가지를 정확히 이해하면 도커 학습의 절반은 끝났다고 봐도 됩니다. 이후 고급 주제(캐시 최적화, 멀티스테이지, 보안 강화)도 결국 이 기초 위에서 확장됩니다. 오늘은 문법을 외우기보다 “왜 이 순서인지”를 이해하는 데 집중하세요. 그러면 Dockerfile이 길어져도 흔들리지 않습니다.
연습문제
python:3.12-slim기반 Dockerfile을 만들어RUN으로requests==2.32.3를 설치하고,CMD로 버전을 출력해보세요.RUN/CMD의 실행 시점 차이를 문장으로 설명해보세요.COPY . /app를 먼저 두는 Dockerfile과,COPY requirements.txt후 설치하고 마지막에 코드 복사하는 Dockerfile을 각각 빌드해보세요. 코드 한 줄 수정 후 재빌드 시간 차이를 비교해보세요.docker run --rm 이미지와docker run --rm 이미지 python -c "print('override')"를 각각 실행해 CMD 덮어쓰기 동작을 확인해보세요.
이전 강의 정답
5강 연습문제 해설:
- 상태 점검은
docker ps -a로 시작해야 종료된 컨테이너까지 포함해 원인 추적이 가능합니다. - 로그 분석은
docker logs --tail 100 --timestamps <name>처럼 시간 정보를 포함해야 배포/장애 시점과 맞춰볼 수 있습니다. - 재시작 루프나 메모리 이슈는 로그만으로 단정하지 말고
docker inspect의ExitCode,OOMKilled,RestartPolicy를 함께 확인해야 정확도가 올라갑니다.
실습 환경/재현 정보
- OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
- Docker 버전: Docker Engine/Docker Desktop 25.x 이상
- 실행 순서:
- 작업 디렉터리 생성 및 Dockerfile 작성
docker build -t docker100-lesson06:v1 .빌드docker run --rm docker100-lesson06:v1실행- Dockerfile을 캐시 친화적으로 수정 후 재빌드
- CMD override 실행으로 동작 차이 확인
- 재현 체크:
RUN은 빌드 로그에서만 보이고CMD는 컨테이너 실행 시 동작하는가?COPY순서 변경이 재빌드 시간/캐시 재사용에 영향을 주는가?- 컨테이너 내부 경로와
CMD실행 경로가 일치하는가?