最小構成のdocker+Railsアプリ(Nginx/PostgreSQL/Rails/docker-compose)

# 概要
docker,Rails,Nginx,PostgreSQLという定番の構成が簡単に出来る記事を目指しました。Rails単独でのアプリ作成経験があることを前提に書いている部分があります。

以下の構成のWebアプリをdocker-composeを使って連携させます。

- ホストOS: CentOS7 (v7.6.1810)
- 仮想化: docker (v19.03.2, build 6a30dfc)
- 仮想化補助: docker-compose (v1.24.1, build 4667896b)
- Webサーバ: nginx (v1.16.1 stable)
- Webアプリ: Ruby On Rails (5.2.3)
- Webアプリ言語: Ruby (v2.6.3)
- DBサーバ: PostgreSQL (v11.5 Debian 11.5-1.pgdg90+1)

# 事前準備

## ディレクトリ・ファイル構成

以下は完成形のディレクトリ・ファイル構成のイメージです。

```
minimal
　┣ nginx
　┃　　┣ Dockerfile
　┃　　┗ nginx.conf
　┣ postgresql
　┃　　┣ Dockerfile
　┃　　┗ after_init.sh
　┣ app
　┃　　┣ Dockerfile
　┃　　┣ startup.sh
　┃　　┣ .dockerignore
　┃　　┗ Rails作成のファイル・フォルダ(Gemfile, app, bin...)
　┣ docker-compose
　┗ docker-comopse.yml
```

## dockerのインストール

[Docker公式インストール手順](https://docs.docker.com/install/linux/docker-ce/centos/) がありますので最新情報はそちらを参照してください。

過去のバージョンのdockerのアンインストール

```
$ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
```

docker動作に必要なパッケージのインストール

```
$ sudo yum install -y yum-utils device-mapper-persistent-data  lvm2
```

yum用リポジトリにdockerパッケージの情報を追加

```
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
```

yumでdockerをインストール

```
$ sudo yum install docker-ce docker-ce-cli containerd.io
```

dockerデーモンの起動と有効化

```
$ sudo systemctl start docker
$ sudo systemctl enable docker
```

## docker-composeのインストール

こちらも [docker-compose公式インストール手順](https://docs.docker.com/compose/install/) があります。最新情報は公式ページのLinuxへのインストールの部分を参照してください。

上記の手順ではdocker-composeは `/usr/local/bin` にインストールしていますが、ここではminimalディレクトリ直下に置きます。docker-composeは単独で動作する実行ファイルなので、同じディレクトリ内で管理した方がバージョン管理がしやすいからです。

docker-composeのダウンロード

```
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /minimal/docker-compose
```

実行権限の付与

```
$ sudo chmod +x /minimal/docker-compose
```

## RubyおよびRailsのインストール

RubyおよびRailsのインストールに関しては他に詳しい記事がいくらでもあるのでこちらでは詳細は割愛します。

通常のRailsと違う点として、コンテナが終了しても残しておきたいデータをボリューム(Data Volume)としてマウントすることを前提とします。Bundlerでのインストールがコンテナ再作成の度に実行されるのはかなり時間がかかるため、gemは共有ディレクトリに配置します。

1. rbenv等で最新のRubyをインストール。
1. `/minimal/app` 配下で `bundle init`。
1. app配下に `share/bundle` ディレクトリを作成。
1. 作成されたGemfileを編集してrailsのgemを有効化。
1. `bundle install --path share/bundle` してrailsのgemをインストール。
1. `bundle exec rails new -d postgresql .` でrailsアプリを初期化。
1. 適宜Gemfile等を編集しながらrailsが動くようにする。

※今回は簡単な動作確認のため、以下のコマンドでscaffoldを作って確認した。

```
$ bundle exec rails g scaffold User name:string
```

# docker-composeの設定

docker-composeの設定ファイル( `docker-compose.yml` )を作成します。各コンテナの構成の紹介の中で内容に触れます。

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

services:
  postgresql:
    build:
      context: ./postgresql
      dockerfile: Dockerfile
    environment:
      - POSTGRES_PASSWORD=password
    expose:
      - "5432"
    restart: always
    network_mode: host
    volumes:
      - postgresql_data:/var/lib/postgresql/data
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    environment:
      - PORT=3000
      - POSTGRES_PASSWORD=password
      - RAILS_MASTER_KEY=0123456789abcdef0123456789abcdef
    expose:
      - "3000"
    restart: always
    network_mode: host
    volumes:
      - app_data:/app/share
    depends_on:
      - postgresql
  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    restart: always
    network_mode: host
    depends_on:
      - app
volumes:
  postgresql_data:
    driver: local
  app_data:
    driver: local
```

# Nginxコンテナの設定

dockerhub上の[「Nginx公式イメージ」](https://hub.docker.com/_/nginx)を使います。

このページの「Complex configuration」セクションを読むと、ユーザ定義の `nginx.conf` をコンテナに取り込む方法が紹介されています。これを参考に設定をコンテナイメージに取り込みます。

## nginx.confの内容

以下の内容は公式イメージそのままの `nginx.conf` にrailsコンテナと連携するための情報を書き加えたものです。

`#include /etc/nginx/conf.d/*.conf;` 以下の部分を変更しました。

```nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;

  sendfile        on;
  #tcp_nopush     on;

  keepalive_timeout  65;

  #gzip  on;

  #include /etc/nginx/conf.d/*.conf;

  upstream web {
    server localhost:3000;
  }
  server {
    listen 80;
    charset UTF-8;
    keepalive_timeout 5;

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_pass http://web;
    }
  }
}
```

変更内容

- include行をコメントアウト
- upstreamセクションでlocalhost:3000を転送先に指定
- serverセクションで80番ポートへの全ての通信をupstreamに転送

**ここでupstreamにlocalhost:3000を指定しているのがポイントです。**

 `docker-compose.yml` で各コンテナが「network_mode: host」で接続されています。また、railsコンテナ設定部分で「expose: "3000"」を指定し、dockerコンテナ内部ネットワーク向けのみに3000番ポートを公開しています。 よってnginxコンテナからrailsコンテナに「localhost＋ポート番号」で名前解決が出来るのです。

## Dockerfileの内容

nginx.confファイルを上書きします。

```Dockerfile
FROM nginx:stable
MAINTAINER Kou Sirataki<srtkkou@outlook.jp>

# 設定ファイルの上書き
COPY nginx.conf /etc/nginx/nginx.conf
```

# PostgreSQLコンテナの設定

dockerhub上の[「PostgreSQL公式イメージ」](https://hub.docker.com/_/postgres)を使います。

このページの「Initialization Scripts」セクションを読むと、 `/docker-entrypoint-initdb.d` 配下の `*.sql, *.sql.gz, *.sh` ファイルはデータディレクトリ(通常は `/var/lib/postgresql/data` )が空の場合には `initdb` によるDB初期化後に実行されるとあります。 `after_init.sh` に初期化後に実行したいスクリプトを書き込みます。

また、DBのデータがコンテナを再起動する度に消えると困るので、データディレクトリ( `/var/lib/postgresql/data` )はボリューム(Data Volume)としてマウントします。

## after_init.shの内容

コンテナ以外のサーバからユーザとパスワードを使ったアクセスを許可するため、データディレクトリ配下の `pg_hba.conf` を編集し、md5で認証可能にします。

```
#!/bin/bash

# pg_hba.confの編集
PG_HBA=/var/lib/postgresql/data/pg_hba.conf
# - 現在の設定のコメントアウト
sed -ri 's/^(local*)/#\1/g' $PG_HBA
sed -ri 's/^(host*)/#\1/g' $PG_HBA
# - パスワードアクセスの許可設定挿入
echo 'local all all peer' >> $PG_HBA
echo 'host all all 127.0.0.1/32 md5' >> $PG_HBA
echo 'host all all 172.0.0.0/8 md5' >> $PG_HBA
echo 'host all all ::1/128 md5' >> $PG_HBA

# 設定のリロード
pg_ctl reload
pg_ctl status
```

## Dockerfileの内容

「Initialization Scripts」セクションに書かれている例を参考に、ロケールを日本語に設定します。また、上の `after_init.sh` スクリプトを適切な場所にコピーします。

```
FROM postgres:11
MAINTAINER Kou Sirataki<srtkkou@outlook.jp>

# 日本語ロケールへの変更
RUN localedef -i ja_JP -c -f UTF-8 \
  -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.UTF-8

# スタートアップスクリプトの登録
COPY after_init.sh /docker-entrypoint-initdb.d/after_init.sh
```

# Railsコンテナの設定

Rails等のgemはGemfileで制御したいので、ruby環境のみ設定されたdockerhub上の[「Ruby公式イメージ」](https://hub.docker.com/_/ruby)を使います。

現在のRailsの既定のwebサーバはPumaであり、環境変数PORTからポート番号を決める設定になっています。よって `docker-compose.yml` 内の「environment」セクションで環境変数として設定しています。

本番環境(RAILS_ENV=production)では暗号化された設定ファイル `credentials.yml.enc` を開くために、環境変数RAILS_MASTER_KEYで復号化鍵を設定する必要があります。これも `docker-compose.yml` 内の「environment」セクションで環境変数として設定しています。上の `docker-compose.yml` ファイルでは仮の内容を設定していますが、実際は `master.key` の内容を記入してください。

## database.ymlの設定(抜粋)

PostgreSQLを使う設定について、参考までに `database.yml` の一部を抜粋してお見せします。 `docker-compose.yml` のPOSTGRES_PASSWORD環境変数からパスワードを取得することを想定しています。

Nginxの項で説明した通り `docker-compose.yml` で各コンテナが「network_mode: host」で接続されており、postgresqlコンテナ設定部分で「expose: "5432"」を指定しているため、railsコンテナからpostgresqlコンテナへは「localhost:5432」で名前解決できます。

```config/database.yml
※抜粋
production:
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch('RAILS_MAX_THREADS'){ 5 } %>
  username: postgres
  password: <%= ENV.fetch('POSTGRES_PASSWORD'){ '' } %>
  host: localhost
  port: 5432
  database: app_production
```

## startup.shの設定

`docker run` 時に実行されるスクリプトを作成します。

```startup.sh
#!/bin/bash

# ボリューム/app/share配下の初期化
mkdir -p /app/share/{bundle,log,pids,sockets}
chown -R app:app /app/share
# Gemの更新
gosu app bundle install --path share/bundle --without test development
gosu app bundle clean
# DB初期化
gosu app bundle exec rails db:create
gosu app bundle exec rails db:migrate
# JS/CSSファイルのコンパイル
gosu app bundle exec rails assets:precompile
# アプリケーションの開始
gosu app bundle exec rails s
```

## Dockerfileの設定

```Dockerfile
FROM ruby:2.6.3
MAINTAINER Kou Sirataki<srtkkou@outlook.jp>

# 日本語ロケールへの変更
RUN apt-get update && \
apt-get install -y \
  locales && \
echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen && \
locale-gen && \
update-locale LANG=ja_JP.UTF-8

# 環境変数の設定
# - LANG: ロケール
# - RAILS_ENV: Rails実行環境
# - RAILS_SERVE_STATIC_FILES: 静的ファイルの公開設定
ENV LANG="ja_JP.UTF-8"\
  RAILS_ENV="production"\
  RAILS_SERVE_STATIC_FILES="true"

# 必要なライブラリのインストール
# - gosu: docker内でroot実行
# - git: ソースコード管理
# - sqlite3: Sqlite3データベース
# - libpq-dev: PostgreSQLデータベース用ライブラリ
# - nodejs: サーバサイドJavaScript
# - yarn: JavaScriptパッケージマネージャ
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get install -y \
  gosu \
  git \
  sqlite3 \
  libpq-dev \
  nodejs \
  yarn

# 実行ユーザ、実行ディレクトリの追加
RUN  useradd -m app -s /bin/bash -u 5000 && \
mkdir /app && \
chown app:app /app && \
chmod 777 /app

# サービス開始スクリプト
CMD ["/app/startup.sh"]

# /app配下へのソースの配置
WORKDIR /app
COPY --chown=5000:5000 ./ /app/
```

## .dockerignoreの設定

railsアプリのディレクトリをdockerでコピーする際に無視したいファイルを `.dockerignore` ファイルに指定できます。マスターキーをコンテナに含めてしまうのはセキュリティ上良くないので、少なくともマスターキーは無視するファイルに含めた方が良いでしょう。

```.dockerignore 
# 環境変数設定ファイルの無視
/.env
# マスターキーの無視
/config/master.key
# Sqliteデータベースの無視
/db/*.sqlite3
/db/*.sqlite3-journal
# share配下の無視
/share/*
!/share/.keep
```

# docker-composeの実行

上記の設定を行った上でdockerコンテナを作成し、動かします。

```
$ sudo /minimal/docker-compose build
$ sudo /minimal/docker-compose run -d
```

以下のコマンドでコンテナが正常に動作していることを確認します。

```
$ sudo docker ps
```

Nginxを通じてRailsのコンテナにアクセスできることを確認します。curlコマンドでアクセス出来ることや、ブラウザで操作が出来ることを確認します。

以上です。

# 補足

1. 今回の `docker-compose.yml` では記述の簡便化のためにパスワードやマスターキーをそのまま設定ファイル内の環境変数として書いていますが、これをそのままリポジトリ管理してしまうと非常に危険です。「environment」の代わりに「env_file」を使い、環境変数は全て別ファイルに定義する等、安全策を取りましょう。
1. 動作確認はCentOS 7(v7.6.1810)でのみ行っています。
1. 本記事内容はあくまで例であり、記事内容によって発生したいかなる損害に対しても責任は取りかねます。

# 参考記事

- [CentOS7にDockerをインストールする](https://qiita.com/inakadegaebal/items/be9fecce813cebec5986)
- [Docker、ボリューム(Volume)について真面目に調べた](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)
- [新規Railsプロジェクト作成時のコマンド](https://qiita.com/k_senbei/items/02c4bacd5bb862535d9d)