シンプルなPython3.9のDocker環境をマルチステージビルドしてスリムにする

# はじめに

MIERUNEアドベントカレンダー22日目です！

いよいよ終盤ですね！

今回はDockerのマルチステージビルドについてやっていきます！

いやいやGIS関係ないんかい！と思う方もいるかもしれませんが、そんなことはありません！

GISに関わるプログラミングをするときにPythonはもはや必須ですし、WebGISをやっていく上でDockerも必須です。
なのでまぁDockerの話題はほぼGISですね。

ということで早速やっていきましょう！

作成するファイルはこちらに置いていますので、せっかちな方はご利用ください。
https://github.com/nokonoko1203/minimize-docker-image

## 必要なファイルを作成する

まずは適当にディレクトリを作成しましょう！

```sh
mkdir minimize-docker-image
cd minimize-docker-image
```

次に適当なPipfileを作成して書き込みます。
Pythonのバージョンはなんでも良いですが、今回はpython3.9のdockerイメージを利用するので、3.9系を指定しました。
マイクロバージョンはほんとになんでも良いです。

```sh
touch Pipfile
```

- Pipfile

```toml
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
autopep8 = "*"
flake8 = "*"
isort = "*"
mypy = "*"

[packages]
requests = "==2.24.0"

[requires]
python_version = "3.9.9"
```

作成したPipfileを利用してlockファイルを作ります。

```bash
pipenv lock
```

最後にDockerfileを作成しましょう。

- Dockerfile

```docker
FROM python:3.9-buster

ENV PYTHONUNBUFFERED=1

RUN mkdir app

WORKDIR /app

COPY Pipfile.lock /app/

RUN pip install -U pip && \
    pip install pipenv==2021.5.29 && \
    pipenv sync --system && \
    pip uninstall --yes pipenv
```

これだけでPythonの実行環境が概ね完成しました！
Dockerってすごいですね！

## イメージを作成して起動してみる

作成したDockerfileを利用してDockerイメージを作成しましょう！
`python-sample`のところは作成したいイメージ名を指定しますので、任意で構いません。

```bash
docker image build . -t python-sample
```

Dockerイメージからコンテナを起動してPythonのパッケージがインストールされているか確認してみましょう！

```bash
% docker container run --rm python-sample pip list
Package                           Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
certifi                           2021.10.8
chardet                           3.0.4
distlib                           0.3.4
filelock                          3.4.0
idna                              2.10
pip                               21.3.1
platformdirs                      2.4.0
requests                          2.24.0
setuptools                        57.5.0
six                               1.16.0
urllib3                           1.25.11
virtualenv                        20.10.0
virtualenv-clone                  0.5.7
wheel                             0.37.0
```

Pythonもちゃんと入っていそうです。

```sh
% docker container run --rm python-sample python -V
Python 3.9.9
```

## Dockerイメージをスリムにする

先程作成したDockerイメージのサイズを確認してみましょう。


```bash
% docker image ls | grep python-sample
python-sample  latest  abd1e5ec8c95  15 minutes ago  945MB
```

dockerイメージの容量は比較的大きめ（945MB）です。

基本的な原則として、イメージサイズが大きいと良いことは少ないです。

**イメージのビルドに時間がかかる上、SSDを圧迫します。**

**配布する際にはアップロードにもダウンロードにも時間がかかってしまいますね。**

なので、特に理由がなければコンパクトにするのが望ましいです。

ということでDockerfileを編集してイメージサイズを下げていきましょう！

### Dockerfileを改修して軽量なイメージを利用する

Dockerfileを以下のように修正します。

```docker
FROM python:3.9-slim-buster

ENV PYTHONUNBUFFERED=1

RUN mkdir app

WORKDIR /app

COPY Pipfile.lock /app/

RUN pip install -U pip && \
    pip install pipenv==2021.5.29 && \
    pipenv sync --system && \
    pip uninstall --yes pipenv
```

利用するイメージを`python:3.9-buster`から`python:3.9-slim-buster`に変更してみました。
このままイメージをビルドしてみましょう。

```sh
docker image build . -t python-sample-minimize
```

動作確認

```sh
% docker container run --rm python-sample-minimize pip list
Package                           Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
certifi                           2021.10.8
chardet                           3.0.4
distlib                           0.3.4
filelock                          3.4.0
idna                              2.10
pip                               21.3.1
platformdirs                      2.4.0
requests                          2.24.0
setuptools                        57.5.0
six                               1.16.0
urllib3                           1.25.11
virtualenv                        20.10.0
virtualenv-clone                  0.5.7
wheel                             0.37.0
```

そのままイメージサイズを確認してみます。

```sh
% docker image ls | grep python-sample             
python-sample-minimize  latest  733101d08d8f  12 seconds ago  174MB
python-sample  latest  abd1e5ec8c95  4 hours ago  945MB
```

！！！

なんとびっくりイメージサイズが1/5以下に！！！！！

というのは当たり前で…

**`python:3.9-buster`: よく利用される（Python以外の）ツールなどもてんこ盛りでプリインストールされている便利なイメージ**
**`python:3.9-slim-buster`: 利用頻度の低いツールを削ぎ落とした軽量イメージ**

です。

ツールがあまりインストールされていないので、当然イメージサイズも小さい、ということになります。

今回は特に問題ないので利用しましたが、slim-busterを利用する際には自分の必要とするツールがプリインストールされているのかどうかよく確認してから利用する必要があります。

また、**運用中のコンテナをbusterからslim-busterへ移行する場合にはより一層注意が必要**です。

### さらなる軽量化を目指す

ということで、（利用したいツールが全て揃っているのであれば）slim-busterを利用するだけでだいぶイメージサイズを軽量化できることがわかりましたが、今回はもう少し攻めてみましょう。

Dockerfileを以下のように修正します。

```docker
FROM python:3.9-buster as builder

ENV PYTHONUNBUFFERED=1

RUN mkdir app

WORKDIR /app

COPY Pipfile.lock /app/

RUN pip install -U pip && \
    pip install pipenv==2021.5.29 && \
    pipenv sync --system && \
    pip uninstall --yes pipenv

FROM python:3.9-slim-buster as production

ENV PYTHONUNBUFFERED=1

RUN mkdir app

WORKDIR /app

COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages/
COPY --from=builder /app /app/
```

Dockerfileの中にFROMが2回出てくるのが見慣れないという方もいるかもしれませんね。

これはマルチステージビルドといって、**FROM文を複数使い、各ステージにbuilder・productionのような名前をつけて段階的にビルドしていく**ことです。

今回の例では、まずbuilderステージで`python:3.9-buster`を利用し、pipからのライブラリのインストールなどを完了させます。
その後、`python:3.9-slim-buster`を利用したproductionステージで、builderステージでインストールしたライブラリが格納されたディレクトリごとコピーしてきています。

こうすることでpipやpipenvなど、アプリケーションの実行に不要なファイルを除外することができます。
では実際にイメージをビルドしてみましょう。

```bash
% docker image build . -t python-sample-minimize
```

その後、イメージサイズを確認してみましょう。

```bash
% docker image ls | grep python-sample-minimize
python-sample-minimize  latest  f9ab98960d48  32 seconds ago  138MB
```

174MBから138MBまで減らすことができました！

## 終わりに

複雑な依存関係をもたず、簡単なスクリプト実行のためにDockerを利用するのであれば、案外サクッとイメージサイズを下げることができます。

今回はマルチステージ前後で４０MB程度しか減りませんでしたが、実行ファイルをビルドする必要があったり、Next.jsを利用して静的ファイルを吐き出す際の利用など、「**ビルド時には多くのライブラリが必要だが、アプリケーションの実行にはほとんど依存関係が必要ない**」場合にはものすごい効果を発揮します。

皆さんもどんどん利用していきましょう！