Rails の Docker Image を Cloud Native Buildpacks で作る

6 min read読了の目安(約5700字

はじめに

プライベートで開発している Rails プロジェクトの Docker image を作りたかったのでやってみました。
普通に Dockerfile を書いて作っても良いのですが、ベストプラクティスを調べて維持し続けるのが面倒だったので以前から気になっていた Cloud Native Buildpacks を使ってみました。

Cloud Native Buildpacks

Cloud Native Buildpacks (CNB) を使うと Dockerfile を書くことなくソースコードから Docker image を作ることができます。

Heroku を使っている方は buildpacks という用語に馴染みがあると思いますが、CNB は heroku で作られたこの buildpacks の仕組みを標準化し、ベンダーロックインなしで使えるようにしたものです。

https://blog.heroku.com/buildpacks-go-cloud-native

Docker image にビルドするための仕組みは builder, buildpacks としてコミュニティによってメンテナンスされているため、Dockerfile のベストプラクティスやセキュリティ対策を自分ひとりで追わなくても良いメリットがあります。

ちょっと前に Google Cloud も全体的に CNB を採用したってアナウンスしていましたね。

https://cloud.google.com/blog/ja/products/containers-kubernetes/google-cloud-now-supports-buildpacks

準備

pack というコマンドをインストールする必要があります。
https://buildpacks.io/docs/tools/pack/ に従ってインストールするだけです。

早速ビルドしてみる

今回検証に使った Rails アプリについて

  • Rails 6.1.3.2
  • Vite Ruby を使って React でフロントエンドを書いています
    • 多分今回の検証に関しては特に Vite や Vite Ruby 特有の何かは無く、webpacker を使っているプロジェクトでも参考になるかと思います

ビルド

これでビルドできます。

$ pack build -e NODE_ENV=development --builder heroku/buildpacks:20 --buildpack heroku/nodejs,heroku/ruby app

heroku に push したときみたいにズラズラとログが流れ、最終的に Successfully built image app と出れば成功です。
以下、少しオプションについて補足します。

builder, buildpacks について

pack builder suggest と打つと以下のように出力されると思います。

$ pack builder suggest                                                                                         
Suggested builders:
        Google:                gcr.io/buildpacks/builder:v1      Ubuntu 18 base image with buildpacks for .NET, Go, Java, Node.js, and Python                                              
        Heroku:                heroku/buildpacks:18              Base builder for Heroku-18 stack, based on ubuntu:18.04 base image                                                        
        Heroku:                heroku/buildpacks:20              Base builder for Heroku-20 stack, based on ubuntu:20.04 base image                                                        
        Paketo Buildpacks:     paketobuildpacks/builder:base     Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, Ruby, NGINX and Procfile                        
        Paketo Buildpacks:     paketobuildpacks/builder:full     Ubuntu bionic base image with buildpacks for Java, .NET Core, NodeJS, Go, PHP, Ruby, Apache HTTPD, NGINX and Procfile     
        Paketo Buildpacks:     paketobuildpacks/builder:tiny     Tiny base image (bionic build image, distroless-like run image) with buildpacks for Java Native Image and Go              

Tip: Learn more about a specific builder with:
        pack builder inspect <builder-image>

今回は自分が heroku に慣れているということもあり、多分共通の知識が役に立つんだろうな。。と予想して heroku/buildpacks:20を選択しました。Paketo は Cloud Foundry (懐かしい)由来なんでしょうか。調べてなくてちょっとわからないです。

そして buildpacks ですが明示的に heroku/nodejs も指定してあげないとダメでした。heorku と同じように勝手に nodejs も入れてくれるわけではないんですね。

そして一つハマりどころがあって、ビルド時の環境変数として NODE_ENV=development を指定しないと nodejs のビルド後に package.json の devDependencies が削除されてしまい asset:precompile 時に vite コマンドが見つからなくてビルドにこける問題がありました。

https://github.com/heroku/buildpacks-nodejs/blob/46dec4f3d9be21d320b6bb912061f1b7a0c63c05/buildpacks/npm/lib/build.sh#L230-L242

他にもビルド時に必要な環境変数があったらここで指定してください。
例えば vite 使ってるなら VITE_* とか。

https://vitejs.dev/guide/env-and-mode.html

project.toml を使う

あまりこだわりがなければもう先程のコマンドで十分なのですが、README.md だったり .git だったり、本来実行時に不要なものも Docker image に入ってしまいます。
それらを取り除こうと思うと project.toml を使って exclude を指定する必要があるようなのでやってみました。

project.toml
[project]
id = "dev.ebisawa.hoge"
name = "hoge"
version = "1.0.0"

[build]
exclude = [
  ".env*",
  ".foreman",
  ".bundle",
  ".git",
  ".github",
  ".vscode",
  ".byebug_history",
  ".DS_Store",
  "node_modules",
  "tmp",
]

[[build.buildpacks]]
uri = "heroku/nodejs"

[[build.buildpacks]]
uri = "heroku/ruby"

[[build.env]]
name = 'NODE_ENV'
value = 'development'

これを置いた状態で以下のようなコマンドでいけます。

$ pack build --builder heroku/buildpacks:20 app

動かす

普通に Docker image なので docker compose でも docker コマンドでも普通に動かせます。
環境変数とかはいい感じに調整してください。

$ docker run --rm -p 3000:3000 --env-file ./.env app 

Procfile を置いておくと heroku/procfile buildpacks のおかげで自動的にその内容に応じた entrypoint を作ってくれて便利です。

Procfile が以下のようになってる場合

release: bin/rails db:migrate
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
$ pack inspect app
Inspecting image: app

REMOTE:
(not present)

LOCAL:

Stack: heroku-20

Base Image:
  Reference: xxxxxx
  Top Layer: sha256:xxxxxx

Run Images:
  heroku/pack:20

Buildpacks:
  ID                              VERSION        HOMEPAGE
  heroku/nodejs-engine            0.7.3          -
  heroku/nodejs-npm               0.4.3          -
  heroku/nodejs-typescript        0.2.2          -
  heroku/procfile                 0.6.2          -
  heroku/ruby                     0.0.1          -

Processes:
  TYPE                 SHELL        COMMAND                                                ARGS
  web (default)        bash         bin/rails server -p ${PORT:-5000} -e $RAILS_ENV        
  console              bash         bin/rails console                                      
  rake                 bash         bundle exec rake                                       
  release              bash         bin/rails db:migrate                                   
  worker               bash         bundle exec sidekiq

Processes に worker や release が定義されているのがわかると思います。
(web は上書きされちゃうみたいですね。)

$ docker run --rm -p 3000:3000 --env-file ./.env --entrypoint worker app

終わりに

あとは必要に応じて GitHub Actions あたりを使って CI/CD するようにすると良さそうです!