マルチステージビルドでPythonのコンテナイメージを軽量にする

仕事でPython/Djgnagoを使ってWEBアプリを開発し、作成したアプリはDockerコンテナにして運用しています。インストールするPythonのライブラリが150個ぐらいあるので、コンテナイメージが大きくなってアップロードやダウンロードに時間がかかっていました。

何とかコンテナイメージを軽くするために、マルチステージビルドを実施（ついでにAWS ECRのイメージスキャンも実施）し、コンテナイメージの容量（と脆弱性）がどのぐらい削減できるのかをそれぞれのパターンで検証しました。


## 修正前
```
FROM python:3.7.13-bullseye

ENV PYTHONUNBUFFERED 1
ENV PIPENV_TIMEOUT 600

# githubから直接pipインストールするため、gitインストール
RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install gcc git mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6 && \
    apt-get autoclean -y && apt-get clean && rm -rf /var/cache/apt/* /var/lib/apt/lists/*

RUN groupadd -r app && useradd --no-log-init -r -g app app
USER app

COPY --chown=app:app . /home/app
WORKDIR /home/app

ENV PATH="/home/app/.local/bin:${PATH}"

RUN pip install --upgrade pip && pip install pipenv
RUN pipenv install --ignore-pipfile --deploy --system

EXPOSE 8000
CMD gunicorn config.wsgi -b 0.0.0.0:8000 --log-level=info
```

`1999.66MB(2重要 + 369その他(詳細))`になりました。容量はほぼ2GBになり、criticalの脆弱性も2件含まれています。


## 軽量なベースイメージを利用
`python:3.7.13-bullseye`イメージより軽量な`python:3.7.13-slim-bullseye`イメージを利用します。ちなみに`slim`がついたイメージは使用頻度の低いツールやライブラリを除外した最小イメージになります。

```
FROM python:3.7.13-slim-bullseye

ENV PYTHONUNBUFFERED 1
ENV PIPENV_TIMEOUT 600

RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install gcc git mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6 && \
    apt-get autoclean -y && apt-get clean && rm -rf /var/cache/apt/* /var/lib/apt/lists/*

RUN groupadd -r app && useradd --no-log-init -r -g app app
USER app

COPY --chown=app:app . /home/app
WORKDIR /home/app

ENV PATH="/home/app/.local/bin:${PATH}"

RUN pip install --upgrade pip && pip install pipenv
RUN pipenv install --ignore-pipfile --deploy --system

EXPOSE 8000
CMD gunicorn config.wsgi -b 0.0.0.0:8000 --log-level=info
```

`1805.31MB(1重要 + 236その他(詳細))`となり、容量を200MB、criticalの脆弱性も1件削減できました。


## マルチステージビルドを利用
マルチステージビルドでコンテナイメージを作成します。pipインストールしたライブラリを実行用のコンテナイメージにコピーしています。

```
FROM python:3.7.13-bullseye as builder

ENV PYTHONUNBUFFERED 1
ENV PIPENV_TIMEOUT 600

RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install gcc git mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6

COPY Pipfile Pipfile.lock /app/
WORKDIR /app

RUN pip install --upgrade pip && pip install pipenv
RUN pipenv install --ignore-pipfile --deploy --system


FROM python:3.7.13-slim-bullseye as runner

ENV PYTHONUNBUFFERED 1

RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6 && \
    apt-get autoclean -y && apt-get clean && rm -rf /var/cache/apt/* /var/lib/apt/lists/*

RUN groupadd -r app && useradd --no-log-init -r -g app app
USER app

COPY --chown=app:app . /home/app
WORKDIR /home/app

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

EXPOSE 8000
CMD gunicorn config.wsgi -b 0.0.0.0:8000 --log-level=info
```

`947.26MB(1高 + 106その他(詳細))`となり、容量は当初の半分以下1GBを割り、criticalの脆弱性もなくすことができました。


## 不要なパッケージをインストールしない
apt-getに`--no-install-recommends`をつけて実行します。

> Ubuntuは8.10から、インストールしようとしているソフトウェアが依存しているパッケージだけでなく「推奨パッケージ」も一緒にインストールするようになりました。これは大抵の場合はユーザーにとって便利ではあるものの、たまに必要以上に多くのパッケージが一緒にインストールされてしまうこともあります。
>
> aptコマンドに--no-install-recommendsオプションを渡すことで、推奨パッケージのインストールを抑制できます。

引用元：[第331回 パッケージ管理のハウツー集](https://gihyo.jp/admin/serial/01/ubuntu-recipe/0331)


```
FROM python:3.7.13-bullseye as builder

ENV PYTHONUNBUFFERED 1
ENV PIPENV_TIMEOUT 600

RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install gcc git mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6

COPY Pipfile Pipfile.lock /app/
WORKDIR /app

RUN pip install --upgrade pip && pip install pipenv
RUN pipenv install --ignore-pipfile --deploy --system


FROM python:3.7.13-slim-bullseye as runner

ENV PYTHONUNBUFFERED 1

RUN apt-get -y update && \
    apt-get -y upgrade && \
    apt-get -y dist-upgrade && \
    apt-get -y install --no-install-recommends mecab libmecab-dev mecab-ipadic mecab-ipadic-utf8 swig fonts-vlgothic poppler-utils libglib2.0-0 libsm6 libxrender1 libxext6 && \
    apt-get autoclean -y && apt-get clean && rm -rf /var/cache/apt/* /var/lib/apt/lists/*

RUN groupadd -r app && useradd --no-log-init -r -g app app
USER app

COPY --chown=app:app . /home/app
WORKDIR /home/app

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

EXPOSE 8000
CMD gunicorn config.wsgi -b 0.0.0.0:8000 --log-level=info

```
`927.14MB(1 高 + 104 その他 (詳細))`となり、ダメ押しでさらに20MBほど削減できました。

## 最後に
上記以外に軽量化のテクニックは様々あると思いますが、マルチステージビルドをするだけで一気に半分くらい容量を削減できるので、コンテナイメージを軽量にしたい場合、まず最初に実践すべき方法と思いました。


### 参考
[Dockerfileを書くためのベストプラクティス【参考訳】v18.09](https://qiita.com/zembutsu/items/a96b68277d699f79418d)
[仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編](https://future-architect.github.io/articles/20200513/)