Webアプリケーションをdocker化する（Ruby on Rails7.0 × postgres12）

## はじめに
本記事はRuby on Rails7.0とPostgresql12で構築したwebアプリケーションへdockerを導入する手順をまとめたものです。
"dockerを導入する"ことにフォーカスした記事となのでrubyやRails、postgresの知識がなくてもお読みいただけます。

## この記事でわかること
 - Webアプリケーションをdocker化する方法
     - ベースイメージのバージョンの決め方
     - Dockerイメージの軽量化
     - docker-composeを使うメリット

## フォルダ構成
![スクリーンショット 2023-07-29 17.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636274/0b26bc04-3396-6def-d591-bded1c9b7296.png)
今回dockerを導入するにあたって追加したファイルは以下の三つです。上から順に説明していきます。
 - Dockerfile
 - docker-compose.yml
 - config/database.yml
## Dockerfile
Dockerfileの中身はこちらです。
```bash:Dockerfile
FROM ruby:3.2.2
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client \
    yarn
WORKDIR /rails-docker
COPY Gemfile Gemfile.lock /rails-docker/
RUN bundle install
```
まずはDocker InstructionのFROMを使ってベースとなるイメージを設定します。このイメージに対して必要なものを追加していくことで最終的にアプリケーションが動く環境が完成します。
```bash:Dockerfile
FROM ruby:3.2.2
```
今回はRailsで作られたアプリケーションを動かすためrubyのベースイメージとします。イメージの情報は[Docker Hub](https://hub.docker.com/_/ruby)から確認できます。必要なバージョンはプロジェクト内のGemfileを参照します。実際にGemfileを見ると以下のようになっています。

```bash:Gemfile
ruby "3.2.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.6"
```
Railsのバージョン7.0.6を使用していて、それに必要なRubyのバージョンは3.2.2ということが分かります。よってruby:3.2.2をベースのイメージとして設定しています。この辺りは使用する言語とフレームワークの依存関係によって変わってきます。
### ruby;latestじゃダメなの？
ちなみに```ruby:latest```とすることで最新バージョンのrubyイメージを指定することができます。個人利用でとりあえず動かしたい場合などはこの方法でも問題はないかと思います。
しかし、Docker hub上のイメージは日夜更新されていくため、意図しないバージョン変更が起こり、不具合の原因となり得ます、その可能性を回避するため、明示的に3.2.2を指定しています。
latestを指定する場合はその目的とリスクを考慮した上で使用するのが良いでしょう。

```bash:Dockerfile
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client \
    yarn
```
続いてapt-get updateとinstallについてです。
まずは```apt-get update```でパッケージリストを更新し、その後必要なパッケージを```apt-get install```でインストールしています。**-y**をオプションを指定して確認プロンプトを自動でスキップするようにしています。
### Dockerイメージを軽量化する
Dockerイメージが大きくなりすぎるといくつかデメリットがあります。
- ローカルのストレージを圧迫する
- buildに時間がかかる

ポイントは**Dockerイメージレイヤーの数を減らすこと**です。
Dockerイメージレイヤーの数が増えることは、イメージサイズの増加につながります。
ではどんな時にイメージレイヤーが作られるか確認していきましょう。Docker Instructionにはいくつか種類がありますが、**イメージレイヤーが作られるのは、 RUN、COPY、ADDを実行した時です**。つまり、この三つのコマンドを使う回数を減らすことで、無駄なイメージレイヤーを作らずにdockerイメージを構築できます。どうすれば減らせるのか、次の2点を確認しましょう。
- **コマンドを```&&```で繋げる**
- **```\```で改行を入れる**
```bash:Dockerfile
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client \
    yarn
```
最初に紹介したコードは```&&```を使ってapt-get updateとapu-get installをまとめています。さらに、```\```で改行を入れることでinstallするパッケージも一挙に書くことができています。
```bash
RUN apt-get update
RUN apt-get install -y
RUN build-essential -y
RUN libpq-dev -y
RUN nodejs -y
RUN postgresql-client -y
RUN yarn
```
```&&```や```\```を使わずに書いた例がこちらです。書いてある内容は同じですが、こちらの方が冗長なのが一目でわかると思います。最初の例ではRUNコマンドが1回で済んでいたのに対し、後者では7回も使っています。
> イメージレイヤーが作られるのは、 RUN、COPY、ADDを実行した時
>
余分なイメージレイヤーが6つもできていることになります。これは効率的とは言えませんし、```&&```や```\```でまとめて書いた方がコードもスッキリ書けるのがお分かりいただけたと思います。

Dockerfileを記述する際はイメージを極力軽量化する点を意識すると、自分だけじゃなく周りもハッピーですね。



## docker-compose.yml
```bash:docker-compose.yml
version: '3'

volumes:
  db-data:

services:
  web:
    build: .
    command: >
      bash -c "rm -f tmp/pids/server.pid && rails db:create && rails db:migrate && rails s -b 0.0.0.0"
    ports:
      - '3000:3000'
    volumes:
      - '.:/rails-docker'
    environment:
      - 'DATABASE_PASSWORD=postgres'
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db
  db:
    image: postgres:12
    volumes:
      - 'db-data:/var/lib/postgresql/data'
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'

```
Dockerfileの設定が完了したので、続いてdocker-compose.ymlです。

docker-composeにはDockerfileを元にできたイメージを元にコンテナをどう立ち上げるかを記載します。```docker run```を実行するとき、必要に応じて```-v```などのオプションをつけますよね。それらを書いていくイメージです。

docker-composeを使うことで以下のようなメリットがあります。
- 実行コマンドが簡潔に
- 複数のコンテナを同時に立ち上げできる

### 実行コマンドが簡潔に
docker-composeを使うことでコンテナ立ち上げ時のコマンドを簡潔にすることができます。今回の場合、```docker-compose up```のみでコンテナを立ち上げることができます。

逆に使わないとどうなるか。webコンテナを立ち上げる場合を例にすると、以下のように非常に長いコマンドが必要となります。
```docker run -it -v<ローカルのパス>:/rails-docker -p 3000:3000 <image> bash```
これを毎回打つのは骨が折れるのでdocker-composeを使って楽しちゃいましょう。

### 複数のコンテナを同時に立ち上げできる
コンテナを複数同時に立ち上げたい場面はよくあると思います。例えばフロントエンド、バックエンド、データベースなど分けることがあります。この場合、一度それぞれの情報をdocker-compose.ymlに記載してしまえば、以降は```docker-compose up```で一挙に立ち上げることができます。

### docker-compose.ymlの中身
```bash:docker-compose.yml
version: '3'

volumes:
  db-data:

services:
  web:
    build: .
    command: >
      bash -c "rm -f tmp/pids/server.pid && rails db:create && rails db:migrate && rails s -b 0.0.0.0"
    ports:
      - '3000:3000'
    volumes:
      - '.:/rails-docker'
    environment:
      - 'DATABASE_PASSWORD=postgres'
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db
  db:
    image: postgres:12
    volumes:
      - 'db-data:/var/lib/postgresql/data'
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'

```
```bash
version: '3'
```
docker-composeのバージョンは3を指定。
servicesの中にwebコンテナとdbコンテナについて記述しています。

```bash
volumes:
  db-data:
```
コンテナを終了or削除してもデータを保持できるようデータベースの中身をホストへ同期します。

まずはwebコンテナについてです。webコンテナは名前の通りwebサーバーとなるコンテナで、ここでrailsを実行しています。
``` bash:webコンテナ
web:
    build: .
    command: >
      bash -c "rm -f tmp/pids/server.pid && rails db:create && rails db:migrate && rails s -b 0.0.0.0"
    ports:
      - '3000:3000'
    volumes:
      - '.:/rails-docker'
    environment:
      - 'DATABASE_PASSWORD=postgres'
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db
```
```bash
build: .
```
現在のディレクトリにあるDockerfileを元にビルドします。
```
bash -c "rm -f tmp/pids/server.pid && rails db:create && rails db:migrate && rails s -b 0.0.0.0"
```
```command```ではコンテナ起動時に実行するコマンドを指定できます。以下のコマンドが順番に実行されます。

1. ```bash -c```
以降の文字列をシェルで実行する

2. ```rm -f tmp/pids/server.pid```
前回のサーバーのプロセスID（PID）ファイルを強制的に削除します。これは、アプリケーションサーバーが以前のセッションからクリーンアップされなかった場合の問題を防ぐためのものです。server.pidが存在すると、WEBサーバが起動中（プロセスがある）と判断され、下記のエラーが発生します。
```A server is already running. Check /tmp/pids/server.pid.　Exiting```

3. ```rails db:create```
Railsのデータベースを作成します。

4. ```rails db:migrate```
データベースのマイグレーションを実行します。

5. ```rails s -b 0.0.0.0```
Railsのサーバーを起動します。

```bash
ports: 
    - '3000：3000'
```
ポート番号を指定します。これによりホストPCのlacalhost:3000でアプリにアクセスできます。

```bash
volumes:
      - '.:/rails-docker'
```
ホストPCとコンテナ内のファイルを同期します。ホストPCのカレントディレクトリとコンテナ内の/rails-docker配下を同期しています。

```bash
environment:
      - 'DATABASE_PASSWORD=postgres'
```
コンテナ内で扱う環境変数を設定します。データベースのパスワードとして'postgres'を設定しています。
※今回はサンプルとして分かりやすいパスワードで設定しています。

```bash
    tty: true
    stdin_open: true
```
コンテナ内で対話的な操作を可能にするオプションです。```docker run -it```と同義です。

```bash
    depends_on:
      - db
```
コンテナ間の依存関係を設定しています。dbコンテナが起動した後webコンテナを起動します
"webコンテナはdbコンテナが必要なのでdbコンテナから立ち上げてね”という意味になります。
```bash
    links:
      - db
```
webコンテナとdbサービスが通信できるようにリンクします。

続いてdbコンテナです。webコンテナと共通したコマンドは割愛します。
```bash:dbコンテナ
  db:
    image: postgres:12
    volumes:
      - 'db-data:/var/lib/postgresql/data'
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'
```
データベースはpostgresを使用します。バージョンは12です。
```bash
    volumes:
      - 'db-data:/var/lib/postgresql/data'
```
前述のdb-dataボリュームをPostgresのデータディレクトリにマウントします。
>コンテナを終了or削除してもデータを保持できるようデータベースの中身をホストへ同期します。
>
## config/database.yml
こちらにはアプリケーションがデータベースに接続する方法を設定するために使用されます。
Rails特有のファイルですので、読み飛ばしていただいて大丈夫です。

開発、テスト、本番環境のデータベースをそれぞれどう設定するかを記述します。今回は開発環境を設定しています。変更箇所を説明します。
```bash config/database.yml
.
.
.
# 開発環境
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  user: postgres
  port: 5432
  password: <%= ENV.fetch("DATABASE_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 } %>
.
.
.
```
```adapter```にはRailsアプリケーションが使用するデータベースの種類を設定します。今回はpostgresを使用するので'postgresql'としています。
ちなみにmysqlの場合は'mysql12'、sqliteの場合は'sqlite3'のようにデータベースの種類によって値が決まっています。

```port```は'5432'とします。これはPostgreSQLデータベースのデフォルトのTCPポート番号です。postgresを使用する場合'5432'を使用するのが一般的なようです。

```password```はdocker-compose.ymlのdbコンテナ部分で指定した```'POSTGRES_PASSWORD=postgres'```とリンクしています。

## アプリケーションの立ち上げ
ここまで完了すればあとは```docker-compose up```でコンテナを起動しlacalhost:3000でアプリケーションにアクセスできるようになります。

以上がWebアプリケーション（Ruby on Rails7.0 × postgres12）をdocker化する手順となります。


## 最後に
ここまでお読みいただきありがとうました。

初めての技術記事投稿となり分かりづらい箇所もあったと思いますが、どなたかのヒントになれば幸いです。

またネタができたら投稿いたします。


##　参考記事
[米国AI開発者がゼロから教えるDocker講座](https://www.udemy.com/course/aidocker/)
[A server is already running. Check /myapp/tmp/pids/server.pid. の対処法](https://qiita.com/Bjp8kHYYPFq8MrI/items/d23eb57e11c9bf448cc5)