Dockerのコンテナイメージサイズを1/10以下に軽量化してみた

# はじめに
VSCode + Python + Poetry + Docker(docker-compose)でdev-containerを作成して開発を行っていました。
Dockerを勉強し、イメージの軽量化に関する記事を読んでいると、自分が使っているコンテナイメージのサイズが気になりました。

```bash
docker images
> REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
> dev-container latest    a9b8e3df9087                    2.31GB
```
**2.31GB！？**
サーバとしてアプリを動かしていないのにここまで大きいなんて…

というわけで勉強も兼ねて、イメージの軽量化に取り組みました。

# イメージが軽量であるメリット
### ストレージの節約
これは言わずもがなだと思います。
限られたリソースを有効に使うことができます。

### ビルド時間の短縮
Dockerは環境を作ったり壊したりが簡単なのもいいところだと思います。
ビルド時間が長いと、この恩恵を感じづらくなりますよね。

# 環境
OS：Ubuntu22.04
Docker: 26.1.1
Docker Compose: v2.27.0

# 軽量化していく
## 最初の状態
```compose.yml
version: "3"
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    tty: true
    volumes:
      - type: bind
        source: ./
        target: /workspace/app
```

```Dockerfile
FROM python:3.11.9-slim

COPY pyproject.toml ./
# pipのアップグレード
RUN pip install --upgrade pip

# パッケージのアップグレード、インストール
RUN apt -y update && apt -y upgrade
RUN apt -y install libopencv-dev

# poetryのインストール、設定
RUN pip install poetry
RUN poetry config virtualenvs.create false \
    && poetry install --no-root

# コンテナの作業ディレクトリを作成
RUN mkdir -p /workspace/app
WORKDIR /workspace/app

EXPOSE 8888
```

この記事ではDockerfileしか触らないですが、compose.ymlも載せておきます。

今見るとかなり無駄な記述が多いと思います。

## ベースイメージ変更、レイヤー数削減
```Dockerfile
# ベースイメージをslimからslim-bullseyeに変更
FROM python:3.11.9-slim-bullseye

# タイムゾーンを日本に設定
ENV TZ Asia/Tokyo
# COPY pyproject.toml .を削除
# 必要なパッケージ　RUNコマンドを一つに結合
RUN apt update && \
    apt -y upgrade && \
    apt -y install libopencv-dev

# pipのアップグレードとpoetryのインストール
RUN pip install --upgrade pip && \
    pip install poetry

# コンテナの作業ディレクトリを作成
RUN mkdir -p /workspace/app
WORKDIR /workspace/app

# 仮想環境を作らないよう設定
RUN poetry config virtualenvs.create false

EXPOSE 8888
```

最初に更新後のものを載せておきます。
```bash
docker images
>REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
>dev-container   latest    585f482be78b                    1.17GB
```
この時点でイメージサイズは約半分になりました。
変更点はベースイメージ、タイムゾーンの追加、レイヤーの削減です。
また、pyproject.tomlのコピーとパッケージインストールも作業ディレクトリをマウントするので削除しました。
※後で調査すると、コピーや`poetry install`が7MBほどを占めていたようです。かなり無駄な記述をしていたと痛感しました。

イメージの軽量化に関わるのは、ベースイメージとレイヤーです。

### ベースイメージ
ベースイメージは[Docker Hub](https://hub.docker.com/)を使って決めました。

Docker Hubメリット
- イメージのサイズや脆弱性のレベルや有無を確認できる
- 公式や他の人のDockerfileを見ることができる

`slim`から`slim-bullseye`への変更は微々たる差ですが、`slim`が付いているものとそうでないものの差には驚きます。
`slim`は使用頻度の低いパッケージが除かれているため軽量になるそうです。

### レイヤーの削減
レイヤーとはざっくり言うと、命令1つごとに作られる変更差分です。
Dockerfileでの命令コマンド`RUN`や`COPY`が実行されるたびにレイヤーが作られます。
つまり命令コマンドが多くなるとその分、変更差分が作られてしまいます。そのためイメージサイズがどんどん大きくなる原因になります。

今回はバラバラになっていた`RUN`コマンドをまとめたり、作業ディレクトリをマウントするため不要だった`COPY`コマンドの削除など行いました。

ただし、レイヤーの削減は可読性の低下といった問題が起こり得ます。
また、本来なら複数の命令(apt updateとapt upgradeなど)をまとめて実行するので、どの処理が失敗したか分かりづらくなる可能性もあります。

## マルチステージビルド
```Dockerfile
# パッケージをインストールするためのイメージ
FROM python:3.11.9-slim-bullseye as builder

# 必要なパッケージ
RUN apt-get update -q
RUN apt-get -y upgrade --no-install-recommends
RUN apt-get -y install libopencv-dev

# pipのアップグレードとpoetryのインストール
RUN pip install --upgrade pip
RUN pip install poetry

# 仮想環境を作らないよう設定
RUN poetry config virtualenvs.create false

# 実際に使用するイメージ
FROM python:3.11.9-slim-bullseye as dev
# タイムゾーンを日本に設定
ENV TZ Asia/Tokyo

# builderから必要な内容をコピー
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

WORKDIR /workspace/app

EXPOSE 8888
```

```bash
docker images
>REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
>dev-container   latest    585f482be78b                    188MB
```
一気にイメージサイズが減りました！

マルチステージビルドに伴って、レイヤーの削減をしないようにしました。

### マルチステージビルドとは
Docker Engine17.05から導入された機能です。
マルチステージビルドを実行するには`FROM`行を複数回記述します。
`FROM`のたびに新しいベースイメージを作成するビルドステージが開始され、一方の内容を他方にコピーすることができ、さらに最終イメージ以外を無視することができます。

1つ目のイメージで必要なパッケージのインストールやアップグレード、アプリのビルドなどを行い、使用したいイメージには使用するファイルやフォルダだけをコピーすることで、余分なレイヤーを作らずイメージを生成できます。

# 最後に
Dockerのコンテナイメージ軽量化には以下の3つが重要だとわかりました。

- ベースイメージ
- レイヤー削減
- マルチステージビルド

特にマルチステージビルドは余分なレイヤーを、使わないイメージに押し付けることができるので、軽量なイメージの作成にはほぼ必須かと思いました。
他にもこんなDockerfileの作り方があるなど教えていただきたいです。

間違いがあれば指摘していただけますと幸いです。