Rails 7系、esbuild、TailwindCSS、Postgresqlを用い、Docker Composeで環境構築する

## はじめに


この記事は[RUNTEQ Advent Calendar 2023](https://qiita.com/advent-calendar/2022/runteq) 8日目に参加しています。

初めまして。

プログラミングスクールRUNTEQで10ヶ月学習し、現在個人開発でアプリを作成中の土井と申します。

今回のテーマは「初めて学んだ技術」ということで、個人開発の際にDockerを用い開発環境を作成したので、環境の紹介と各種ファイルの説明をしたいと思います。

今後この環境でDockerを立ち上げようとしている方へ少しでも参考になるようにわかりやすく書ければ、と思っています。

また、この環境通りに行いエラーやうまくいかなかった点、間違っている点などございましたらコメントにてご指摘いただけると幸いです。

使用環境

- MacbookAir (M2)
- Ruby 3.2.2
- Rails 7.0.8
- esbuild
- tailwindCSS
- postgresql

## そもそもDockerとは？

Dockerは、アプリケーションを開発・配置・実行するためのオープンプラットフォームです。コンテナ仮想化を使います。

説明を受けても、個人的にはピンときませんでした。

そのアプリケーションに必要なもの

具体的には、アプリケーションに必要なもの（例: Railsやyarnなど）を一つのコンテナと呼ばれる仮想マシンにまとめ、どんな環境でも動かせるようにするものです。これにより、複数人で開発する際にはどの開発者の環境でもDockerを使用して管理することができ、簡単に共有できるというメリットがあります。

個人開発でも、環境の分離や共有の容易さなどのメリットがあります。

例えば、異なる環境を使用する場合でも、Dockerを使うことで同じように環境構築が行え、ローカル環境を汚すことなく開発ができます。

### イメージとは？

コンテナを作るための設計図です。

コンテナを起動するために必要な設定ファイルをまとめたものであり、他の人と共有することでどんなマシンでも同じ環境を作ることができます。

イメージはコンテナの元となり、イメージからコンテナが起動します。

イメージを用意する方法は、他の人が作成したイメージを取得する（例: Docker Hubなど）か、自分でイメージを作成することがあります。

今回はDockerfileと呼ばれるテキストファイルにイメージ作成に関わる設定を記述し、そのファイルからイメージを作成します。

### コンテナとは？

コンテナはイメージをもとに作成されたアプリケーションの仮想環境のことです。

これにより、アプリケーションが依存するライブラリやツールなどが一つのまとまった状態で提供され、開発や実行が容易になります。

### Docker composeとは？

Docker Composeは、複数のコンテナを定義し実行する Docker アプリケーションのためのツールです。

YAMLファイルを使い、アプリケーションのサービスを設定し、1つのコマンドを実行するだけで、設定内容に基づいた全てのサービスを生成・起動します。

これにより、複数のコンテナで構成されるアプリケーション（例: webサーバーとdbサーバー）のDockerイメージのビルドや各コンテナの起動・停止を簡単に行えるようになります。

## 実装

ここからは実際にDockerでRails 7系の環境を作成します。

まず、プロジェクトのディレクトリを作成し、作業ディレクトリに移動します。

```jsx
mkdir test_app
cd test_app
```

次に、各種設定ファイルを作成します。

```jsx
touch {docker-compose.yml,Dockerfile,Gemfile,Gemfile.lock,entrypoint.sh}
```

### Dockerfile

```docker
# Rubyのバージョンを指定した公式イメージをベースに使用
FROM ruby:3.2.2

# 必要なパッケージのインストール
RUN apt-get update -qq \
 && apt-get install -y nodejs postgresql-client npm vim \
 && rm -rf /var/lib/apt/lists/* \
 && npm install --global yarn

# コンテナの作業ディレクトリを指定
RUN mkdir /myapp
WORKDIR /myapp

# ホストのGemfileとGemfile.lockをコンテナにコピー
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock

# bundle installを実行
RUN bundle install

# カレントディレクトリのファイルをコンテナにコピー
ADD . /myapp

# コンテナ起動時に実行されるスクリプトをコピーして実行可能にする
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh

# コンテナが外部に公開するポート番号を指定
EXPOSE 3000

# コンテナ起動時に実行されるメインプロセスを指定
CMD ["rails", "server", "-b", "0.0.0.0"]
```

上記で説明した通り、イメージを作るための設定を書いています。

### docker-compose.yml

```
version: "3"
services:
  db:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_volume:/var/lib/postgresql/data
    restart: always
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bin/dev"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    restart: always
    tty: true
    stdin_open: true
    depends_on:
      - db
volumes:
  postgres_volume:
```

- `services`
以下に作成したサービス（コンテナ）の名前を書いていきます。
今回railsを動かすウェブサーバーと、DBを動かすDBサーバーがほしいので、
「web」と「db」としていますが、このコンテナの名前は任意ですので、違う名前でも大丈夫です。
ただ、注意点としては基本的に複数の人が使うためわかりやすい命名を心がけることです。
基本的には「web」や「db」にするといいでしょう。

- `image`
既存のpostgresイメージを使用しています。
こうすることで、postgresのDockerイメージよりビルドします。

- `build`
imageではなく、用意したDockerfileよりビルドする場合、こちらにdocker-compose.ymlファイルがおいてあるディレクトリからDockerfileがおいてあるディレクトリへの相対パスを記述します。


- 各サービス内にある方の`volumes`
データをコンテナ外に保存するための設定項目で、コンテナの中で変更されたデータがホストマシンにも共有されます。
例えば、コンテナの中でRails g controller 〜　をした場合、コンテナの中にデータが増えるのと同時にホストマシンのディレクトリにもデータが増えます。
ここを設定しないとコンテナを削除してしまった時にコンテナ内で行ったデータも消えてしまいます。

- サービスの外、最後にある方の`volumes`
こちらはDocker composeにボリュームを作成させています。
ここで定義したボリュームはサービス内のvolumesで使用することができます。
こちらはDockerコンテナが停止しても独立して保存・管理ができるので、コンテナが削除されてもデータが保持されます。
https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32
ボリュームに関して詳しくはこちらがわかりやすいので参考にどうぞ。

- `environment`
コンテナ内で使用される環境変数を設定しています。
Dockerコンテナとホストマシンの環境は完全に分離しているため、コンテナ内で環境変数を設定してあげる必要があります。
今回は双方にpostgresを使う際のパスを記載しています。
```jsx
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
```

- `ports`
ここではホストマシンとコンテナの間でネットワークポートのマッピングを指定しています。
書き方は、ホストマシンのポート番号:コンテナのポート番号　となります。

- `depends_on`
サービス間の依存関係を定義します。
今回だとwebコンテナははdbコンテナに依存関係があるため、dbコンテナが先に開始されることをDocker composeが確認するために使用されます。

- `command`
サービスコンテナが起動したときに実行したいコマンドを指定します。
今回ウェブサーバーが起動したときにbin/devを起動したいのでこれと、
rails serverを立ち上げた時に、server.pidというものが作られるのですが、何らかの問題が発生した際に、終了してもserver.pidが残ってしまうことがあり、これを削除しておかないとrails serverが立ち上がらないので、bin/devする前に消してあげるコマンドを指定しています。

- `restart: always`
Dockerを終了したり、再起動しても自動的にコンテナを起動するよう設定しています。
手動でコンテナを止めるまで続きます。
- tty, stdin_open
ttyはキーボード入力をコンテナに結びつけます。
**`stdin_open`** は、標準入出力とエラー出力をコンテナに結びつけます。
とみて私はピンときませんでしたが、Railsでデバッグする際に、コンテナの中でbindingなどを使いたい時のための設定のようです。
### entrypoint.sh

```bash
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```

**`set -e`**: この行は、シェルスクリプトの各コマンドがエラーで終了した場合に、スクリプト全体が停止するようにします。**`-e`**は"exit on error"の略です。

rm~は先ほどと同じなので割愛

**`exec "$@"`**: この行は、**`"$@"`**で指定されたコマンドを実行します。DockerイメージのCMDディレクティブで指定されたものがここに渡され、通常はRailsサーバーを起動するコマンドが設定されます。

このスクリプトの主な役割は、サーバーのプロセスIDを保守するために**`server.pid`**を削除し、それから指定されたコマンドを実行してコンテナを起動することです。

### Gemfile

```ruby
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.8"
```

コンテナに入った後にRails newするのでrailsのversionだけ指定しておきます。

### Gemfile.lock

空のままでOK、rails new時に必要なので用意だけしておく。

ここまで設定できたら、以下のコマンドを実行してコンテナをビルドし、Railsプロジェクトを作成します。

```jsx
docker compose run web rails new . --force --database=postgresql --javascript=esbuild --css=tailwind
```

追記：失敗する場合、一度```docker compose build```を行なってから再度```docker compose up```を行なってください。


プロジェクトの作成が完了したら、データベースを作成します。

```jsx
docker compose run web rails db:create
```

追記：
db:createを行う前に```databese.yml```に下記設定を追加してください
```
default: &default
  adapter: postgresql
  encoding: unicode
  # ここから
  host: db
  username: postgres
  password: password
　　　　# ここまで
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  ```

最後に、以下のコマンドでコンテナを起動します。

```jsx
docker compose up
```

これでlocalhost:3000にアクセスすると、Railsアプリケーションが確認できるはずです。

ただし、通常の状態ではlocalhost:3000に直接接続できないかもしれません。

もし接続できない場合は、次のようなエラーが表示される可能性があります。
<br>
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070807/70027781-7276-9654-ca6b-c184ff010d23.png)


このエラーは、現在Dockerコンテナ内でサーバーを立ち上げているため、ローカル環境とは接続されていないことを示しています。

これを解決するためには、コンテナ内のサーバーにアクセスできるように設定する必要があります。

### Procfile.dev

```jsx
web: env RUBY_DEBUG_OPEN=true bin/rails server -b '0.0.0.0' -p 3000
js: yarn build --watch
css: yarn build:css --watch
```

`0.0.0.0`を指定することで、Railsサーバーはすべてのネットワークインターフェースからのリクエストを受け付けるようになる。

これは特にDockerを使っている場合に重要で、Dockerコンテナ内のサーバーをコンテナ外からアクセス可能にするために必要になります。

これで再度 **`docker compose up`** を実行すれば、localhost:3000に正常に起動したら成功です。

最後に

今回Dockerの説明や使い方を説明しつつ環境をご紹介しました。

個人的に、最後のProcfileの設定がわからずに苦労しました。

また、記事を書くにあたり調べながらの記載でしたので、至らない点や間違っている点、よりよい方法などあると思いますので、修正点などありましたらコメントにてお願いいたします。

ここまでみていただきありがとうございました。

下記にGithubのURLも載せておきますので、よければ参考にしてみてください。

https://github.com/anna-cat51/test_app

## 参考

https://docs.docker.jp/index.html

https://qiita.com/gon0821/items/77369def082745d19c38

https://zenn.dev/wakkunn/articles/33c84147608078#discuss

https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32