Shinonome Tech Blog

株式会社Shinonomeの技術ブログ
3 min read

Dockerイメージを軽量化しよう

今回はDockerイメージの軽量化に関する知見がたまってきたので、実際にイメージサイズを小さくしながらどのような方法があるのか紹介したいと思います

こんにちは!PlayGroundでバックエンドとインフラを触っているこのぴーです
今回はDockerイメージの軽量化に関する知見がたまってきたので、実際にイメージサイズを小さくしながらどのような方法があるのか紹介したいと思います

軽量化していないと何がダメなの?

軽量化していない時のデメリットをいくつか挙げたいと思います

  • ローカルのストレージを圧迫する
  • Docker HubやAWS ECRなどのレジストリにpush/pullする際に時間がかかる
  • ビルドに時間がかかる

レジストリからpullする時間が多くなるとデプロイやスケールアウトする際に待ち時間が多くなってしまいます
このほかにも様々なデメリットがあると思いますが、パッと思いつくだけでこれだけあります

軽量化前のイメージ

今回はこちらのDockerfileでビルドされるイメージを軽量化していこうと思います

Dockerfile

FROM python:3.8
ENV PYTHONUNBUFFERED 1

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y postgresql netcat
RUN pip install --upgrade pip

RUN mkdir /code
WORKDIR /code

COPY requirements.txt /code/
RUN pip install -r requirements.txt

COPY . /code/

docker-compose.yml(抜粋)

version: '3'

services:
  web:
    build: ./api
    volumes:
      - ./api:/code
    ports:
      - "8000:8000"
    command: >
      /bin/sh -c
        'echo "Waiting for postgres..." &&
        while ! nc -z db 5432; do sleep 1; done &&
        echo "PostgreSQL started" &&

        python manage.py migrate &&
        python manage.py runserver 0.0.0.0:8000'

 db:
    image: postgres:11.9
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    volumes:
      - ./db/:/var/lib/postgresql/data

このDockerfileとdocker-compose.ymlは以前Python + DjangoでWebアプリケーションを開発するプロジェクトで使用したものです
webコンテナの方でDjangoアプリケーションを動かし、dbコンテナの方でPostgreSQLを動かしています

とりあえず何もしていない状態だとどれくらいの大きさになるかビルドして確認してみます

docker build -t docker-image-before .
docker image ls docker-image-before
>> REPOSITORY            TAG       IMAGE ID       CREATED          SIZE
>> docker-image-before   latest    43162b60031f   36 seconds ago   1.5GB

requirements.txtに記載されたライブラリにもよりますが今回の場合は1.5GBになりました

実際に軽量化していく

それでは、この1.5GBのイメージを様々な手法を用いて小さくしていきます

1, 不要なパッケージインストールの削除

まずは、使わないパッケージのインストールをやめることでパッケージ分の容量を削減してイメージを小さくします

現在、Dockerfileの4~6行目でapt-get update, upgrade, install postgresql netcatしていますが、これらの中から無駄なものを削除していきます

まずpostgresqlですが、このコンテナではDBを動かすことはしないので不要になります(なぜ入っていたのかも不明)

そしてもう1つインストールされているnetcatですが、こちらも不要になります
ローカルで開発する際、Pythonコンテナ上でDjangoを起動する前にDBが接続待ちの状態になっていないと動かないのでコンテナ内でnetcatを使用してDBのヘルスチェックを行っています
実はこのヘルスチェックはコンテナ内で行わずともdocker compose上で行えるのでdocker-compose.ymlを編集します

services:
  db:
    ...
    volumes:
      - ./db/:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 1s
      timeout: 5s
      retries: 5
      start_period: 30s

  web:
    build: ./api
    ...
    depends_on:
      db:
        condition: service_healthy
    command: >
      /bin/sh -c
        'python manage.py migrate &&
        python manage.py runserver 0.0.0.0:8000'

dbにhealthcheckが増えてwebのcommandがスッキリしました
これでDockerfileからパッケージインストールのコマンド群を削除することができます

削除した結果、Dockerfileはこうなります

FROM python:3.8
ENV PYTHONUNBUFFERED 1

RUN pip install --upgrade pip

RUN mkdir /code
WORKDIR /code

COPY requirements.txt /code/
RUN pip install -r requirements.txt

COPY . /code/

また、ビルドしてサイズの確認も行います

docker build -t docker-image-after .
docker image ls docker-image-after
>> REPOSITORY           TAG       IMAGE ID       CREATED              SIZE
>> docker-image-after   latest    5b7a32f97384   About a minute ago   1.11GB

400MBほど小さくなりました

2, ベースイメージの変更とマルチステージビルドの使用

次に、ベースイメージをサイズが小さいものへと変更します
いくらパッケージなどが小さくなったとしてもベースとなるイメージが大きければ意味は半減してしまいます
有名な軽量イメージとしてはAlpineやDebian slimなどがありますが、今回はDistrolessイメージを選択します

DistrolessとはGoogleがメンテナンスしているイメージでその名の通りLinuxのdistro(ディストリビューション)が含まれていません(厳密には含まれていますがここでこの説明は割愛します)
また、軽量でアプリケーションの実行に必要な最低限の物しか含まれていないため脆弱性やバグが生まれにくくセキュアです

しかし、Distrolessにはパッケージマネージャはもちろんshellすらないため、pipが使えずライブラリのインストールができません
そこで使用するのがマルチステージビルドです
マルチステージビルドでは、パッケージインストールやビルド専用のイメージとアプリケーション実行専用イメージなど複数のイメージを使用して最終的なイメージをビルドします。中間イメージでインストールしたパッケージやビルドしたアプリケーションを最終イメージへコピーすることで無駄なものが含まれていないイメージを作成することができます。

下のDockerfileはdocker docsにあるGolangで作られたアプリケーションを実行する例です

# syntax=docker/dockerfile:1
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

builderという名前の付いたビルドステージでインストールとビルドを行い、2つ目のビルドステージでalpineイメージにビルドされたバイナリをコピーしています
このようにすることで実行時には不要なビルドツールやソースコードを含めない軽量なイメージが出来上がります。

今回はpython:3.7-busterイメージ上でpip installを行い、ライブラリをdistroless/pythonイメージの方へコピーします

マルチステージビルドを使った結果Dockerfileはこのようになりました

FROM python:3.7-slim-buster as builder

WORKDIR /code
COPY requirements.txt /code/

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

FROM gcr.io/distroless/python3-debian10

COPY --from=builder /usr/local/lib/python3.7/site-packages /root/.local/lib/python3.7/site-packages

WORKDIR /code
COPY . /code/

ビルドしてサイズの確認を行います

docker build -t docker-image-after-2 .
docker image ls docker-image-after-2
>> REPOSITORY             TAG       IMAGE ID       CREATED          SIZE
>> docker-image-after-2   latest    65046a8a7cd3   49 seconds ago   208MB

なんとベースイメージを変更する前から5分の1になりました
最初と比べると7分の1以下です

さいごに

今回は巨大なDocker imageを軽量化しましたが、不要なパッケージインストールの回避とベースイメージの変更で1.5GBものイメージを200MBほどまでに抑えられました
また、今回はDockerfileを書きかえていくことで軽量化をしましたが、DockerSlimというツールを用いるともっと手軽に軽量化することができます