👷‍♂️

Rails 7.1 の Dockerfile を試してみる

2023/10/13に公開

Ruby on Rails 7.1が正式公開されたので触ってみます。
https://railsguides.jp/v7.1/7_1_release_notes.html

今回は、新たに rails new で作成されるようになった Dockerfile について見てみます。

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
$ gem install rails -v 7.1.1 --no-document
$ rails -v
Rails 7.1.1
$ rails new moblog
$ Code moblog

Rails 7.1 の Dockerfile

Rails 7.1 で rails new したときの Dockerfile は以下の内容です。開発用ではなく本番環境用ですね。

# syntax = docker/dockerfile:1

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base

# Rails app lives here
WORKDIR /rails

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"


# Throw-away build stage to reduce size of final image
FROM base as build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libvips pkg-config

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile


# Final stage for app image
FROM base

# Install packages needed for deployment
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Copy built artifacts: gems, application
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN useradd rails --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER rails:rails

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server"]

ローカルで起動してみる

試しにこれを使って作成したアプリケーションを起動してみます。
Rails 7.1ではslite3データベースファイルは /db でなく /storage に作成されるようになっています。(コミット:Use storage/ instead of db/ for sqlite3 db files (#46699)

$ bin/rails g scaffold Post body:text
$ bin/setup
$ docker build -t moblog .
$ docker volume create moblog-storage
$ docker run --rm -it -v moblog-storage:/rails/storage -p 3000:3000 --env RAILS_MASTER_KEY=`cat config/master.key` moblog
W, [2023-10-13T07:13:34.531872 #7]  WARN -- : You are running SQLite in production, this is generally not recommended. You can disable this warning by setting "config.active_record.sqlite3_production_warning=false".
I, [2023-10-13T07:13:34.543185 #7]  INFO -- : Migrating to CreatePosts (20231013071205)
== 20231013071205 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0004s
== 20231013071205 CreatePosts: migrated (0.0005s) =============================

=> Booting Puma
=> Rails 7.1.1 application starting in production 
=> Run `bin/rails server --help` for more startup options
W, [2023-10-13T07:13:34.937148 #1]  WARN -- : You are running SQLite in production, this is generally not recommended. You can disable this warning by setting "config.active_record.sqlite3_production_warning=false".
[1] Puma starting in cluster mode...
[1] * Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
[1] *  Min threads: 5
[1] *  Max threads: 5
[1] *  Environment: production
[1] *   Master PID: 1
[1] *      Workers: 10
[1] *     Restarts: () hot () phased
[1] * Listening on http://0.0.0.0:3000
[1] Use Ctrl-C to stop
[1] - Worker 0 (PID: 11) booted in 0.01s, phase: 0
[1] - Worker 1 (PID: 13) booted in 0.01s, phase: 0
[1] - Worker 2 (PID: 15) booted in 0.01s, phase: 0
[1] - Worker 3 (PID: 19) booted in 0.01s, phase: 0
[1] - Worker 4 (PID: 35) booted in 0.01s, phase: 0
[1] - Worker 5 (PID: 51) booted in 0.01s, phase: 0
[1] - Worker 6 (PID: 64) booted in 0.01s, phase: 0
[1] - Worker 7 (PID: 70) booted in 0.0s, phase: 0
[1] - Worker 8 (PID: 84) booted in 0.0s, phase: 0
[1] - Worker 9 (PID: 97) booted in 0.0s, phase: 0

起動時に bin/docker-entrypoint により bin/rails db:prepare が実行されていますね。
これはデータベースが存在しない場合はデータベースを作成してスキーマを読み込みseedを実行し、存在するときはmigrateを実行するものです。

このまま http://localhost:3000 にアクセスしても、 config.force_ssl = true のため https://localhost:3000 にリダイレクトされてしまい puma のログに 2023-10-13 06:54:45 +0000 HTTP parse error, malformed request: #<Puma::HttpParserError: Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?> と記録されてしまいます。本番では前段にロードバランサーやリバースプロキシを配置するので問題にならないのですが、ローカルではちょっと面倒ですね。
今回はNgrokを使って転送します。

$ ngrok http 3000 --region jp
ngrok                                                                                                                    (Ctrl+C to quit)
                                                                                                                                         
Introducing Always-On Global Server Load Balancer: https://ngrok.com/r/gslb                                                              
                                                                                                                                         
Session Status                online                                                                                                     
Account                       Yuichi Takeuchi (Plan: Pro)                                                                                
Update                        update available (version 3.3.5, Ctrl-U to update)                                                         
Version                       3.1.1                                                                                                      
Region                        Japan (jp)                                                                                                 
Latency                       -                                                                                                          
Web Interface                 http://127.0.0.1:4040                                                                                      
Forwarding                    https://f6a50b7d8fda.ngrok.app -> http://localhost:3000

転送URLを使ってアクセスしてみます。

Render.com で起動してみる

Rander.comでRailsを起動するには通常 Native Runtime を使いますが、Dockerfileからビルド&デプロイして使うこともできます。

Web Service を作成

Render.comにログインして New => Web Service をクリック

デプロイ元はGitリポジトリを使います。

設定

Web Serviceの設定します。
/rails/storage は SQLiteデータベースやActiveStorageに使うので永続化しますが、これには Render Disks を使います。サービス作成時に設定することで自動でマウントしてくれるようになります。

ビルド・起動

Web Serviceを作成すると自動でビルドが始まるので、しばらく待ちます。

無事起動しました。

Kamalでデプロイしてみる

せっかくなのでコンテナ用のCapistranoというKamalも試してみます。
https://techracho.bpsinc.jp/hachi8833/2023_08_25/127981

デプロイ先の準備

Dockerが利用できるサーバなら何でもよさそうですが、今回はさくらのクラウド上の Ubuntu Server 22.04 を使ってみます。

SSHキーペアの作成

$ ssh-keygen -t ed25519 -C "yuichi.takeuchi@takeyuweb.co.jp" -f ~/.ssh/moblog-demo.id_rsa

公開鍵をサーバ側においておきます。

$ cat ~/.ssh/moblog-demo.id_rsa.pub

SSH接続設定

$ vi ~/.ssh/config
Host moblog-demo
        HostName 153.120.18.91
        User ubuntu
        Port 22
        IdentityFile ~/.ssh/moblog-demo.id_rsa
        IdentitiesOnly yes

接続できることを確認します。

$ ssh moblog-demo
The authenticity of host '153.120.18.91 (153.120.18.91)' can't be established.
ED25519 key fingerprint is SHA256:UHXXcBzWBaDwhtToerB8NMndWRzgZYXYyPLfbbmswHo.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '153.120.18.91' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-78-generic x86_64)

bootstrap実行

kamalはデフォルトでrootを使いますが、今回はrootでのssh接続はできません。その場合は事前に手動でDockerのインストールなどをしておく必要があるようです。
https://kamal-deploy.org/docs/configuration

リモートで

ubuntu@xxxxx:~$ sudo apt update
ubuntu@xxxxx:~$ sudo apt upgrade -y
ubuntu@xxxxx:~$ sudo apt install -y docker.io curl git
ubuntu@xxxxx:~$ sudo usermod -a -G docker ubuntu

コンテナレジストリの作成

Docker HubやECRなどコンテナイメージのアップロード先を作成します。
今回はさくらのクラウドのコンテナレジストリを試してみます。

さくらのクラウド > LAB > コンテナレジストリ

作成したコンテナレジストリを選択 > ユーザ でアクセスに使用するユーザ名とパスワードを登録

Kamalのセットアップ

まずはkamalコマンドをインストールします。

$ gem install kamal

Railsプロジェクトのルートに移動し、 kamal init --bundle すると、設定ファイルなどが作成されます。

$ cd RAILS_ROOT
$ kamal init --bundle
reated configuration file in config/deploy.yml
Created .env file
Created sample hooks in .kamal/hooks
Adding Kamal to Gemfile and bundle...
  INFO [f3ee60bf] Running /usr/bin/env bundle add kamal as yuichi@localhost
  INFO [f3ee60bf] Finished in 2.273 seconds with exit status 0 (successful).
  INFO [d59db3cb] Running /usr/bin/env bundle binstubs kamal as yuichi@localhost
  INFO [d59db3cb] Finished in 0.109 seconds with exit status 0 (successful).
Created binstub file in bin/kamal

Kamalの設定

config/deploy.yml を開き、デプロイ先などの設定を変更します。

  • デプロイ先のサーバー名は ~/.ssh/config に記載したホスト名にしました。
  • イメージ保存先のコンテナレジストリはさくらのクラウドにしました。
  • storage ディレクトリ
service: moblog-demo

# Name of the container image.
image: rails-moblog-demo/app

# Deploy to these servers.
servers:
  - moblog-demo

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  server: moblog-demo.sakuracr.jp
  username: moblogdemouser

  # Always use an access token rather than real password when possible.
  password:
    - KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
# env:
#   clear:
#     DB_HOST: 192.168.0.2
#   secret:
#     - RAILS_MASTER_KEY
env:
  secret:
    - RAILS_MASTER_KEY

# Use a different ssh user than root
ssh:
  user: ubuntu

# サーバ上のディレクトリをマウント(事前に作っておく)
volumes:
  - "./moblog-demo-storage:/rails/storage"
ローカルで使う環境変数

Docker Hubのパスワード(アクセストークン)は .env に書いておきます。
Docker Hub上で Account Settings > Security > Access Tokens で作成できます。

# .env
KAMAL_REGISTRY_PASSWORD=change-this
RAILS_MASTER_KEY=another-env
リモートに送る環境変数

RAILS_MASTER_KEY は env:RAILS_MASTER_KEY と記載した上で、 .env に書いておきます。
こうすることで、kamal によってサーバ上の .kamal/env/roles/moblog-demo-web.env に、 RAILS_MASTER_KEY が設定されます。(ローカルに作った .env がそのまま配置されるわけではない)

ボリュームのマウント

コンテナ内の /rails/storage ディレクトリは永続化する必要があるので、 volumes: で指定しています。
このように指定することでコンテナ起動時のオプションが追加されます。

https://kamal-deploy.org/docs/configuration

Kamalの実行

setup

bin/kamal setup を実行すると、

  • ローカルでのappイメージの build と push
  • リモートへのenvファイルの配置(ローカルの.envとは別)
  • リモートでのリバースプロキシ traefik コンテナの作成
  • appイメージを pull して起動しヘルスチェック実行

などが行われます。
ヘルスチェックに成功すると完了します。

$ bin/kamal setup
  INFO [fe523bf0] Running /usr/bin/env mkdir -p .kamal on moblog-demo
  INFO [fe523bf0] Finished in 1.080 seconds with exit status 0 (successful).
Acquiring the deploy lock...
Ensure Docker is installed...
  INFO [2cdd9cb3] Running docker -v on moblog-demo
  INFO [2cdd9cb3] Finished in 0.155 seconds with exit status 0 (successful).
  INFO [ba1ed433] Running /usr/bin/env mkdir -p .kamal on moblog-demo
  INFO [ba1ed433] Finished in 0.112 seconds with exit status 0 (successful).
Push env files...
  INFO [d5d2607b] Running /usr/bin/env mkdir -p .kamal/env/roles on moblog-demo
  INFO [d5d2607b] Finished in 0.111 seconds with exit status 0 (successful).
  INFO Uploading .kamal/env/roles/moblog-demo-web.env 100.0%
  INFO [c9bbd2ca] Running /usr/bin/env mkdir -p .kamal/env/traefik on moblog-demo
  INFO [c9bbd2ca] Finished in 0.113 seconds with exit status 0 (successful).
  INFO Uploading .kamal/env/traefik/traefik.env 100.0%
Log into image registry...

中略

  INFO [bb3423c3] Finished in 3.463 seconds with exit status 0 (successful).
  Finished all in 100.3 seconds
Releasing the deploy lock...
  Finished all in 102.7 second

http://サーバのIPアドレス でアクセスできました。

deploy

変更を反映するには kamal deploy を使います。env:の変更がある場合は、事前に kamal env push が必要とのこと。

$ bin/kamal env push
$ bin/kamal deploy
  INFO [5cc64564] Running /usr/bin/env mkdir -p .kamal on moblog-demo
  INFO [5cc64564] Finished in 1.271 seconds with exit status 0 (successful).
Acquiring the deploy lock...
Log into image registry..

中略

  INFO [469f18d4] Finished in 0.285 seconds with exit status 0 (successful).
Releasing the deploy lock...
  Finished all in 109.0 seconds

起動中のコンテナにログイン

bin/kamal app exec -i --reuse bash

--reuse をつけない場合は新しくコンテナを起動してログインします。

トラブルシューティング

アーキテクチャが一致しない

Gemfile.lock 記載のCPUアーキテクチャと bundle install した環境のアーキテクチャの不一致らしい。
エラーメッセージにあるように bundle lock --add-platform アーキテクチャ名 して再試行。

#20 45.07 Your bundle only supports platforms ["x86_64-linux"] but your local platform is
#20 45.07 aarch64-linux. Add the current platform to the lockfile with
#20 45.07 `bundle lock --add-platform aarch64-linux` and try again.
------
Dockerfile:26
--------------------
  25 |     COPY Gemfile Gemfile.lock ./
  26 | >>> RUN bundle install && \
  27 | >>>     rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
  28 | >>>     bundle exec bootsnap precompile --gemfile
  29 |     
--------------------
ERROR: failed to solve: process "/bin/sh -c bundle install &&     rm -rf ~/.bundle/ \"${BUNDLE_PATH}\"/ruby/*/cache \"${BUNDLE_PATH}\"/ruby/*/bundler/gems/*/.git &&     bundle exec bootsnap precompile --gemfile" did not complete successfully: exit code: 16

本番環境にHTTPでアクセスしたい

rails new で作成したままだと config.force_ssl = true なので、 kamal(traefik)の80番ポート(HTTP)では 443番へのリダイレクトが発生してしまいますが、自前でHTTPSアクセスできるようにするのは面倒です。
手っ取り早くアクセスできるようにするには

  # Assume all access to the app is happening through a SSL-terminating reverse proxy.
  # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
  # config.assume_ssl = true

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = false

とします。

まとめ

これまで本番用に各々自前の Dockerfile や関連ファイルを構成していましたが、標準ができたことで今後はそちらに寄せる判断ができ、プロジェクト固有の秘伝のタレを減らすことができそうです。

Dockerfile 自体は特別なことはしていないので、Kamalの他、さまざまなツールやプラットホームで動作させることができます。

弊社としてはRailsを使う以上はなるべく標準に寄せた方がよいと考えているので、今後はうまく活用していきたいと思います。

タケユー・ウェブ株式会社

Discussion