[Docker] Docker 고급
Docker Image Size
- ML 프레임워크나 모델이 들어간 Docker Image 는 사이즈가 매우 크다. 이는 빌드 타임 / 런타임 / host 머신 디스크 관점에서 문제가 생길 수 있다.
- 빌드 타임
- Image 를 빌드하는 시점에서의 속도 문제다.
- 새로운 Image 로 교체하기 위해 기다려야 하는 시간이 늘어나기 때문에, 신속하게 대응하기 어려워진다.
- 네트워크 전송 측면에서 네트워크 전송량은 비용에 해당한다.
- 네트워크 뿐만 아니라 디스크 용량 등 빌드 시간에 따라 비용이 늘어난다.
- 런타임
- Docker Image 실행 시점의 문제다.
- 컨테이너 시작 시 메모리에 로드되는 용량이 매우 크게 된다.
- Image Pull 시 기다리는 시간이 매우 길어진다.
- host 머신 디스크
- Image 하나의 용량이 매우 크기 때문에 VM 인스턴스 등 환경에서 디스크에 대한 용량 관리가 필요하다.
- 클라우드 환경에서는 디스크 용량이 비용 이기 때문에 주의해야 한다.
- 빌드 타임
- 따라서 Image 의 크기가 작을수록 좋다.
-
이를 위해 작은 Base Image 를 선정해서 사용할 수 있다. Base Image 는 매우 다양하기 때문에 알맞는 Base Image 를 찾는 게 중요하다.
- 위 그림과 같이 사용할 OS package 들만 설치 후 사용한다. 디버깅 할 목적으로 필요한 shell 환경도 중요하다. bash, zsh 등이 깔리지 않은 Image 도 있다.
- 또한 목적에 따라 Image 를 설치할 수 있다. 대표적으로 Python Image 의 경우 아래와 같다.
python:3.9
: Python 표준 Imagepython:3.9-slim
: 슬림한 데비안 Image 기반. Production 환경에 적합한 Python Image.python:3.9-alpine
: Alpine Linux 기반으로 사용. 작은 크기. 종속성은 수동 설치해야 할 수도 있다.- 현업에서
slim
을 많이 쓴다고 한다. 그러나 이것도 목적에 따라 다르다. - Image 에서
-slim
은 항상 Image 크기가 좀 작은 것을 뜻한다.
Multi Stage Build
- Multi Stage Build 를 통해 Docker Image Size 에 대한 최적화를 할 수 있다.
- Multi Stage Build 는 Docker Image 를 효율적으로 작성하고 최적화하기 위한 방법이다.
- Container Image 를 만들 때, 빌드에는 필요하지만 최종 Container Image 에는 필요 없는 내용을 제외하며 Image 를 생성하는 방법이다.
- 하나의 Dockerfile 에 여러 Image 를 빌드하고 사용한다. 이를 통해 Base Image 를 바꾸면서 사용하며 2 개 이상의 Dockerfile 이 있는 것처럼 빌드를 수행한다.
- COPY 명령어에서
—from
옵션을 통해 실행 Image 로 전달할 수 있다. 이는 특정 Image 를 참고해서 복사하라는 의미다. -
Single Stage 예시
# Single Stage Build FROM python:3.9 WORKDIR /app COPY ./requirements.txt /app/requirements.txt RUN pip install --no-cahce-dir --upgrade -r /app/requirements.txt COPY ./simple_webserver.py /app/simple_webserver.py CMD ["python", "simple_webserver.py"]
- 위 코드는 최적화 하기 전 single stage 다. 일반 python Image 를 사용했다.
-
이를 가지고 Build 해보자.
docker build -f Dockerfile.single -t myapp:single
명령어를 CLI 에 입력하면 된다. 파일 이름을 명시하고 싶다면-f
명령어를 사용할 수 있다. -
Multi stage 예시
# Stage 1: Build FROM python:3.9 as build WORKDIR /app COPY ./requirements.txt /app/requirements.txt RUN pip install --no-cache-dir --user --upgrade -r /app/requirements.txt # Stage 2: Runtime FROM python:3.9-slim as runtime WORKDIR /app # 필요한 파일들을 빌드 스테이지에서 복사 COPY --from=build /root/.local /root/.local COPY ./simple_webserver.py /app/simple_webserver.py # 환경변수 설정. pip install 시 --user 로 설치하면 /root/.local 에 저장됨. ENV PATH=/root/.local/bin:PATH CMD ["python", "simple_webserver.py"]
- 위와 같이 stage 1 에서
as build
라는 명칭을 주고, stage 2 에서-slim
Image 를 사용하면서as runtime
이라는 명칭을 준다. COPY --from=build /root/.local /root/.local
은 stage 1(build) 에서 pip install 로 설치된 것들이--user
옵션으로 인해서/user/.local
에 저장이 되는데, build 의 해당 폴더에 저장된 것을 runtime 으로 복사하겠다는 것이다.-
이후
/user/.local
에 설치된 것을 활용하기 위해서 ENV 를 설정해준다. 이 때 실제 경로에bin
까지 포함해야 작동한다. - Multi stage build 를 통해 위 그림과 같이 image size 가 매우 작아지는 것을 확인할 수 있다. 필요한 것은 pip install 의 내용인데 그것을 복사했기 때문이다.
Container 패키징
- Container 를 패키징하는 과정에서도 최적화할 수 있는 부분이 있다. 아래는 용량을 줄이기 위해 체크해야 할 요소들이다.
- 먼저
.dockerignore
파일로 빌드 시 필요 없는 파일들을 제거할 수 있다..pt
,.pth
파일과 같은 큰 사이즈의 asset 들은 빌드에서 포함하지 않고, 빌드 타임 혹은 컨테이너를 시작하는 스크립트에서 다운받을 수 있다.- 예를 들어
.pt
,.pth
모델 파일을 Image 안에 저장하는 것이 아니라, 컨테이너 스크립트가 시작될 때 Image 를 받아오도록 바꾸는 것이다. - 즉 Image 에는 모델 파일을 포함하지 않고, 컨테이너를 시작할 때 모델을 특정 URL (ex. S3, Google Drive, Hugging Face 등)에서 다운로드 받는 것이다. 이후 모델이 이미 다운로드돼 있으면 재다운로드 생략하고 모델 캐시 디렉토리를 볼륨으로 지정해서 유지하는 방법도 있다.
- 아래와 같이 실행 스크립트
entrypoint.sh
를 짜고, Dockerfile 에서 해당 스크립트를 복사 후 실행하는 설정을 주면 된다.
#!/bin/bash MODEL_PATH="/app/model/model.pth" MODEL_URL="https://path/to/model.pth" # 모델이 없으면 다운로드 if [ ! -f "$MODEL_PATH" ]; then echo "Downloading model..." curl -o "$MODEL_PATH" "$MODEL_URL" echo "Download complete!" else echo "Model already exists, skipping download." fi # 이후 원래 실행할 파이썬 스크립트 실행 exec python main.py
- Dockerfile 은 아래와 같다.
# Stage 1: Build FROM python:3.9 as build WORKDIR /app COPY ./requirements.txt /app/requirements.txt RUN pip install --no-cache-dir --user --upgrade -r /app/requirements.txt # Stage 2: Runtime FROM python:3.9-slim as runtime WORKDIR /app # 필요한 파일들을 빌드 스테이지에서 복사 COPY --from=build /root/.local /root/.local COPY ./simple_webserver.py /app/simple_webserver.py COPY ./entrypoint.sh /app/entrypoint.sh # 환경변수 설정 ENV PATH=/root/.local/bin:$PATH # (필요 시) bash 설치 # RUN apt-get update && apt-get install -y bash && rm -rf /var/lib/apt/lists/* RUN chmod +x entrypoint.sh ENTRYPOINT ["sh", "./entrypoint.sh"] CMD ["python", "simple_webserver.py"]
- 다음으로 Dockerfile 안에서 command 들의 순서 최적화를 통해 캐싱을 최대한 이용한다.
- 변경 가능성이 낮은 명령어를 위로, 변경 가능성이 높은 명령어는 아래에 위치시킨다.
Error 예시
- FastAPI 애플리케이션을 Docker Image 로 만들고 Registry 에 업로드 했는데, 컨테이너에서 실행되는 서버로의 요청이 정상적으로 되지 않는 경우가 있다.
- 이는
docker run your-image:latest
와 같이 실행할 때 컨테이너의 포트를 노출하지 않았기 때문이다. - 외부(브라우저 등)에서 컨테이너에 실행 중인 프로그램에 접근하려면 컨테이너의 포트를 호스트 머신과 연결해야 한다.
- 따라서
docker run -p 8000:8000 your-image:latest
와 같이 호스트 8000 번으로 들어온 요청을 컨테이너 내 8000 번 포트로 포워딩해야 정상 작동한다.
Docker Compose
- Docker Compose 는 여러 Docker Image 를 실행할 수 있는 방법이다.
docker run
을 여러 번 실행하는 것은 휴먼 에러의 발생 가능성이 있다. 이를 해결하기 위해 하나의 Docker Image 가 아니라 여러 Docker Image 를 동시에 실행할 수 있는docker compose
가 많이 사용되고 있다.- A Image 로 컨테이너를 띄우고, 이후에 B Image 컨테이너를 실행해야 하는 경우는 의존성에 해당하는 경우다. 예를 들어 A 는 DB 고, B 는 웹 서비스인 경우가 이에 해당한다.
- DB 먼저 띄우고 웹서비스를 띄우는 경우가 많다.
- 이처럼 의존성이 필요한 경우에
docker compose
를 사용할 수 있다.
- 또한
docker run
할 때 옵션이 너무 다양하고, Volume Mount 를 하지 않았다면 데이터가 모두 날라가게 된다. 이런 경우에도 Docker Compose 를 활용할 수 있.- 여러 컨테이너를 한번에 실행할 수 있다.
- 여러 컨테이너의 실행 순서, 의존도를 관리할 수 있다.
- docker-compose.yml 파일에 작성한다.
Docker Compose YAML 작성
-
docker-compose.yml
파일에 아래와 같이 특정 문법으로 작성하게 된다. 아래 코드는 db 컨테이너와 app 컨테이너 두 개를 실행시키는 내용이다.version: '3' services: db: image: mysql:5.7.12 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: my_database ports: - 3306:3306 app: build: context: . environment: DB_URL: mysql+mysqldb://root:root@db:3306/my_database?charset=utf8mb4 ports: - 8000:8000 depends_on: - db restart: always
version
은 docker compose 의 버전을 뜻한다.services
는 실행할 컨테이너를 정의한다. 각 서비스는 하나의 컨테이너가 되고 세부 설정을 저장하게 된다. 위 예제에서는 db, app 서비스가 존재한다.image, environment, ports
각각 Image 명시, 환경변수, 포트 설정을 뜻한다.depends_on
에서는 명시된 서비스가 실행된 이후에 실행된다. 즉 위에서는 db 서비스가 정상적으로 동작한 후에 app 이 실행된다는 것이다.restart
는 컨테이너의 재실행 정책을 뜻한다. 컨테이너에 이슈가 있으면 바로 다시 띄울지 혹은 죽일지 등을 정의하게 된다.- 그 외 아래의 내용을 추가할 수 있다.
volumes
: 볼륨 정의. 호스트와 컨테이너의 저장소를 지정한다.secrets
: 보안이 필요한 데이터를 전달한다.configs
: 컨테이너에 사용할 config 파일을 전달한다.command
: 컨테이너가 시작될 때 실행할 명령을 지정한다.
Docker Compose 실행
-
docker-compose up
명령어를 통해 Docker Image 를 일괄 실행한다. - 그렇게 되면
docker-compose.yml
파일을 파싱하여 컨테이너를 실행한다. 이 때 필요한 Image 를 pull 하거나 build 하는 등의 과정도 포함된다. docker-compose up -d
는docker run -d
와 동일하게 백그라운드에서 실행하는 명령어다.docker-compose down
은 컨테이너, 불륭 등을 삭제하는 서비스 중단 명령어다.docker-compose logs <서비스명>
명령어를 통해 로그를 확인해볼 수 있다.- 참고로
docker-compose.yml
파일을 수정하고docker-compose up
을 하면 컨테이너를 재생성하고, 서비스를 재시작하게 된다. -
docker-compose up
이 완료되면 아래와 같이docker ps
나docker-compose ps
로 현재 실행되고 있는 컨테이너를 확인할 수 있다. - 이후에는 일반적인 docker 사용과 동일하다.
- 컨테이너 서버가 띄워져 있고 로컬 호스트와 연결되어 있으면 통신이 가능하다.
docker-compose
로 여러 Image 를 같이 쓸 수 있게 하는 것이 좋다.
- 앞으로 사용할 많은 기능들이 분명 Docker 에 있다. Docker 를 통해 컨테이너를 띄워서 사용하면 매우 유용하니, 잘 알아두자.
그 외
- pip install cache
- pip install 을 실행하면 재설치 등을 피하기 위해 캐시가 함께 저장된다.
- pip install 시
--no-cahce-dir
옵션을 주면 캐시를 사용하지 않고 pip install 을 하게 된다. - 이는 하드디스크에 공간이 없거나 Docker Image 를 작게 유지하고 싶을 때 사용한다.
- Docker build 시 캐싱 관련 이슈
- Dockerfile 을 작성하고 컨테이너를 띄우기 전에
docker build -t <빌드할 이미지 이름:태그 이름> “Dokcerfile이 위치한 경로”
로 Image 를 빌드하게 된다. - 이 때 Dockerfile 의 layer 와 캐싱(caching) 문제로 패키지 관련 이슈가 발생할 수 있다.
- 일반적으로 Dockerfile 에 정의된 명령어와 이전 명령어들이 같다면 동일한 레이어로 판단하기 때문에 다시 다운로드 받지 않으며, 캐싱 되어있는 부분을 가져온다. 이를 통해 데이터 저장의 효율성과 빌드 시간 단축이라는 효과를 얻을 수 있다.
- 그러나 새로운 이미지 빌드 시, 파일이나 패키지가 변경되었음에도 처음 캐싱된 레이어의 정보로 인해 별도의 업데이트 없이 이전의 것이 그대로 이미지에 포함되는 문제가 발생한다.
- 이를 해결하기 위해
ONBUILD RUN apt update
와 같이 build 할 때 마다 실행할 명령을 지정할 수 있다. - 두번째로 빌드 시
--no-cache
옵션을 사용할 수 있다. 이 옵션을 사용하면 docker build 시 캐시를 사용하지 않고 빌드를 진행하게 된다. - 다만,
--no-cache
옵션을 사용하면 모든 레이어에 캐시를 아예 사용하지 않기 때문에 빌드 시간이 오래 걸릴 수도 있다는 단점이 있다. - 따라서 build 시간을 줄이는 것이 목적이라면
ONBUILD
를 사용하는 것이 효과적일 수도 있고, 명령마다 지정해야 하는 번거로움이 싫다면--no-cache
옵션을 선택할 수 있다.
- Dockerfile 을 작성하고 컨테이너를 띄우기 전에
댓글 남기기