😎

Rails7 の本番環境(Docker)を楽々に作成してSSL化する

2022/09/09に公開

目的

WEBサーバー(nginx,apacheなど)やアプリケーションサーバー(unicorn,pumaなど)、その他のSSL対応手続きに必要な、面倒なセットアップを大幅にカットあるいは簡略化することで、ローカル環境で実装したRailsとDockerのアプリケーションの本番環境作成を容易にすること。
ステージングに関しては別の記事で書いているので、かなり作業は似ていますが、こちらも見ておくとタメになると思います。

ステージング環境作成
https://zenn.dev/dragonarrow/articles/48e4257c5fcbe7

前提条件

  • ローカル環境でアプリケーションが正常に動いている (http://localhost:3000)
  • 本番サーバーの契約済み
  • 本番サーバーのインフラ設定(ネットワーク、セキュリティなど含む)が完了している
  • 本番サーバー内にdockerとdocker-composeがインストールされている
  • 独自ドメインを取得済み
  • DNS設定にて、ドメインとパプリックIPを紐付け済み

別に一緒でなくて全然構いませんが、筆者の環境は以下のようになっていますので、参考までに。

  • ドメイン取得 => ムームードメインで購入
  • ステージングサーバー => EC2でインスタンス作成
  • ドメインとパプリックIPを紐付け => Route53で設定済み

バージョン情報

  • 手元の作業PC: Apple M1 Pro
  • Rails: 7.0.2
  • Ruby: 3.1.1
  • PostgreSQL: 14.3
  • Docker: 20.10.17
  • Docker-compose.yml: 2.10.1

ゴール

ブラウザに 独自ドメインでアクセスできること。
今回は、例として、https://www.mysite.work で アクセスできるようにします。

ディレクトリ構成

プロジェクトルート
├── data (postgresqlのデータマウント用)
├── docker-compose.production.yml
├── https-portal
└── web
    ├── .env
    ├── Dockerfile.prod
    ├── Gemfile
    ├── Gemfile.lock
    ├── Rakefile
    ├── app
    ├── bin
    ├── config
    ├── config.ru
    ├── db
    ├── entrypoint.sh
    ├── lib
    ├── log
    ├── public
    ├── storage
    ├── test
    ├── tmp
    └── vendor

「Rails7.0.2 + PostgreSQL14.3 開発環境(docker)を構築する」の構成をベースにしています。
https://zenn.dev/dragonarrow/articles/8503c36d19eb6f

手順1: 各ファイルの作成・編集

.envを作成

秘匿したい情報はこのファイルに記載

DB_PROD_DATABASE="xxxxxxxxxx"
DB_PROD_HOST="db" # 今回は、docker-composeで立ち上げるdbサービスを利用
DB_PROD_USER="xxxxxxxxxx"
DB_PROD_PASSWORD="xxxxxxxxxx"
DB_PROD_PORT="xxxxx" # デフォルトポート(5432)から変えるのを推奨

docker-commpose.production.yml作成

docker-commpose.production.yml
version: '3'
services:
  db:
    image: postgres:14.3
    ports:
      - ${DB_PROD_PORT}:5432
    volumes:
      - ./data:/var/lib/postgresql/data
      # - ./db/initdb.d:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_USER: ${DB_PROD_USER}
      POSTGRES_PASSWORD: ${DB_PROD_PASSWORD}
    restart: always
    networks:
      dragon-network:
        ipv4_address: 192.168.1.2

  web:
    build: 
      context: web
      dockerfile: Dockerfile.prod
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0' -e production"
    restart: always
    volumes:
      - ./web:/app
    environment:
      DB_PROD_DATABASE: ${DB_PROD_DATABASE}
      DB_PROD_HOST: ${DB_PROD_HOST}
      DB_PROD_USER: ${DB_PROD_USER}
      DB_PROD_PASSWORD: ${DB_PROD_PASSWORD}
      EDITOR: vim
      RAILS_ENV: production
    depends_on:
      - db
    networks:
      dragon-network:
        ipv4_address: 192.168.1.3

  https-portal:
    image: steveltn/https-portal:1
    ports:
      - '80:80'
      - '443:443'
    restart: always
    environment:
      DOMAINS: 'mysite.work => www.mysite.work, www.mysite.work -> http://web:3000'
      STAGE: "production"
      WORKER_PROCESSES: 2
      CLIENT_MAX_BODY_SIZE: 10M
    volumes:
      - ./https-portal:/var/lib/https-portal
    networks:
      dragon-network:
        ipv4_address: 192.168.1.4

networks:
  dragon-network:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.1.0/24
          gateway: 192.168.1.1

https-portal の詳細に関しては以下をご覧ください
https://github.com/SteveLTN/https-portal

  • 今回のポイント
    • mysite.workにアクセスするとwww.mysite.workにリダイレクトするように設定
    • STAGE: "production"により、local,stagingのオレオレ証明書とは違って、let's encryptによる正式なSSL証明書が自動発行される
  • 注意点
    • サービス名webのポート3000番をdockerネットワークの外部にまで公開しない。ports: 3000:3000などでポートフォワーディングしてしまうとブラウザから、[server_public_ip]:3000でアクセスできてしまいます。
    • インフラでネットワーク設定がある場合は、インバウンドルールで、DB_PROD_PORTの指定ポートを開けておく

environments/production.rbの強制SSLをオフにしておく

プロジェクトルート/web/config/environments/production.rb

production.rb
...
	config.force_ssl = false
...

https-portalサービスにて、SSL証明書を発行するときに、httpの通信が発生するため、docker-compose.production.ymlにて80番ポートが開いていなかったり、force_sslをtrueにしてしまっていると、SSL証明書の発行に失敗してしまう。

publicディレクトリ内の静的ファイルのアクセスを許可する

プロジェクトルート/web/config/environments/production.rb

production.rb
...
	config.public_file_server.enabled = true
...

上記設定に変更しないと、publicディレクトリ内の静的ファイルのアクセスが全て404 Not Foundになる。
(たとえば、ドメイン/ads.txtやドメイン/robots.txtなど)

Dockerfile.prodを作成

# gemインストールのみに使用
FROM ruby:3.1.1-alpine as builder

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

COPY Gemfile Gemfile.lock ${ROOT}

RUN apk add \
    alpine-sdk \
    build-base \
    # sqlite-dev \
    # mysql-client \
    # mysql-dev \
    postgresql-dev \
    postgresql-client \
    tzdata \
    git

# M1のRails(Docker環境)起動時にnokogiriがLoadErrorとなる問題の解決方法
RUN apk add --no-cache gcompat

RUN apk add --no-cache --virtual .build-deps \
    build-base \
    ruby-dev

RUN gem install bundler && bundle install
RUN bundle update webdrivers selenium-webdriver
RUN bundle exec rails assets:precompile RAILS_ENV=production

# マルチステージビルド
FROM ruby:3.1.1-alpine AS runner

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apk update && \
    apk add \
        postgresql-dev \
        tzdata \
        bash \
        gcompat \
        vim

RUN apk add --no-cache nodejs

COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . ${ROOT}

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
  • ポイント
    • docker-composeでビルドしたときに、アセットのプリコンパイルを行うようにしている
      • これをしないと、jsとcssが本番に反映されない
  • 注意点
    • EXPOSE 3000などで、外部に公開しない

web/config/database.ymlを編集

web/config/database.yml
production:
  <<: *default
  database: <%= ENV["DB_PROD_DATABASE"] %>
  host: <%= ENV["DB_PROD_HOST"] %>
  username: <%= ENV["DB_PROD_USER"] %>
  password: <%= ENV["DB_PROD_PASSWORD"] %>

(uglifierを使っている人) terserに置き換え

2年前にRails5.2で動かしていた頃は、自分は、uglifierというgemを使って、javascriptのコードを軽量化していたが、Rails7.0で同じように使用すると、precompile時にエラーを吐いていた。ES6構文まではuglifierが使えるけど、ES2020とかになるとterserに置き換えないといけないらしい。以下のようにしてterserに置き換えることで解決した。

Gemfile
- gem 'uglifier'
+ gem 'terser'
web/config/environments/production.rb
- config.assets.js_compressor = :uglifier
+ config.assets.js_compressor = :terser

手順2: アプリケーションを本番サーバーに転送

それぞれのやり方で転送してください。自分は、fileZillaを愛用しております。

手順3: 本番環境で起動

ビルド

$ docker-compose -f docker-compose.production.yml build

DB作成とマイグレーションファイルの実行

$ docker-compose -f docker-compose.production.yml run --rm web bundle exec rails db:migrate:reset RAILS_ENV=production

起動

 $ docker-compose -f docker-compose.production.yml up -d

すると、https-portalサービスからlet's encryptが自動で起動し、正式なSSL証明書が自動作成される。
ログで確認する。

docker-compose -f docker-compose.production.yml logs -f

証明書が発行されたら、ブラウザにて以下3点を確認する。

  1. https://www.mysite.workにアクセスして、Webページが表示される
  2. https://mysite.workにアクセスしてhttps://www.mysite.workにリダイレクトされる
  3. public内の静的ファイルにアクセスして、表示できる (https://www.mysite.work/robots.txt やhttps://www.mysite.work/ads.txtなど)

確認できたら、手順4に入る。

手順4: environments/production.rbの強制SSLをオンにしておく

プロジェクトルート/web/config/environments/production.rb

production.rb
...
	config.force_ssl = true
...

設定を変えたら、立ち上げ直す

$ docker-compose -f docker-compose.production.yml down
$ docker-compose -f docker-compose.production.yml up -d --build

変更のデプロイ

アプリケーションのコードを変更したときは、イメージを再構築し、アプリケーションのコンテナを作り直す必要があります。web というサービスを再デプロイするには、次のようにします:

$ docker-compose -f docker-compose.production.yml build web
$ docker-compose -f docker-compose.production.yml up --no-deps -d web

これは、まず web イメージの再構築するため、停止、破棄をしてから、web サービスのみ再作成します。Compose に --nodeps フラグを使うことで、web に依存するサービスの再作成をしません。

終わりに

今回はRailsとDockerで作成したローカル環境を容易に本番に作成する方法を紹介いたしました。
https-portalは、wordpressで使っている記事はよく見るものの、Railsで使っている記事は、自分が探した限りでは、ただの1件もなかったので、非常に価値のある記事が、公開できたのではないかと思っています。
ログに関しては、標準出力で出すように設定するなり、マウントして確認できるようにするなり、各自のやり方で設定してください。

docker + Railsの本番環境作成にぜひ役に立てば幸いです。

Discussion