Nginx + Rails (Puma) on Docker のいくつかの実用パターン


フロントとバックエンドを同一コンテナで済ませるパターンと, 1 プロセス 1 コンテナの原則に従い個別コンテナでやるパターンです.

他にも, [共有ストレージ専用の busybox を利用するパターン](http://qiita.com/k-shogo/items/3c887cbf86b3cab85b72) などがあるようです.

# 今回の要件

- フロントエンドは Nginx, バックエンドは Rails5 (Puma)
- Nginx-Puma 間は socket 通信
- Rails の静的コンテンツ (`public/` 配下) は Nginx が処理する

# パターンA: Nginx と Rails が同一コンテナ

全てのコードはこちらにあります.
https://github.com/na-o-ys/rails_docker_sample/tree/single-container

#### 全体図

`$ rails new` 直後のディレクトリに `Dockerfile` などを配置したもの.

一つの Dockerfile で Rails のセットアップ, Nginx のインストール & セットアップを行うため, 複数コンテナの連携など特別なことをする必要は無い.

```主要ファイルの構成
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── app/
├── config
│   ├── ...
│   └── puma.rb
├── log
├── nginx.conf
├── public
│   ├── 404.html
│   └── ...
└── .dockerignore
```

#### Puma

最終行で, `#{app_root}/tmp/sockets/puma.sock` に bind しています.

```ruby:config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
port        ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
```

#### Nginx

- `/app/public` を静的コンテンツとして配信
- その他を `/app/tmp/sockets/puma.sock` にプロキシ

```nginx:nginx.conf
# https://github.com/puma/puma/blob/master/docs/nginx.md
upstream app {
  server unix:///app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name localhost; # TODO

  keepalive_timeout 5;

  # static files
  root /app/public;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # static files
    if (-f $request_filename) {
      break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://app;
      break;
    }
  }

  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    expires max;
    break;
  }
}
```

#### Dockerfile

ホストのカレントディレクトリ (Rails root) を `/app` にマウントしています.

```Dockerfile
FROM ruby:2.3.1
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs && \
    apt-get install -y nginx

# Nginx
ADD nginx.conf /etc/nginx/sites-available/app.conf
RUN rm -f /etc/nginx/sites-enabled/default && \
    ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf

# Rails App
RUN mkdir /app
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
ADD . /app
RUN mkdir /app/tmp/sockets

# Start Server
CMD bundle exec puma -d && \
    /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

#### .dockerignore

ホスト側のゴミファイルが紛れ込まないように.

```.dockerignore
tmp/*
log/*
.git
```

# パターンB: Nginx と Rails が個別コンテナ

#### 全体図

全てのコードはこちらにあります.
https://github.com/na-o-ys/rails_docker_sample/tree/multiple-container

`$ rails new` 直後のディレクトリに `Dockerfile` などを配置したもの.

Rails 用の Dockerfile (ルート直下) と Nginx 用の Dockerfile (`containers/nginx/Dockerfile`) を作成し, Docker Compose で２つのコンテナをまとめて起動します.

```主要ファイルの構成
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── app/
├── config
│   ├── ...
│   └── puma.rb
├── containers
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
├── log/
├── public
│   ├── 404.html
│   └── ...
└── .dockerignore

```

#### .dockerignore

`containers/` を ignore する.

```.dockerignore
tmp/*
log/*
.git
containers
```

#### Puma

ファイルはパターンAと全く同じです. 最終行で `#{app_root}/tmp/sockets/puma.sock` に bind しているのがポイント.

```ruby:config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
port        ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
```

## Dockerfile (Rails)

`VOLUME` を使って, `/app/public` と `/app/tmp` を他コンテナからマウント可能にしています. それぞれ静的コンテンツと socket を Nginx コンテナから利用するためです.

```Dockefile
FROM ruby:2.3.1
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs

# Rails App
RUN mkdir /app
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install
ADD . /app
RUN mkdir -p tmp/sockets

# Expose volumes to frontend
VOLUME /app/public
VOLUME /app/tmp

# Start Server
# TODO: environment
CMD bundle exec puma
```

#### Nginx

パターンAと全く同じです.

- `/app/public` を静的コンテンツとして配信
- その他を `/app/tmp/sockets/puma.sock` にプロキシ

```nginx:nginx.conf
# https://github.com/puma/puma/blob/master/docs/nginx.md
upstream app {
  server unix:///app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name localhost; # TODO

  keepalive_timeout 5;

  # static files
  root /app/public;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # static files
    if (-f $request_filename) {
      break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://app;
      break;
    }
  }

  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    expires max;
    break;
  }
}
```

#### Dockerfile (Nginx)

nginx のイメージを使います. `/etc/nginx/sites-enabled/` がデフォルトで認識されないので, `/etc/nginx/conf.d/` にコンフィグを配置しています.

```containers/nginx/Dockerfile
FROM nginx:1.11.3
RUN rm -f /etc/nginx/conf.d/*
ADD nginx.conf /etc/nginx/conf.d/app.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

#### Docker Compose

ポイントは `nginx` の `volumes_from: web` です. これにより, `web` コンテナの `/app/public` と `/app/tmp` が `nginx` コンテナにマウントされ, 静的コンテンツ配信と socket 通信が可能になります.

```yml:docker-compose.yml
version: '2'
services:
  web:
    build: .
    volumes:
      - /app/log # persist logs
    depends_on:
      - db

  nginx:
    build: containers/nginx
    ports:
      - "80:80"
    volumes_from:
      - web
    depends_on:
      - web

  db:
    image: postgres
```

# 参考

- Quickstart: Docker Compose and Rails - https://docs.docker.com/compose/rails/
- VOLUME - https://docs.docker.com/engine/reference/builder/#/volume
- Compose file reference - https://docs.docker.com/compose/compose-file/
- 複雑な構成のrailsアプリをdockerで動かしたい場合はどうするべきか - http://qiita.com/k-shogo/items/3c887cbf86b3cab85b72
- Docker上でnginx + unicorn + mysql環境を構築 - http://qiita.com/konpyu/items/2c93e52e9cabc547d94b