お前たちのrails on dockerのCIは遅すぎる。倍早くできる方法５選！

# はじめに
どうも！生産技術部で製品の検査工程を担当しているエンジニアです。今流行の煽り商法ですが、中身は有益な情報を詰め込んでいますので最後までお付き合いください。

Docker ComposeでRuby on Railsの開発環境を整えていく中で、CIの実行時間がネックになる課題に取組み改善を行いました。改善によって、8分の実行時間を４分まで削減する事ができました。

本記事では、CIを倍早くするために必要な５つの方法を紹介します。紹介の中には、各ステップの参考実装と、スムーズに改善できるようなエッセンスを散りばめてあります。

改善結果は以下の通りです。上から進めていき５番目まで実践する事で、倍早くなり、幅広いシチュエーションで柔軟に対応が可能な仕組みを実現することができます。

* CIの改善

|index|改善内容|CI実行時間|
|---|---|---|
|[０](#ciの改善)|CIの改善なし|00:08:01|
|[１](#１キャッシュの利用-docker-layer-cachedlc)|キャッシュの利用|00:03:21|
|[２.１](#２１ciパイプラインで処理の並列化artifactsの利用)|CIパイプラインで並列化(Artifactsの利用)|00:06:50|
|[２.２](#２２ciパイプラインで処理の並列化gitlab-registoryの利用)|CIパイプラインで並列化(registoryの利用)|00:04:42|
|[３](#３buildkitの利用)|BuildKitの利用|00:04:51|

* Dockerfileの改善

|index|改善内容|イメージサイズ|CI実行時間|
|---|---|---|---|
|[-](#dockerfileの改善)|Dockerfileの改善なし|1.61 GB|00:04:51|
|[４](#４alpineベースのrubyイメージを利用して軽量化)|Alpineベースに変更|857.22 MB|00:04:04|
|[５](#５マルチステージビルドを利用して軽量化と並列化)|マルチステージビルドに変更|568.92 MB|00:03:54|


## 環境
![](https://img.shields.io/badge/docker-20.10.2-brightgreen.svg?logo=docker)![](https://img.shields.io/badge/ruby-2.6.5-brightgreen.svg?logo=ruby)![](https://img.shields.io/badge/rails-6.1.0-brightgreen.svg?logo=ruby-on-rails)![](https://img.shields.io/badge/bootstrap-4.5.3-brightgreen.svg?logo=bootstrap)![](https://img.shields.io/badge/fontawesome-5.15.1-brightgreen.svg?logo=font-awesome)![](https://img.shields.io/badge/vue.js-2.6.12-brightgreen.svg?logo=vue.js)![](https://img.shields.io/badge/postgreSQL-13.1-brightgreen.svg?logo=postgresql)![](https://img.shields.io/badge/nginx-1.19.6-brightgreen.svg?logo=nginx)

CIの実行には普段から利用させていただいており愛着のあるGitLabを利用しています。具体的に作成しているアプリの中身については触れませんが、アプリケーション、データベース、ウェブで構成され、appは、ruby on rails、vue.js、pumaとbootstrapやfontawesomeを含み、dbはpostgreSQL、webはnginxとなっております。これらはdocker-composeを利用して構成管理します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/63bb1645-a1d6-e390-1972-893536147b05.png)

Docker Composeは、app、db、webディレクトリ直下のDockerfileを使ってビルドします。

```yml:docker-compose.yml
version: "3.9"
services:
  app:
    build: 
      context: ./app
    container_name: app01
    volumes:
      - ./app:/make-it-quick
      - web-socket:/make-it-quick/tmp/sockets
    depends_on:
      - db
  db:
    container_name: db01
    build: ./db
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    container_name: web01
    build: ./web
    volumes:
      - ./web/config/nginx.conf:/etc/nginx/conf.d/make-it-quick.conf
      - web-socket:/tmp/sockets
    ports:
      - 80:80
    depends_on:
      - app

volumes:
  web-socket:
  db-data:
```

## 読み進め方
章ごとに以下のような構成で記載しています。参考実装には、改善を実施した結果のCIスクリプトもしくはDockerfileへのリンクが貼ってあります。ポイントは、主に自分が実装していく中で詰まった箇所が記載されています。ポイントで説明した箇所に関するプログラムを抜粋してありますので、説明と合わせてみてください。

-------
[参考実装] CI実行時間(00:08:01)

簡単な技術説明と技術的アプローチの説明
参考サイト様へのURLリンク

**ポイント：**
1. ~
2. ~

```
プログラム抜粋
```
------
# CIの改善
[[改善前の参考実装] CI実行時間(00:08:01)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/cd0f1a50b0706edbc89cd879f20858e16f2e6641/.gitlab-ci.yml)

まずは、工夫せずにCIを実装しますが、下記ドキュメントに記載されている通り、素のShellを利用してDockerのCIを実行するアプローチと、Docker in Docker用のdindというDockerイメージ上でDockerのCIを実行するアプローチがあり、どちらかを選択する必要があります。今回は、推奨であるdindを利用します。dindは処理速度が課題として挙げられていますので、なんとかしていきたいと思います。

[GitLab CI/CDによるDockerイメージのビルド](https://gitlab-docs.creationline.com/ee/ci/docker/using_docker_build.html)

**ポイント：**
1. dindにはDockerのツールがあらかじめ準備された状態で使用することが出来ます。しかし、docker-composeやdockerizeは含まれていませんので、準備する必要があります。
2. `docker-compose up -d`でDockerを立ち上げた直後にテストを実行すると、テストが実行できたり出来なかったりと不安定になります。これはDockerが立ち上がり切っていないことが原因です。dockerizeを利用して、立ち上がったことを確認してからテストを実行しましょう。
3. dindで、dockerizeする時のurlがlocalhostではなくdockerになっていることにも注意しましょう。

```yml:gitlab-ci.yml
image: docker:latest

services:
  - docker:dind

variables:
  DOCKERIZE_VERSION: v0.6.1 # dockerizeのバージョンを指定する

before_script:
  - (略)
  - apk add docker-compose # docker-composeを入れる
  - apk add --no-cache openssl
    && wget https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz
    && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz
    && rm dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz # dockerizeを入れる

test:
  stage: test
  script:
    - docker-compose up -d
    - dockerize -wait tcp://docker:80 -timeout 1m # dockerizeで待つ
    - docker-compose run --rm app yarn test
```

## １．キャッシュの利用 Docker Layer Cache(DLC)
[[参考実装] CI実行時間(00:03:21)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/f561283d3304db3e4bf42e576b79ff5d27651688/.gitlab-ci.yml)

Dockerでは、各コマンドはレイヤーとなり、レイヤーはキャッシュとして保存されます。そして、ビルド時にレイヤーの変更がなければ再利用できることは、様々なDocker関連記事からご存知かと思います。この仕組みにより、Dockerでは既存のイメージをキャッシュとして利用できるため、大幅な高速化が可能になります。GitLabには、Dockerのイメージを保存するためのDocker registoryがサービスとして用意されています。キャッシュ用のイメージの保存先は、DockerHubや他のレジストリでも構いません。

[DockerレイヤーキャッシングでDocker-in-Dockerビルドを高速化する](https://gitlab-docs.creationline.com/ee/ci/docker/using_docker_build.html#making-docker-in-docker-builds-faster-with-docker-layer-caching)

**ポイント：**
1. `docker build`では`--cache-from`でイメージを指定することで、指定したイメージをキャッシュとして利用できます。しかし、`docker-compose build`には、`--cache-from`が無いため、指定することが出来ません。そこで、Docker Composeで利用するイメージを`docker build`を使って指定したイメージからビルドしておき、docker-composeで立ち上げる事により、指定したイメージのキャッシュが利用できるのと同じ状態を作る事ができます。
2. Docker Composeでは、Dockerとビルド方法が異なるため、Dockerとキャッシュが異なります。そのためDockerのビルド結果をDocker Composeで使用するといった事ができません。しかし、`COMPOSE_DOCKER_CLI_BUILD`を有効にする事で、DockerのCLIが使用されるため、Dockerのビルド方法を利用できる様になります。これにより、DockerとDocker Composeでキャッシュを共用できます。
3. よし！DLC使って４分切ったから問題なし！と言う訳には行きません。テスト以外のステージにもイメージを持って回る必要があります。docker-compose内に複数のイメージがある場合、ビルド時間が多くかかってしまいます。並列化できる仕組みにしていきましょう。

```yml:gitlab-ci.yml
before_script:
  - (略)
  # Use gitlab docker registry 
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY # レジストリにログイン

test:
  stage: test
  script:
    # Caching docker layer
    - docker pull $CI_REGISTRY_IMAGE/app:latest || true # レジストリからイメージを取ってくる
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/app:latest
      --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
      --tag $CI_REGISTRY_IMAGE/app:latest
      --tag make-it-quick_app
      ./app # 取ってきたイメージをキャッシュに追加（--cache-from）してビルドする
      # 最後のタグ(make-it-quick_app)は、docker-compose up -dで利用するためのタグ
    - docker push $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/app:latest # 次のビルド時キャッシュとして利用するため、レジストリに保存
    - COMPOSE_DOCKER_CLI_BUILD=1 docker-compose up -d # Docker cliを利用
```

## ２．１．CIパイプラインで処理の並列化(Artifactsの利用)
[[参考実装] CI実行時間(00:06:50)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/33d0f3a4303e89a41757e37de91cc09840e4ed93/.gitlab-ci.yml)

3つのイメージをビルドし、テストに使いたい場合を考えます。ビルドとテストのステージを分割し、CIの前段のステージで３つのイメージを並列にビルドする。テストのステージでビルド結果を利用することでビルド時間の短縮が狙えます。ビルド結果を次のステージに渡す方法は、ビルド結果をtarに圧縮してArtifacts（成果物）として設定し、次のステージで利用します。感のいい人なら既に気づいていると思いますが、今から実施する方法は失敗です。

[Passing Docker Image between Build and Test stage in gitlab-runner](https://forum.gitlab.com/t/passing-docker-image-between-build-and-test-stage-in-gitlab-runner/2444/7)

この方法は、パイプラインを使った並列化には成功しているものの、tarに圧縮する工程、圧縮したものを展開してDockerで利用できる形にする工程で時間がかかり、実行時間は７分程度まで増加してしまいます。

```yml:gitlab-ci.yml
build-app:
  stage: build
  script:
    - (略)
    - docker build --cache-from $CI_REGISTRY_IMAGE/app:latest --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE/app:latest --tag make-it-quick_app ./app
    - (略)
    - mkdir -p docker_images
    - docker save make-it-quick_app > docker_images/make-it-quick_app.tar # tarに圧縮
  artifacts:
    paths:
      - docker_images
build-db:
(略)
build-web:
(略)

test:
  stage: test
  script:
    - docker load < docker_images/make-it-quick_app.tar # tarを展開しロード
    - docker load < docker_images/make-it-quick_db.tar
    - docker load < docker_images/make-it-quick_web.tar
```

## ２．２．CIパイプラインで処理の並列化(GitLab registoryの利用)

[[参考実装] CI実行時間(00:04:42)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/b9e3844e2b4d26025ced55a8e31d830b3cadae78/.gitlab-ci.yml)

キャッシュの利用を思い出すと簡単なことですが、次のステージでもレジストリに保存したイメージを利用すれば、テストステージで再度ビルドし直しても、レイヤーに変更が無いため、時間を消費することなく利用可能と言うことになります。このことに気がつくまでに大分時間がかかってしまいました。理解力のある人なら、すぐにこの解法に気が付いたでしょう。

**ポイント：**
1. テストステージでは、コミットハッシュのタグがついたイメージを利用するところになります。latestは、後から実行したCIによって書き換えられてしまう可能性があるからです。
2. 「キャッシュの利用」よりも１分程度の時間が伸びてしまっている理由は、マルチステージ化によって`docker pull/push`の回数が増えたことが原因となります。イメージサイズが大きくなるほど、`docker pull/push`の時間は無視できないものとなります。この問題については、Dockerイメージの改善で取り上げています。

```yml:gitlab-ci.yml
build-app:
  stage: build
  script:
    # Using DLC
    - docker pull $CI_REGISTRY_IMAGE/app:latest || true
    - docker build --cache-from $CI_REGISTRY_IMAGE/app:latest --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE/app:latest ./app
    - docker push $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA # コミットハッシュのついたイメージをレジストリに保存
    - docker push $CI_REGISTRY_IMAGE/app:latest
build-db:
(略)
build-web:
(略)

test:
  stage: test
  script:
    # コミットハッシュのついたイメージを取得
    - docker pull $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA || true
    - docker pull $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA || true
    - docker pull $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA || true
    # 再度ビルドし直すが、変更なしのためロスタイムなし
    - docker build --cache-from $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA --tag make-it-quick_app ./app
    - docker build --cache-from $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA --tag make-it-quick_db ./db
    - docker build --cache-from $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA --tag make-it-quick_web ./web
```

## ３．BuildKitの利用
[[参考実装] CI実行時間(00:04:51)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/d97bd1306aba72a37acb7af5c2555c71bb0b1f39/.gitlab-ci.yml)

BuildKitは、18.09以降のDockerから導入されたビルドツールであり、`docker build`に対して、性能、ストレージ管理、特徴的な機能性、セキュリティに関する改善に繋がります。BuildKitは、Dockerとは別のプロジェクトである[Mody BuildKit](https://github.com/moby/buildkit)で開発されたものになります。下記に記載されている通り、Dockerでは`DOCKER_BUILDKIT=1 docker build .`と環境変数を設定することでBuildKitが有効になりますが、全てのBuildKitの機能をサポートしたものではありません。さらにLinuxコンテナのみのサポートとなります。Docker 19.03から導入されたBuildxというビルドツールは、Mody BuildKitに完全対応したものになります。そして、ユーザがビルドに利用した環境（アーキテクチャやプラットフォーム）に限らず、様々なプラットフォームに対するイメージをビルドする事が可能になります。ただし、現状のBuildxは試験的な機能となっています。

* [BuildKitでイメージ構築](https://docs.docker.jp/develop/develop-images/build_enhancements.html)
* [Docker Buildx (試験的な機能)](https://matsuand.github.io/docs.docker.jp.onthefly/buildx/working-with-buildx/)


**ポイント：**
1. Dockerには`--pararel`で並列ビルドが可能になりますが、BuildKitでは標準的に並列ビルドを行います。そして、実行時間に差があり、[Docker:並列buildオプション速度比較](https://engineer.retty.me/entry/2019/12/22/183605)に記載されている通り、BuildKitの方が実行時間が短いです。
2. 環境変数でDOCKER_BUILDKITを有効にする事で`docker build`した際にBuildKitが有効になります。
3. Docker Composeでは、`COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build`として利用します。しかし、今回は`docker build`を利用するため、Docker Composeは起動のみに留めてビルドには利用していません。
4. `docker build`では`--cache-from`によりイメージをキャッシュとして利用できますが、BuildKitは`docker build`の際のキャッシュとは別の場所にキャッシュを保存します。そのため、BUILDKIT_INLINE_CACHEを有効にして、BuildKitのキャッシュを利用できるようにする必要があります。


```yml:gitlab-ci.yml
variables:
  DOCKER_BUILDKIT: 1 # BuildKitを有効化
  COMPOSE_DOCKER_CLI_BUILD: 1 # docker-composeでDocker cliを利用

build-app:
  stage: build
  script:
    # Use Docker Layer Cache (DLC) to reduce build execution time
    - docker pull $CI_REGISTRY_IMAGE/app:latest || true
    # Use BuildKit to build fast
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/app:latest
      --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
      --tag $CI_REGISTRY_IMAGE/app:latest
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./app # --build-argでBuildKitのキャッシュを利用

```
# Dockerfileの改善
[[改善前の参考実装] イメージサイズ(1.61 GB) CI実行時間(00:04:51)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/d97bd1306aba72a37acb7af5c2555c71bb0b1f39/app/Dockerfile)

現状では、Dockerfileの改善がされておらず、BuildKitを利用したことによる実行時間の改善が見られません。しかし、以降で説明するマルチステージビルドと併用する事で真価を発揮します。また、キャッシュが効きにくいシチュエーションでも同様です。まずは、CIパイプラインの課題で挙げられた`docker pull/push`の時間が無視できなくなってきている問題に取り掛かります。

## ４．AlpineベースのRubyイメージを利用して、軽量化
[[参考実装] イメージサイズ(857.22 MB) CI実行時間(00:04:04)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/bcf8dd44bde42b7d862cb7ccdec558bef495ed98/app/Dockerfile)

ご存じの通りAlpine Linuxは、イメージサイズがUbuntuと比べても小さいです。これは、最小限の機能のみに制限されていることを意味しますので、必要なパッケージは各自で追加しなければいけません。そして、Alpine Linuxをそのまま利用すると、イメージのサイズを必要最小限に留める事はできますが、Dockerfileは複雑になり、Ruby on Railsを立ち上げるだけでも一苦労します。そこで、AlpineベースのRubyイメージを利用して、効率的に改善し、素のAlpine Linuxを使った場合とそん色のないパフォーマンスを出していきます。

**ポイント：**
1. Alpine LinuxはBashではなくAshが利用されます。Dockerfile内で`ENTRYPOINT ["entrypoint.sh"]`を設定している場合、スクリプトの指定がBashになっていてビルドが通らないなんてことがあります。
2. `apk add`でパッケージの追加を行いますが、追加が必要なパッケージの選定が難しいです。はじめは、ほかの人のブログ等を参考に追加してみることをお勧めします。そして、パッケージを厳選する作業は、マルチステージビルドを追加してから実施する方が効率的です。
3. `apk add`の`--vertual`を使う事で追加するパッケージ類に仮想の名前を定義でき、`apk del`で定義した名前を指定することでパッケージを一括削除することが出来ます。不要なパッケージは削除し、イメージのサイズを抑える様にしましょう。
4. Alpineベースのイメージにしただけでイメージサイズは半減し、約５０秒程度の時間短縮となっています。それだけ`docker pull/push`に時間がかかっていたと言えます。イメージサイズを意識してDockerfileを作りましょう。

```Dockerfile:Dockerfile
FROM ruby:2.6.5-alpine3.11
LABEL maintainer="Tomoyuki Sugiyiama"

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql yarn"\
    DEV_PACKAGES="build-base" # 必要なパッケージを選択

WORKDIR /make-it-quick

RUN apk add --update --no-cache $RUNTIME_PACKAGES

COPY Gemfile /make-it-quick
COPY Gemfile.lock /make-it-quick

RUN apk add --virtual build-dependencies --no-cache $DEV_PACKAGES\
    && bundle install\
    && apk del build-dependencies # build-dependenciesを削除

(略)
```

## ５．マルチステージビルドを利用して、軽量化と並列化
[[参考実装] イメージサイズ(568.92 MB) CI実行時間(00:03:54)](https://gitlab.com/iyama_yuki/make-it-quick/-/blob/aac9383e2e0cd11e4946f70f0dc163c4c25ffdb8/app/Dockerfile)

マルチステージビルドは、複数のFROM文を定義することができ、それぞれ別のイメージとなります。そしてイメージ内で生成された内容を選び、もう一方のイメージにコピーする事が出来ます。コピーには、`COPY --from=定義したイメージ名`と記載する事で、別のイメージからコピーします。最終的なイメージは、指定が無い限り最後のFrom以降のビルド結果がイメージとして生成され、それ以外のイメージは最終的なイメージに含まれません。この仕組みにより、本当に必要なファイルのみをイメージに追加することができ、イメージサイズを抑えることが出来ます。さらに、BuildKitの並列ビルドを利用する事で、複数のFrom文を並列に実行することが出来ます。

[マルチステージビルドの利用](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)

**ポイント：**
1. `Bundle install`の時間が長いので、その間に`yarn install`も実施しています。最終成果物(以降base-devとする)のイメージについても`COPY -from`の直前まで並列でビルドされます。
2. base-devにyarnやnodeコマンドが必要な場合は、nodeイメージのbuild-jsからコピーします。base-devのビルド時に`apk add yarn`を追加しても良いですが、yarnを追加した際に一緒にインストールされたnodeのバージョンが12.xであり、`yarn install`に使用したnodeのバージョンが14.xであることから、実行時のデバッグコンソールにエラーが出力されましたので、正しいバージョンを指定して入れるようにして下さい。
3. マルチステージビルドに変更する前の、コマンドやパッケージ類のバージョンから変えてしまわないように最新の注意を払いましょう。バージョン違いで余計な手間が発生します。

```Dockerfile:Dockerfile
# Bundle install
# build-gemと定義
FROM ruby:2.6.5-alpine3.11 as build-gem
#（略）
# Yarn install
# build-jsと定義
FROM node:14.15.5-alpine3.11 as build-js
#（略）

# Create base image for develop
# 最終成果物(base-deb)
FROM ruby:2.6.5-alpine3.11 as base-dev
#（略）
# build-gemからビルドが終わったbundleを取得
COPY --from=build-gem /usr/local/bundle /usr/local/bundle
# build-jsからビルドが終わったnode_modulesを取得
COPY --from=build-js /build-js/node_modules /make-it-quick/node_modules
## Get node & yarn packages from build-js 
COPY --from=build-js /usr/local/bin/node /usr/local/bin/node
COPY --from=build-js /usr/local/include/node /usr/local/include/node
COPY --from=build-js /opt/yarn-v1.22.5/bin/yarn /opt/yarn-v1.22.5/bin/yarn
COPY --from=build-js /opt/yarn-v1.22.5/bin/yarn.js /opt/yarn-v1.22.5/bin/yarn.js
COPY --from=build-js /opt/yarn-v1.22.5/lib/cli.js /opt/yarn-v1.22.5/lib/cli.js
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
    ln -s /opt/yarn-v1.22.5/bin/yarn /usr/local/bin/yarn

```

**最終的な細かいポイント:**
1. 最後のbase-devと、base-devに受け渡すbundleやnode_modules以外は、イメージのサイズに影響しませんので、build-gemやbuild-jsの関係のないところで不要なファイルを削除してもCIの時間が伸びてしまうだけなので気をつけてください。
2. `apk add`で追加したapline-sdkにはgccやmakeなどが詰め込まれているため、パッケージの追加に結構時間がかかってしまいますが、Dockerfileの可読性維持を優先します。ここのパッケージ選択にこだわっても、多少の時間削減にしかなりません。（最終的なイメージ作成には関係なく、イメージサイズに影響が無いため）
3. `bundle install`や`yarn install`の結果からキャッシュや不要なものはオプションで削除しておきます。
4. `bundle install`は`-j4`オプションをつけて、４並列で実行しています。

```Dockerfile:Dockerfile
# Bundle install
FROM ruby:2.6.5-alpine3.11 as build-gem
RUN apk --update --no-cache add alpine-sdk postgresql-dev # ビルドに必要なツール類を追加
WORKDIR /build-gem
COPY ["Gemfile", "Gemfile.lock", "./"]
RUN gem install bundler --version 1.17.2 && \
    bundle install --without test -j4 # 不要ファイルの削除と並列実行

# Yarn install
FROM node:14.15.5-alpine3.11 as build-js

WORKDIR /build-js
COPY ["package.json", "yarn.lock", "./"]
RUN yarn install && yarn cache clean # 不要ファイルの削除

# Create base image for develop
FROM ruby:2.6.5-alpine3.11 as base-dev

RUN apk --update --no-cache add shadow sudo busybox-suid execline tzdata postgresql-dev
（略）
```
# 最後に
普段、Dockerはよく使いますが、DockerのCIは初めて実施しました。CIを実施する事で今まで知らなかったDockerに関する知識を得る事が出来ました。良い経験となりましたので、皆さまもぜひトライしてみてください。

## 最終成果物

* .gitlab-ci.yml

```yml:.gitlab-ci.yml
image: docker:latest

services:
  - docker:dind

variables:
  DOCKERIZE_VERSION: v0.6.1
  DOCKER_BUILDKIT: 1
  COMPOSE_DOCKER_CLI_BUILD: 1

before_script:
  - docker -v
  - apk update
  - apk upgrade
  - apk add docker-compose
  - apk add --no-cache openssl
    && wget https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz
    && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz
    && rm dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz
  # Use gitlab docker registry 
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build-app:
  stage: build
  script:
    # Use Docker Layer Cache (DLC) to reduce build execution time
    - docker pull $CI_REGISTRY_IMAGE/app:latest || true
    # Use BuildKit to build fast
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/app:latest
      --tag $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
      --tag $CI_REGISTRY_IMAGE/app:latest
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./app
    - docker push $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/app:latest

build-db:
  stage: build
  script:
    # Use Docker Layer Cache (DLC) to reduce build execution time
    - docker pull $CI_REGISTRY_IMAGE/db:latest || true
    # Use BuildKit to build fast
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/db:latest
      --tag $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA
      --tag $CI_REGISTRY_IMAGE/db:latest
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./db
    - docker push $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/db:latest

build-web:
  stage: build
  script:
    # Use Docker Layer Cache (DLC) to reduce build execution time
    - docker pull $CI_REGISTRY_IMAGE/web:latest || true
    # Use BuildKit to build fast
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/web:latest
      --tag $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA
      --tag $CI_REGISTRY_IMAGE/web:latest
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./web
    - docker push $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE/web:latest

test:
  stage: test
  script:
    # Pulling docker images from gitlab docker registry
    - docker pull $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA || true
    - docker pull $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA || true
    - docker pull $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA || true
    # These images are pre-built at build stages
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
      --tag make-it-quick_app
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./app
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/db:$CI_COMMIT_SHA
      --tag make-it-quick_db
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./db
    - docker build
      --cache-from $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA
      --tag make-it-quick_web
      --build-arg BUILDKIT_INLINE_CACHE=1
      ./web
    # Use the Docker CLI when executing a build
    - docker-compose up -d
    # Wait for docker-compose up
    - dockerize -wait tcp://docker:80 -timeout 1m 
    - docker-compose run --rm app rake db:create
    - docker-compose run --rm app rails db:migrate
    - docker-compose run --rm app yarn test

```

* Dockerfile

```Dockerfile:Dockerfile
# Bundle install
FROM ruby:2.6.5-alpine3.11 as build-gem

RUN apk --update --no-cache add alpine-sdk postgresql-dev
WORKDIR /build-gem
COPY ["Gemfile", "Gemfile.lock", "./"]
RUN gem install bundler --version 1.17.2 && \
    bundle install --without test -j4

# Yarn install
FROM node:14.15.5-alpine3.11 as build-js

WORKDIR /build-js
COPY ["package.json", "yarn.lock", "./"]
RUN yarn install && yarn cache clean

# Create base image for develop
FROM ruby:2.6.5-alpine3.11 as base-dev
LABEL maintainer="Tomoyuki Sugiyiama"

RUN apk --update --no-cache add shadow sudo busybox-suid execline tzdata postgresql-dev
WORKDIR /make-it-quick
COPY --from=build-gem /usr/local/bundle /usr/local/bundle
COPY --from=build-js /build-js/node_modules /make-it-quick/node_modules
## Get node & yarn packages from build-js 
COPY --from=build-js /usr/local/bin/node /usr/local/bin/node
COPY --from=build-js /usr/local/include/node /usr/local/include/node
COPY --from=build-js /opt/yarn-v1.22.5/bin/yarn /opt/yarn-v1.22.5/bin/yarn
COPY --from=build-js /opt/yarn-v1.22.5/bin/yarn.js /opt/yarn-v1.22.5/bin/yarn.js
COPY --from=build-js /opt/yarn-v1.22.5/lib/cli.js /opt/yarn-v1.22.5/lib/cli.js
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
    ln -s /opt/yarn-v1.22.5/bin/yarn /usr/local/bin/yarn

COPY . /make-it-quick
## Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
RUN mkdir -p /make-it-quick/tmp/sockets
CMD bundle exec puma -C config/puma.rb
```

## おまけ
最後までお付き合いいただいた方へ、僕からのささやかのプレゼント（お得情報）です。

* イメージサイズを極限まで押さえたい方は、duしていきましょう。nodeコマンドって72Mもあるんですね。

```powershell

/make-it-quick # du / -hd 1
92.0K   /var
0       /dev
359.8M  /usr
5.0M    /lib
0       /proc
0       /sys
16.0K   /media
4.0K    /mnt
1.5M    /etc
8.0K    /run
56.0K   /tmp
5.1M    /opt
4.0K    /home
8.0K    /root
4.0K    /srv
392.0K  /sbin
1.9M    /bin
299.9M  /make-it-quick
673.7M  /

/usr/local # du ./ -hd 2
8.0K    ./lib/pkgconfig
25.6M   ./lib/ruby
34.5M   ./lib
16.0K   ./share/.cache
4.0K    ./share/ca-certificates
220.0K  ./share/man
244.0K  ./share
72.3M   ./bin
924.0K  ./include/ruby-2.6.0
7.0M    ./include/node
7.9M    ./include
288.0K  ./bundle/specifications
188.6M  ./bundle/gems
15.4M   ./bundle/cache
4.0K    ./bundle/doc
8.4M    ./bundle/extensions
4.0K    ./bundle/build_info
64.0K   ./bundle/bin
212.8M  ./bundle
8.0K    ./etc
327.7M  ./

/usr/local/bin # ls
bundle   bundler  erb      gem      irb      node     nodejs   rake     rdoc     ri       ruby     yarn
/usr/local/bin # du ./ -hd 1
72.3M   ./
/usr/local/bin # du ./node -hd 1
72.1M   ./node
```


* [Dockerfileの可視化ツール](https://github.com/po3rin/dockerdot)を利用して、分析しつつ楽しみましょう。

![dockerfile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/afca0e73-6313-308d-4f92-95ba3be85ea3.png)