이번 포스팅은 Docker 이미지 구조에 대해 더 자세히 정리했다.
내용은 이전 포스팅과 연결된다.

FROM python:3.11
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
COPY ./app /code/app
RUN apt-get update
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
이미지는 이 명령어들을 하나하나 실행할때마다 각각의 스냅샷들을 기록해놓고, 이 변화들이 쌓이는 개념이다.
그래서 빌드를 할 때 상위 명령어부터 차례대로 실행되는데, 만약 빌드를 하는 도중에 중단하면 docker images에는 나타나지 않지만 캐시는 남는다. 그래서 다음에 동일한 Dockerfile로 빌드를 시도하면 Docker는 이 캐시된 레이어를 재사용해서 중단된 지점 이후부터 빌드를 재개한다. (이 캐시들은 이전 포스팅에서 언급했던 docker image prune 명령어로 없앨 수 있다.)
갑자기 이 얘기를 왜 하냐?
패키지 설치하는 requirements.txt 파일은 평소에 업데이트가 자주 되니까(패키지를 담고 있는 파일이니까 의존성 문제 등 이것저것 건들 게 많음), 다음에 실행할때도 업데이트된 COPY ./requirements.txt /code/requirements.txt 줄부터 실행된다.
하지만 RUN apt-get update 같이 자주 실행할 필요가 없는 명령어도 윗줄부터 실행되니까 어쩔 수 없이 매번 실행된다.
그래서 자주 변경되지 않는 명령어는 되도록이면 위쪽에 두는 게 좋다.
근데 그렇다고 순서를 아무렇게나 배치하면 빌드의 논리적 흐름을 깨뜨릴 수 있기 때문에 잘 생각하고 해야한다.
예를 들면,
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./requirements.txt /code/requirements.txt
이런 식으로 배치하면 당연히 안 된다.
Dive
Docker 이미지를 분석하는 데 사용되는 오픈 소스 도구이다.
Docker 이미지의 레이어 구조를 시각적으로 탐색하고, 각 레이어가 이미지 크기에 얼마나 기여했는지 분석하여 이미지를 최적화할 수 있게 한다.
dive 설치는 여기를 참고하면 된다.
설치를 완료했다면, 먼저 dive에 fastapi-app 이미지를 넣어보자.
dive fastapi-app

뭐가 많이 나오는데, 간단히 알아보자.
좌측 상단의 Layers는 Dockerfile의 각 명령어가 이미지 크기에 얼마나 기여했는지 보여준다.
ex: FROM blobs는 120 MB, 등등
우측은 현재 선택된 레이어(FROM blobs)에서 파일 시스템에 어떤 변화가 있었는지 보여준다.
ex: 변경점, 스냅샷 등
좌측 하단에는 Image Details가 보이는데, 이미지의 상태를 간단히 보여준다.
이미지 이름, 이미지의 최종 크기, 낭비된 공간, 이미지 효율성 점수가 포함되어 있다.
도커 최적화 [기본]: 4분 26초 소요

코드는 자주 업데이트 되지만 패키지는 상대적으로 자주 바뀌지 않는다.
그러므로 소스코드를 변경할 때마다 pip install 작업을 할 필요가 없다.
그렇다면?
자주 변경되지 않는 것들만 모아서 base 이미지로 만들어 놓고,
그 base 이미지를 기반으로 새로운 이미지를 만들게 디자인하면 된다.
도커 최적화 [캐싱]: 34초 소요 (- 87 %)

지금까지 작업했던 basic 폴더 말고 Optimization 폴더에 가서 base 파일을 확인해보자.
ls
Dockerfile Dockerfile.base README.md app requirements.txt
vi Dockerfile.base
FROM python:3.11
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
COPY ./app /code/app
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
~
~
~
~
~
~
~
~
~
~
~
~
"Dockerfile.base" 7 lines, 168 bytes
이 자주 쓰는 base 도커 이미지를 미리 구워놓자.(빌드해보자.)
docker build . -f Dockerfile.base -t fastapi-app:base
[+] Building 17.9s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.base 0.0s
=> => transferring dockerfile: 212B 0.0s
=> [internal] load metadata for docker.io/library/python:3.11 1.4s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 52B 0.0s
=> [1/5] FROM docker.io/library/python:3.11@sha256:38639aa0267125ab 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 253B 0.0s
=> CACHED [2/5] WORKDIR /code 0.0s
=> CACHED [3/5] COPY ./requirements.txt /code/requirements.txt 0.0s
=> [4/5] COPY ./app /code/app 0.0s
=> [5/5] RUN pip install --no-cache-dir --upgrade -r /code/require 16.0s
=> exporting to image 0.5s
=> => exporting layers 0.5s
=> => writing image sha256:67b1d4787b294c199cd1cb72c1a75bf3cd44d40d 0.0s
=> => naming to docker.io/library/fastapi-app:base 0.0s
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
fastapi-app base 67b1d4787b29 42 seconds ago 1.17GB
fastapi-app latest 3c9dc174b172 2 hours ago 1.19GB
Docker Hub에 이미지 배포하기
먼저 DockerHub 가입하고, 레포지토리를 생성한다.(레포지토리 이름은 fastapi-app으로 했음)
그리고 base 이미지에 새로운 태그를 붙여준다.
docker tag fastapi-app:base connorchoi/fastapi-app:base
fastapi-app:base -> connorchoi/fastapi-app:base
새로운 태그에는 push를 위해 필자의 DockerHub 이름을 붙였다.
docker images를 실행하면
REPOSITORY TAG IMAGE ID CREATED SIZE
connorchoi/fastapi-app base 71925412b821 10 minutes ago 1.17GB
fastapi-app base 71925412b821 10 minutes ago 1.17GB
라고 뜨는데 Image ID가 같으므로 새로운 이미지가 생성된 건 아니고 그냥 바로가기 느낌
이제 허브에 Push 해보자
먼저, 로그인을 해준다.
docker login
입력하고 본인의 docker hub 이름을 입력하면 웹으로 코드가 뜬다.
확인을 누르면 로그인 완료.
그 다음, push 해보자
docker push connorchoi/fastapi-app:base
The push refers to repository [docker.io/connorchoi/fastapi-app]
12225b4d1b6b: Pushed
7e9f26a1728f: Pushed
63a662e2d3f5: Pushed
8f0fb126babf: Pushed
6e3ea126874c: Mounted from library/python
935c35d41239: Mounted from library/python
4e19de02f185: Mounted from library/python
662b8975365c: Mounted from library/python
8e47c7b8acf3: Mounted from library/python
0b208bc29d30: Mounted from library/python
e9b010e49a06: Mounted from library/python
base: digest: sha256:8dd67fb22a0fa7af0c8bb6d33bdd18279f78815134c30f9c040552f0604abfcd size: 2629
그리고 레포를 보면

push가 잘 된 것을 확인할 수 있다.
회고
이번 포스팅에서는 Dive를 통한 이미지 파일 분석, 최적화(캐싱)의 원리를 알아보았다.
추후
- Docker Compose
- Docker Swarm
- Kubernetes
까지 정리해보겠다.
'AI > MLOps' 카테고리의 다른 글
| [12/15] 아이펠 리서치 15기 TIL | Model Serving (TorchServe) (0) | 2025.12.15 |
|---|---|
| [12/12] 아이펠 리서치 15기 TIL | Kubernetes(K8s): minikube (1) | 2025.12.12 |
| [12/12] 아이펠 리서치 15기 TIL | Docker Swarm (1) | 2025.12.12 |
| [12/11] 아이펠 리서치 15기 TIL | Docker 빌드 (0) | 2025.12.11 |
| [12/10] 아이펠 리서치 15기 TIL | Docker: 컨테이너 환경 구성 및 이미지 활용 (0) | 2025.12.11 |