DockerでRailsの開発環境を構築

## はじめに

Dockerに関する学習のアウトプットとして、RailsプロジェクトをDocker化しましたため、その方法を記事として残しておきます。また、詳細は後述しますが、初めてマルチステージビルドでインストールにも挑戦してみましたため、今後マルチステージビルドで環境構築される方の参考になればと思います。


## 前提条件

開発環境は以下の通りです。
- Mac(Intel)
- Ruby 3.0.2
- postgres 12.0
- node 14.8.0
- yarn 1.22.4

## 作成したファイル

Dockerで開発環境を構築するために作成した`Dockerfile`と`docker-compose.yml`について説明していきます。ちなみにローカル環境のディレクトリ構成は以下の通りとなっております。

```
/rails-docker
    ├── Dockerfile
    ├── Gemfile
    ├── Gemfile.lock
    ├── README.md
    ├── Rakefile
    ├── app
    ├── babel.config.js
    ├── bin
    ├── config
    |     └- database.yml
    ├── config.ru
    ├── db
    ├── docker-compose.yml
    ├── lib
    ├── log
    ├── node_modules
    ├── package.json
    ├── postcss.config.js
    ├── public
    ├── storage
    ├── test
    ├── tmp
    ├── vendor
    └── yarn.lock
```

<br>

まずはDockerfileについてです。

```dockerfile:Dockerfile
FROM node:14.8.0-stretch as node

FROM ruby:3.0.2

ENV YARN_VERSION 1.22.4
COPY --from=node /opt/yarn-v$YARN_VERSION /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
  && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx

RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    postgresql-client

WORKDIR /rails-docker
COPY Gemfile Gemfile.lock /rails-docker/
RUN bundle install

```

### マルチステージビルドについて

最初に説明しましたが、nodejs、yarnは`apt-get`を使用せずにマルチステージビルドでインストールしております。Dockerファイルに`FROM`を２つ書くことによって実現しております。

そもそもマルチステージビルドとは？？という方のために、まずそのことについて記載します。

マルチステージビルドでインストールすることの最大のメリットは **イメージが軽量になることです。**

イメージが軽量になることによって、**ビルド時間の短縮やホストへのディスク容量の逼迫を防ぐことなど** にも繋がります。

なぜイメージの軽量化が実現しているのかと言いますと、nodejsやyarnを`apt-get`でインストールする方法ですと、Railsプロジェクトを実行するのに不要なファイルも含まれてしまいます。そのため、`FROM`を2つ書くことで、nodejsのイメージ(node:14.8.0-stretch)の中からRailsプログラムを実行するのに必要なプログラムだけを抽出して、rubyのイメージ(ruby:3.0.2)にコピーすることによって、イメージの軽量化を実現しております。


例えば、Go言語のようにビルドした結果のバイナリファイルだけあればプログラムとして、実行できます。コンパイル前のファイルはプログラムを実行するのには不要なため、コンパイル前のプログラムはイメージに含めずに作成することによって、イメージの軽量化を実現しています。

以下の記事を参考にしました。
- [Multi-stage build でNode.jsのインストールをちょっぴり効率化する](https://tech.actindi.net/2019/06/20/084247)
- [Dockerfileのベストプラクティス](https://y-ohgi.com/introduction-docker/3_production/dockerfile/)
- [マルチステージビルドの利用](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)

### Dockerfileのコード説明

以下コードはマルチステージビルドの説明で概要はお伝えしておりますので、細かいコードについて説明を記載します。

```dockerfile:Dockerfile
FROM node:14.8.0-stretch as node

FROM ruby:3.0.2

ENV YARN_VERSION 1.22.4
COPY --from=node /opt/yarn-v$YARN_VERSION /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
  && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx

```

- `FROM node:14.8.0-stretch as node`で`as 〜`で`node`というエイリアス(別名)を定義しております。

- `ENV YARN_VERSION 1.22.4`で`YARN_VERSION`という環境変数にyarnのバージョン1.22.4を設定しております。

- `COPY --from=node`で`FROM node:14.8.0-stretch as node`で指定したエイリアスnodeのイメージから`FROM ruby:3.0.2`で指定したRubyのイメージにnodeとyarnをコピーしております。

- `ln -s　リンク元　リンク先`はLinuxコマンドでシンボリックリンクを作成しております。これによって、Rubyイメージでyarnやnodeなどを使用できるようになります。



```dockerfile:Dockerfile
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    postgresql-client
```

- `apt-get update`でパッケージリストを最新の状態に更新します。

- `apt-get install -y`で指定したパッケージをインストールします。`-y`オプションでパッケージのインストール中にYes、Noが聞かれた際にYesで回答するように指定しております。

- `build-essential`は開発に必須のビルドツールを提供しているパッケージ。gcc(GNU C Compiler)、g++(GNU C++ Compiler)、 makeなどが入っております。（RubyはC言語で作られた言語のため、インストールが必要と思われます。）

- `libpq-dev`はRuby(C言語で作られたプログラム)からPostgreSQLに通信するために必要なライブラリ

- `postgresql-client`はpsqlを使用して、PostgreSQLデータベースに接続して対話的にクエリを実行できます。SQLのコマンドを直接入力できます。

- `&&`はLinuxコマンドを続けて実行することができます。

- `\`は改行で、見た目を整えることができます。

以下の記事を参考にしました。
- [apt-get - パッケージの操作・管理 - Linuxコマンド](https://webkaru.net/linux/apt-get-command/)
- [Ubuntuのaptを調べたのでまとめておく](https://zenn.dev/ikkik/articles/ffa45ed1fe5a1a)
- [build-essentialとは](https://qiita.com/h_tyokinuhata/items/431a56fb054145468c16)
- [パッケージ: libpq-dev](https://packages.debian.org/ja/sid/libpq-dev)
- [psqlとは？PostgreSQLへの接続と利用方法を解説！](https://and-engineer.com/articles/YT75bhAAACMApKt9#heading3-3)


### docker-compose.ymlのコード説明

docker-composeを使用することで、長くなってしまうDockerコマンドを短いコマンドで実行できるようにしております。こちらもコードの細かい説明を記載しております。

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

volumes:
  db-data:

services:
  web:
    build: .
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/rails-docker
    environment:
      - 'DATABAS_PASSWORD=postgres'
    ports:
      - "3000:3000"
    depends_on:
      - db
    links:
      - db
    tty: true
    stdin_open: true

  db:
    image: postgres:12.0
    volumes:
      - 'db-data:/var/lib/postgresql/data'
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'
```

<br>

Dockerfileをビルドします。`.`と指定しているため、ビルドするためには`docker-compose.yml`と`Dockerfile`が同じ階層にある必要があります。
```
build: .
```

<br>

`bundle exec rails s -p 3000 -b '0.0.0.0'`でrailsサーバを起動しております。また、`/bin/sh -c "rm -f tmp/pids/server.pid"`と記載しているのはrailsサーバを立ち上げる際に前回立ち上げた際に作成された`tmp/pids/server.pid`が残っていることによって、railsサーバがうまく立ち上がらない事象が発生しましたため、毎回削除するようにしております。
```
command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
```

<br>


ホストのdocker-composeコマンドを実行するディレクトリとコンテナのディレクトリ/rails-dockerをマウントしております。マウントすることによって、コンテナからホストのRailsプログラムを参照できるため、コンテナ上にRailsプログラムを持たなくてよくなり、コンテナの軽量化にも繋がります。Dockerコマンドの`-v ホスト:コンテナ`と同一です。
```
volumes:
  - .:/rails-docker
```

<br>

以下は`config/database.yml`で定義している`password: <%= ENV.fetch("DATABAS_PASSWORD") %>`で使用するために記述しております。Railsからposgresに接続するためのパスワードを設定するために記述しております。
```
environment:
  - 'DATABAS_PASSWORD=postgres'
```

以下でポートの開放を行います。
左にホストのポートを、右にコンテナのポートを指定します。
Dockerコマンドの場合、 `-p 3000:3000` オプションと同一です。
```
ports:
  - "3000:3000"
```
<br>

サービス間の依存関係を示します。webの前にdbを開始して、その後にwebが開始するようにしております。
```
depends_on:
  - db
```

<br>

~~コンテナを他のサービスとリンクlinkします。サービスwebからdbを参照できるようになります。~~
以下は記述しなくてもwebからdbを参照できます。networkで名前解決してくれるためです。

```
links:
  - db
```

<br>


コンテナに接続(実行中のコンテナに`docker-compose exec web bash`コマンドを叩いて接続)した際に`stdin_open: true`で標準入力を可能にしております。また、`tty: true`で出力結果をきれいに表示できるようにしております。Dockerコマンドの`-it`と同一です。
```
tty: true
stdin_open: true
```

<br>


以下はDBデータをコンテナ上に保存するのではなく、ホスト側に保存するようにマウントしております。コンテナ上に保存してしまうとコンテナが消えてしまうと、DBデータが消えてしまうためです。コンテナを止めてもデータが残るようにホスト側に保存するようにしております。
```
volumes:
  - 'db-data:/var/lib/postgresql/data'
```

上記で指定した`db-data`は以下でDocker volumeを作成しております。

```
volumes:
  db-data:
```


<br>

以下の記事を参考にしました。
- [Compose ファイル version 3 リファレンス](https://docs.docker.jp/compose/compose-file/compose-file-v3.html#links)
- [A server is already running. Check /tmp/pids/server.pidエラーの対応](https://blog.cloud-acct.com/posts/u-rails-serverpid-error/) 
- [いい加減docker-composeでlinksを使うのをやめてnetworkでコンテナ間名前解決をする](https://qiita.com/dyoshikawa/items/05d627b962da35f7d5b6)
- [docker-compose-で、links-使う必要なかったんや](https://it-afi.com/docker/docker-compose-%E3%81%A7%E3%80%81links-%E4%BD%BF%E3%81%86%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%93%E3%82%84/)



## webアプリケーションの起動・停止方法

Dockerとdocker-composeで作成したアプリケーションを起動・停止するためのコマンドを記載します。

1. アプリケーションを起動
```
docker-compose up
```
<br>
2. データベースを作成。docker-compose upを実行したターミナルとは別のターミナルを新たに開き、以下コマンドを実行

```
docker-compose run web rake db:create
```

<br>
3. データベースにテーブルを作成

```
docker-compose run web rake db:migrate
```

<br>
4. ブラウザを開き、以下URLを入力

```
http://localhost:3000/
```

<br>

以下画面が表示される。

<img width="833" alt="image" src="https://github.com/YokoyamaGen/rails-docker/assets/66200883/72940065-a100-4db4-a30d-6aeabea7d83f">

<br>
5. アプリケーションを停止。`docker-compose up`を実行したターミナルで以下を実行

```
Ctrl-C
```

<br>
6. アプリケーションを再起動

```
docker-compose up
```