既存のRailsプロジェクトをDocker composeしてDBと接続する


## はじめに

対象者：
- dockerは使ったことあるけど自分で構築した経験がない
- [クィックスタート: Compose と Rails](https://docs.docker.jp/compose/rails.html)は出来たから
既存のRailsプロジェクトをDocker化したい


ゴール：
- 既存のRailsプロジェクトを`docker compose up -d`だけで
Rails + DBが立ち上がる環境の構築

までを解説したいと思います。

## 前提
- Docker, Docker Composeのインストールが済んでいること
- 既存のRailsプロジェクトがあること
- [クィックスタート: Compose と Rails](https://docs.docker.jp/compose/rails.html)を参考に既存のアプリをDocker化していきます。

## 手順

1. dockerfileの作成
2. compose.ymlの作成（docker-compose.ymlは非推奨となっています）
3. database.ymlの修正

の順番で進めていきます。

### 1. dockerfileの作成


まず、Rubyを使えるコンテナを作成していきます。
既存のRubyプロジェクトのルートにDockerfileを作成していきます。

[クィックスタート: Compose と Rails](https://docs.docker.jp/compose/rails.html)のDockercfileを借りてきます。

```
FROM ruby:2.3.3
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
ADD . /myapp
```

クィックスタートのDockerfileはそのまま使えないので少し修正していきます。

- バージョンを既存プロジェクトと一致するように
- RUN mkdir /myappを削除
- 既存プロジェクトのgemfileを使うように
- ローカルのプロジェクト自体をコンテナにコピー


以下が、**修正後**のDockerfileです。

```
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
WORKDIR /myapp
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . ./
```
修正内容を見ていきます。

- `FROM ruby:3.2.2`
バージョンの修正
こちらはご自身のRailsプロジェクトのバージョンと同じにしてください。
- `WORKDIR /myapp`
コンテナで作業するディレクトリを指定します。
この時、指定の/myappがない場合はディレクトリを作成してくれるので
`RUN mkdir /myapp`を削除しています。
- `COPY Gemfile Gemfile.lock ./`
既存プロジェクトのgemfileを使うように
ローカルのPCにある`Gemfile Gemfile.lock`をコンテナの`./`のディレクトリにコピーします。
※ `./`は省略すると現在の作業ディレクトリにCOPYされます。今回は`WORKDIR /myapp`を指定しているので省略しても`/myapp`にコピーされます。


ローカルのPCにあるファイルをコンテナにコピーします。
`.`とはdockerfileがあるディレクトリのことです。
`./`とはコンテナのディレクトリのことです。今回の場合は`WORKDIR /myapp`で指定したディレクトリですね。

ここで疑問に思う方もいるかもしれません。
> `COPY Gemfile Gemfile.lock ./`と`COPY . ./`分ける必要ないじゃん！

そうです、一緒にしてもいいです。

ただ、こればDockerのキャッシュを効率的に使うために分けています。

結論から言うと
> **`Gemfile` `Gemfile.lock`に変更がない場合は`RUN bundle install`はスキップされます。**


前回の`COPY Gemfile Gemfile.lock ./`でキャッシュされた結果が再利用され、その後の`RUN bundle install`もキャッシュを利用します。

つまり、`COPY . ./`だけにしてしまうと、プロジェクトのファイルに変更があるたびに`RUN bundle install`が再実行される可能性が高くなります。

## 2. compose.ymlの作成

最終的なコードはこちらです。



```
services:
  web:
    build: .
    command: bash -c "rails db:create db:migrate && rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    volumes:
      - .:/myapp
    depends_on:
      - db
  db:
    image: postgres:12
    environment:
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
```

`services:`
 - `web`
   - `build: .`:ホストのカレントディレクトリにあるDockerfileを使用して、webサービスのイメージをビルドします。
   - `command`: コンテナ起動時に実行するコマンドを指定しています。ここでは、データベースの作成とマイグレーションを行った後、Railsサーバーを起動します。
   - `ports`: ホストのポート3000をコンテナのポート3000にマッピングします。これにより、ホストのポート3000でRailsアプリケーションにアクセスできます。
   - `volumes`: ホストのカレントディレクトリ（dockerfileがあるディレクトリ）をコンテナ内の/myappディレクトリにマウントします。これでホスト側のファイル変更がコンテナ内に反映されます。
   - `depends_on`: webサービスはdbサービスに依存しており、dbサービスが起動してからwebサービスが起動します。
- `db`
  - `image`: postgres:12: PostgreSQLのバージョン12のイメージを使用します。
  - `environment`: 環境変数を設定します。ここでは、PostgreSQLのパスワードを設定します。
  - `ports`: ホストのポート5432をコンテナのポート5432にマッピングします。

`version: '3'`の指定は非推奨になったので削除しています。


## 3. database.ymlの修正

最後にdatabase.ymlを修正していきます。

`config/database.yml`にあると思います。

defaultの箇所を以下のように追加・修正します。
```
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: postgres
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
```

- `default:`
　すべての環境で共通の設定を定義します。
   - `adapter`: 使用するデータベースの種類（ここではpostgresql）。
   - `encoding`: データベースの文字エンコーディング（unicode）。
   - `host`: データベースのホスト名（db、Docker Composeで定義されたサービス名）。
   - `usernameとpassword`: Railsがデータベースに接続するためのユーザー名とパスワード。

以上で設定は終了です。


compose.ymlがあるディレクトリで`docker compose up -d`を実行して
`localhost:3000`にアクセスしてみてください。

既存のRailsプロジェクトが表示されれば完了です。


## データの永続化について

今回のサンプルではPostgreSQLデータベースのデータを永続化していません。
データの永続化にはDockerのボリュームを作成してそれをマウントする必要があります。

Dockerで作成したボリュームを、データを保存したいコンテナの特定のディレクトリにマウントします。
そうすることで、コンテナが停止したり削除された場合でも、データはボリュームに保存され続けます。

今回のサンプルでは紹介しませんが、気になる方は調べてやってみてください。