Dockerイメージの軽量化



久しぶりにリーグのサッカーに参戦して、3日間筋肉痛の streampack の Tana です。

## 概要

アプリケーションのコンテナ対応を行う際にこのような経験される方が多いかと思います。

<a href="https://imgflip.com/i/2ydr4v"><img src="https://i.imgflip.com/2ydr4v.jpg" title="made at imgflip.com"/></a>

Dockerイメージサイズが大きくなると、Docker Hub や Amazon ECR などのプライベートレポジトリに
push/pull する際に時間がかかり、サービス反映に時間がかかったり待たされるのでストレスになります。
よって、少しでもサイズを小さく軽量化しセキュリティリスクを軽減するための一つ方法です。

軽量化するには、

- [Multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)
- Alpine Linux
- 不要なパッケージやファイルの削除

がキーポイントになるかと思ってます。

例えば、オフィシャル版と Alpine と比較すると 1/10 ぐらいサイズが異なります。

**nodeの場合**
- node:8 -> 670MB
- node:8-alpine -> 65MB

**rubyの場合**
- ruby:2.4 -> 684MB
- ruby:2.4-alpine -> 61.7MB

## Rails のための Dockerfile 例

では、 Dockerfile をステップバイステップで見ていきましょう。

最初に、Ruby on Rails の場合は、Gem install によるパッケージなどのインストールが必要なので、
それに伴って必要なパッケージを apk を使ってインストールします。
今回のケースでは yarn を使っているとし、すでに node がインストールされている ruby-2.6 を指定してます。 必要に応じて gem install で必要なパッケージを追加してください。例えば、mysql-dev などなど。

### ファーストステージ(Builder)

``` Dockerfile
FROM ruby:2.5.0-alpine as Builder

RUN apk add --update --no-cache \
    build-base \
    openssl \
    git
```

次は、 bundle install です。
-j4 は同時に4つダウンロード&インストール実施してくれるので、少しでも時間を短縮したい場合に便利です。 また、終わった後に不要な cache, コンパイル用ファイルは削除します

``` Dockerfile
RUN gem install --no-document bundler
RUN bundle install -j4 --retry 3 \
      && rm -rf /usr/local/bundle/cache/*.gem \
      && find /usr/local/bundle/gems/ -name "*.c" -delete \
      && find /usr/local/bundle/gems/ -name "*.o" -delete
```

あとは、yarn install などを実施して、ファーストステップとしては終了です。
rails assets::precompile などは時間を要するので、Dockerfile では指定せず、別タスクとして事前に実施することをオススメします。

``` Dockerfile
# Install yarn packages
COPY package.json yarn.lock /app/
RUN yarn install

# Add the Rails app
ADD . /app
```

### ファイナルステージ

ここでは、実際にアプリケーションを動かすのに必要なバージョンを From にて指定します。
また、ここでは必要最低限のパッケージしかインストールしないのが特徴です。
余計なパッケージを入れると、サイズが大きくなり、またセキュリティリスクも高まります。

``` Dockerfile
# -----ファイナルステージ -----
FROM ruby:2.4.1-alpine

RUN apk add --update --no-cache \
    tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
```

ここでは仮で tester というユーザーを作成してます。
アプリを動かすのには、root 権限は不要なためです。

``` Dockerfile
# Add user
RUN addgroup -g 1000 -S tester \
 && adduser -u 1000 -S tester -G tester
```

ファーストステージにてインストールした gem ライブラリや Railsソースコードをコピーします。
権限も chown で書き換えることを忘れずに。

``` Dockerfile
COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/
COPY --from=Builder --chown=tester:tester /app /app
```

最後は Rails(pumaサーバ)を起動させて終了です。

``` Dockerfile
USER tester
WORKDIR /app

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
```

アプリケーションにとっては不要なものは入っていないので、Rails console などは動きませんのでその点注意が必要です。

### おまけ
Golang の場合だともっとシンプルですね。

``` Dockerfile
# --- ビルドステージ ---
FROM golang:alpine AS Builder
RUN apk update && apk add ca-certificates git

WORKDIF /app
ADD . /app

# mod を使った場合のパッケージダウンロード
COPY ./go.mod ./go.sum ./
RUN go mod download
RUN cd /app && go build -o goapp

# --- ファイナルステージ ---
FROM alpine
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*

WORKDIR /app
COPY --from=Builder /app/goapp /app
ENTRYPOINT ./goapp
```

## 結論

以前は 1Gほどなってたイメージサイズが 400MB ぐらいになり、ECR からの pull/push が以前より早くなり快適になりました。Golang の場合だと、20MB 程度なのでさらに快適でした。シンプル・軽量・かつ早い・・よりコンテナに向いているのが理解できますね。

Multi-stage builders を学習するに当たってこちらの YouTube がとてもイメージしやすく参考になりました。
https://www.youtube.com/watch?v=wGz_cbtCiEA

では、良いコンテナライフを。