Ruby on Rails 7.1 with Bootstrap on Docker Compose 開発環境を構築する方法

## はじめに

前回は、Rails 7.1 により自動生成された `Dockerfile` の中身を一通り確認してみましたが、

https://qiita.com/normalsalt/items/0bea5f2efb76fa71dd61

今回は、いよいよ、実際にコンテナを起動してみて、動作確認を行なっていきます。[^1]

[^1]: ちなみに、今回、利用した PC は、MacBook Pro（ 13-inch, M1, 2020 ）で、macOS Sonoma 14.1.1 環境です。

と言っても、自動生成される `Dockerfile` は、本番環境での使用に向けて調整されたものになっているので、まずは、開発環境として利用するために、少し弄らせてもらうことにしましょう。

## 改めて `rails new` を実行しよう

`Dockerfile` を自動生成する際の（ 特に CSS フレームワークをオプションで指定する場合の ）注意点については、すでに別の記事にも書いているので、詳細は割愛しますが、

https://qiita.com/normalsalt/items/af06ecf39da848157e4f

結論から言えば、以下のような手順で、必要なファイル群を自動生成（ `rails new` ）していきます。

### Rails 環境の確認

ここでは、`rails new` するためだけに、次のようなコンテナを利用しますが、適宜、自分の環境で、各種ツールのバージョンを確認してもらえれば、それでも構いません。

```bash
docker run --rm -v .:/content -w /content -it ruby:3.2.2 bash
```

ともかく、`rails` をインストール、もしくは `7.1` に更新して、バージョンを確認しておいてください。

```bash
gem install rails
```

↓（ バージョンを確認 ）

```bash
# rails --version
Rails 7.1.2
```

### Node 環境の確認

今回は、`rails new` を実行する際、`css` オプションで Bootstrap を指定する予定なので、Node 環境も確認しておきます。[^2]

[^2]: 詳細については、[以前の記事](https://qiita.com/normalsalt/items/af06ecf39da848157e4f) もご参照ください。

お好みに合わせて、バージョンを調整しておきましょう。

```bash
apt-get update -qq && apt-get install --no-install-recommends -y npm
npm install -g n
n lts
```

↓（ バージョンを確認 ）

```bash
# node --version
v20.9.0
```

### Yarn 環境の確認

同様に、とりあえず、Yarn 環境もあった方が良さそうなので、必要に応じて、確認しておいてください。[^2]

```bash
npm install --global yarn
```

↓（ バージョンを確認 ）

```bash
# yarn --version
1.22.21
```

### `rails new` の実行

さて、環境は整ったので、そろそろ `rails new` を実行していきます。

```bash
rails new . --database=postgresql --css=bootstrap
```

ちなみに、オプションとして、データベースには、PostgreSQL も指定しておきました。

これで、各種ファイルが、正常に生成されましたでしょうか？

## 開発環境用の `Dockerfile` も作ろう

コンテナ内で作業していたとしても、ちゃんとマウントされていれば、ローカルにも、以下のようなファイル群が生成されているはずです。

```zsh
% ls
.dockerignore   .gitignore      Dockerfile      Procfile.dev    app/            config.ru       log/            public/         tmp/
.git/           .node-version   Gemfile         README.md       bin/            db/             node_modules/   storage/        vendor/
.gitattributes  .ruby-version   Gemfile.lock    Rakefile        config/         lib/            package.json    test/           yarn.lock
```

それでは、次のようにコピーして、本番環境用の `Dockerfile.prod` も確保しつつ、開発用として `Dockerfile` を編集していきたいと思います。

```zsh
cp Dockerfile Dockerfile.prod
```

試しに「 本番環境での使用に向けて調整されている部分を、あえて逆調整する 」なら、例えば、以下のような感じになるでしょうか？

```Dockerfile: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

# Install packages needed to build gems and node modules
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential curl git libpq-dev libvips node-gyp pkg-config python-is-python3 postgresql-client

# Install JavaScript dependencies
ARG NODE_VERSION=20.9.0
ARG YARN_VERSION=1.22.21
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
    /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
    npm install -g yarn@$YARN_VERSION && \
    rm -rf /tmp/node-build-master

# 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

# Install node modules
COPY package.json yarn.lock ./
RUN yarn install

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

# 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"]
```

本番環境用の環境変数の設定とか、マルチステージビルドだとか、non-root ユーザーの作成などといった部分を、単に、根こそぎ削除してみました。[^3]

[^3]: 元の `Dockerfile` の内容については、[前回の記事](https://qiita.com/normalsalt/items/0bea5f2efb76fa71dd61) をご参照ください。

今回は、これでうまく動くかどうか、ビルドしてみるとしましょう。

## `compose.yaml` を作成しよう

次に、Rails と PostgreSQL のコンテナを、Docker Compose を使って、簡単に立ち上げ、連携させるために、`compose.yaml` ファイルを作成していきます。

この辺については、昔からお世話になっている「 Quickstart: Compose and Rails 」が、未だに参考になりますね。[^4]

https://docs.docker.com/samples/rails/

[^4]: ただし、今回の `Dockerfile` では、Rails の `WORKDIR` に `/rails` を利用しているので、`compose.yaml` の `volumes` の指定と合うよう、調整が必要です。

```yaml:compose.yaml
services:
  db:
    image: postgres:16.1-alpine
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/rails
    ports:
      - "3000:3000"
    depends_on:
      - db
```

一応、PostgreSQL [^5] のバージョンも、固定しておきました。[^6]

[^5]: https://hub.docker.com/_/postgres

[^6]: 長らく `latest` を使っていると、ふいにメインバージョンを上げてしまって、マウントしているデータとの整合性に問題が発生し、起動できなくなってしまう可能性があるので、ご注意ください。

関連して、`config/database.yml` を編集し、データベースの設定も整えておきましょう。

```diff_yaml:config/database.yml
(...)

default: &default
  adapter: postgresql
  encoding: unicode
+ host: db
+ username: postgres
+ password: password
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: content_development

(...)
```

要するに、以下の三行を追加するだけですね。

```yaml
  host: db
  username: postgres
  password: password
```

## コンテナを起動しよう

それでは、次のコマンドを実行して（ ビルドしつつ ）コンテナを起動しましょう。

```
docker compose up -d --build
```

ここまでで、起動自体はできているはずですが、接続するデータベースが存在しないとエラーになってしまうので、次のコマンドで作成しておきます。

```
docker compose run --rm web rails db:create
```

そして、http://localhost:3000 にアクセスし、ちゃんと Rails が動いているかどうか、確認してみてください。

次のような「 ウェルカムページ 」が表示されれば、とりあえずは、成功です。

![Screenshot 2023-11-15 at 17-24-11 Ruby on Rails 7.1.2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109175/30a71ee0-4711-ed8d-40c6-0ef3256fb2d9.png)

ただ、これだけだと「 本当に Bootstrap が使えるようになっているのか？ 」までは分かりませんし、今後、その他の確認を行なっていくためにも、次のようなコマンドを実行するなりして、適当なページを作成しておきます。

```
docker compose run --rm web rails generate controller Articles index
```

## Bootstrap も動作確認しよう

最後に、Bootstrap が使えるようになっているかどうかを確認するため、例として `app/views/layouts/application.html.erb` を編集し、Navbar を導入してみましょう。

https://getbootstrap.com/docs/5.3/components/navbar/

ほとんどコピペですが、以下のように、`body` を更新してみてください。

```erb:app/views/layouts/application.html.erb
(...)

  <body>
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
      <div class="container-fluid">
        <a class="navbar-brand" href="#">Navbar</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="#">Home</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">Link</a>
            </li>
            <li class="nav-item dropdown">
              <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                Dropdown
              </a>
              <ul class="dropdown-menu">
                <li><a class="dropdown-item" href="#">Action</a></li>
                <li><a class="dropdown-item" href="#">Another action</a></li>
                <li><hr class="dropdown-divider"></li>
                <li><a class="dropdown-item" href="#">Something else here</a></li>
              </ul>
            </li>
            <li class="nav-item">
              <a class="nav-link disabled" aria-disabled="true">Disabled</a>
            </li>
          </ul>
          <form class="d-flex" role="search">
            <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
            <button class="btn btn-outline-success" type="submit">Search</button>
          </form>
        </div>
      </div>
    </nav>
    <div class="container-fluid mt-2">
      <%= yield %>
    </div>
  </body>
</html>
```

これで、http://localhost:3000/articles/index にアクセスすると、次のように表示されるでしょうか？

![Screenshot 2023-11-15 at 17-41-33 Content.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109175/d8c89f61-b60e-7f87-3adc-e809c76ccc0d.png)

ちなみに、自動的にインストールされた Bootstrap のバージョンについては、`package.json` や `yarn.lock` ファイルを参照するか、以下のようにコマンドを実行するなどして、確認することができます。

```zsh
% docker compose run --rm web npm show bootstrap version
5.3.2
```

### import maps について

ところで、上で追加した Navbar ですが、Dropdown を押してみてください。ちゃんと機能しているでしょうか？

ここまで、全く同じように作業してきた場合、おそらく、CSS については、確かにコンパイルされているのですが、実は、Bootstrap が利用する JavaScript の方は、たぶん読み込まれていません。

ブラウザのデベロッパーツールにも、次のようなエラーが出ているはずです。

```
Uncaught TypeError: Failed to resolve module specifier "bootstrap". Relative references must start with either "/", "./", or "../".
```

Rails 7 からデフォルトになったらしい、import maps [^7] の設定ファイルを確認してみると、やはり Bootstrap は含まれていないようですね。

[^7]: https://github.com/rails/importmap-rails

```rb:config/importmap.rb
# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
```

いずれは、自動的に追加されるようになるかもしれませんが、例えば、次のようなコマンドを実行すれば、Bootstrap を追加することができます。

```zsh
docker compose run --rm web bin/importmap pin bootstrap
```

これで、上の設定ファイルに、以下の二行が追記され、Dropdown も動くようになっているはずです。

```rb
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.3.2/dist/js/bootstrap.esm.js"
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js"
```

場合によっては、import maps さえあれば、Node や Yarn は必要ないようですし、JavaScript 周りについては、改めて整理した方が良さそうですね。

詳細は、Rails ガイドなどを参照してください。

https://railsguides.jp/v7.1/working_with_javascript_in_rails.html

## おわりに

もう少し触ってみないことには、どのような問題が潜んでいるか、定かではありませんが、まぁ、最低限の開発環境として、スタートラインには立てたと言って良いでしょうか？

ちなみに、Bootstrap Icons [^8] も、すでに使えるようになっているようです。

[^8]: https://icons.getbootstrap.com/

ともかく、これをベースとして、色々と試してみるとしましょう。