[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 를 찾는 게 중요하다.

    Untitled

  • 위 그림과 같이 사용할 OS package 들만 설치 후 사용한다. 디버깅 할 목적으로 필요한 shell 환경도 중요하다. bash, zsh 등이 깔리지 않은 Image 도 있다.
  • 또한 목적에 따라 Image 를 설치할 수 있다. 대표적으로 Python Image 의 경우 아래와 같다.
    • python:3.9 : Python 표준 Image
    • python: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 명령어를 사용할 수 있다.

    Untitled

    Untitled

  • 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 까지 포함해야 작동한다.

    Untitled

    Untitled

  • 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 를 일괄 실행한다.

    Untitled

  • 그렇게 되면 docker-compose.yml 파일을 파싱하여 컨테이너를 실행한다. 이 때 필요한 Image 를 pull 하거나 build 하는 등의 과정도 포함된다.
  • docker-compose up -ddocker run -d 와 동일하게 백그라운드에서 실행하는 명령어다.
  • docker-compose down 은 컨테이너, 불륭 등을 삭제하는 서비스 중단 명령어다.
  • docker-compose logs <서비스명> 명령어를 통해 로그를 확인해볼 수 있다.
  • 참고로 docker-compose.yml 파일을 수정하고 docker-compose up 을 하면 컨테이너를 재생성하고, 서비스를 재시작하게 된다.
  • docker-compose up 이 완료되면 아래와 같이 docker psdocker-compose ps 로 현재 실행되고 있는 컨테이너를 확인할 수 있다.

    Untitled

  • 이후에는 일반적인 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 옵션을 선택할 수 있다.
맨 위로 이동 ↑

댓글 남기기