Rails 用の nginx の docker image を multi-stage build で作成する

# この記事でやりたいこと

本番環境で Rails application を動かすとき、public ディレクトリ配下の assets を nginx から直接配信したいですよね。Dockerのversion 17.05 以前だと、public配下のassetsを取り出した docker imageを作成して、 `docker run` で pipe して、それ上で nginx の docker image を作成したりといろいろ手間がかかっていました。

でも、Docker の version 17.05 以降からサポートされた [multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/) を使ったらかなりシンプルに書き換えれるのでそれを試して見たいと思います。

# 事前準備

前提として、手元に Rails new を実行して Rails application があるとします。
まずは、Rails application 用の Dockerfile を作成して、docker image を作成しておきます。Dockerfileは以下のような感じです。Dockerfile 内で `assets:precompile` をしておきます。

```dockerfile:Dockerfile

FROM ruby:2.5.1
ENV LANG C.UTF-8

RUN apt-get update -qq
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends apt-utils
RUN apt-get install -y libpq-dev graphviz imagemagick
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && apt-get install -y nodejs build-essential
RUN npm install -g yarn

RUN gem install bundler
RUN gem update

ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

ADD Gemfile* $APP_HOME/
ENV GEM_HOME /bundle
ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
  BUNDLE_JOBS=2 \
  BUNDLE_PATH="$GEM_HOME"
RUN bundle install

ADD package.json $APP_HOME/
ADD yarn.lock $APP_HOME/
RUN yarn install

ADD . $APP_HOME

RUN bundle exec rails assets:precompile

CMD ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
```

docker コマンドで Rails application を build しちゃいます。

```sh

docker build -t yourproject/rails .
```

# nginx 用の Dockerfile の作成

続いて、 multi-stage build を用いて nginx 用の Dockerfile を作成します。ポイントは

- `FROM` コマンドを２回呼び出します。１回目は assets を取り出す用の rails application の docker image です。
- `COPY --from=0` で１回目に `FROM` で呼び出した image から assets だけをコピーします。
- ついでにですが、dockerize というツールを使って、起動時に環境変数からnginxの設定ファイルを書き換えれるようにしておきます。

```dockerfile:Dockerfile.nginx

FROM yourproject/rails
FROM nginx:1.11.9-alpine

RUN apk --no-cache --update add curl
ENV DOCKERIZE_VERSION v0.6.1
RUN curl -sL https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz  | tar -C /usr/local/bin -xz

RUN mkdir -p /etc/nginx/sites-enabled

COPY nginx.conf /etc/nginx/nginx.conf
COPY rails.tmpl /srv/rails.tmpl
COPY --from=0 /app/public /srv/shared/public

CMD ["dockerize", "-template", "/srv/rails.tmpl:/etc/nginx/sites-enabled/rails", "nginx", "-g", "daemon off;"]
```

以下は nginx.conf と `sites-enabled` 配下に配備する Rails application 用の nginx の設定ファイルです。

```nginx:nginx.conf

user nginx;
worker_processes  1;

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

events {
  worker_connections  1024;
}

http {

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

  map $http_upgrade $connection_upgrade {
      default upgrade;
      ''      close;
  }

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

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout  65;

  gzip  on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_proxied any;
  gzip_vary off;
  gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/json;
  gzip_min_length  1000;
  gzip_disable     "MSIE [1-6]\.";

  server_names_hash_bucket_size 64;
  types_hash_max_size 2048;
  types_hash_bucket_size 64;

  # use this when need to record response time.
  log_format timed_combined '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '$request_time $upstream_response_time';

  include /etc/nginx/sites-enabled/*;
}
```

```nginx:rails.tmpl

server {

    server_name _;

    listen   *:{{ default .Env.SERVER_LISTEN_PORT "80" }};
    listen   [::]:{{ default .Env.SERVER_LISTEN_PORT "80" }} default ipv6only=on;

    location /assets {
        autoindex    on;
        alias /srv/shared/public/assets;
    }

    location /packs {
        autoindex    on;
        alias /srv/shared/public/packs;
    }

    location /nginx_status {
        empty_gif;
        break;
    }

    location /cable {
        proxy_pass http://127.0.0.1:3000/cable;
        proxy_http_version 1.1;
        proxy_set_header Upgrade websocket;
        proxy_set_header Connection Upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $proxy_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $http_host;

        client_max_body_size 30M;
        proxy_read_timeout 600s;
        proxy_connect_timeout 60s;
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

```

以上のファイルを配備したら、 nginx 用の docker image を build します。

```shell
docker build -t yourproject/nginx -f Dockerfile.nginx
```

こんな感じで簡単に public 配下の assets を配信可能な nginx の docker image が簡単に作成できます。あとは、kubernetes なり、ECS なりでいい感じにデプロイすればよいかと思います。今個人的に GKE 上で Rails application のデプロイ環境を構築しているので、そこらへんの構築手順の記事もそのうち書ければと思います。