使用Docker建立Rails环境(Nginx+Unicorn+MySQL)并部署到Fargate之前的步骤〜开发环境部分〜

摘要

由于在将Rails应用部署到ECS Fargate时遇到了比预想更多的困难,所以我决定将我的输出和备忘录结合起来写下来。关于ECS的想法是:“只需要直接部署在开发环境中创建的镜像就可以了☆\(^o^)/”,但事实并没有那么简单。本次内容是关于开发环境。

环境

    • M1mac

 

    • Visual Studio Code

 

    Docker(20.10.17)

组成

今回、Dockerfileを開発環境と本番環境で分けるか否かでとても悩みました。環境毎で動きが異なる部分があるため、分けた方が正直楽でした。しかし同時に「環境差分も生まれてしまう」こと、「DRYの原則に乗っ取っていない」ことから、Dockerfileは環境統一を図っています。実務ではどうなのでしょうか。。

.(各Railsファイル)
├── config
|   └── datebase.yml
|   └── unicorn.rb
├── nginx
|    ├── Dockerfile
|    └── nginx.conf
├── docker-compose.dev.yml
├── (docker-compose.prd.yml)←次回作成
├── Dockerfile
├── entrypoint.sh
├── Gemfile
└── Gemfile.lock

设定步骤。

1. 创建Rails的Dockerfile和docker-compose.dev.yml文件
2. 进行Rails的设置
3. 编辑每个配置文件
4. 编辑Rails的Dockerfile和docker-compose.dev.yml文件
5. 创建Rails的Web应用程序

创建Rails的Dockerfile和docker-compose.dev.yml。

まずは、アプリのための新しいフォルダと必要なファイルを作成していきます。
ここではデスクトップ配下にフォルダを作成していきます。

cd ~/Desktop
mkdir myapp-miguchi && cd myapp-miguchi
touch Dockerfile docker-compose.dev.yml Gemfile Gemfile.lock

それぞれのファイルの作成が完了したら、順番にファイルの中身を記述していきます。

Dockerfile (仅需一种选项)

FROM --platform=linux/x86_64 ruby:3.1

#環境変数
ENV APP="/myapp-miguchi"  \
    CONTAINER_ROOT="./" 

RUN apt-get update && apt-get install -y \
        nodejs \
        mariadb-client \
        build-essential \
        wget \
        yarn

WORKDIR $APP
COPY Gemfile Gemfile.lock $CONTAINER_ROOT
RUN bundle install

在这里,通过–platform=linux/x86_64指定基础镜像,并指定CPU架构。在M1Mac上安装ruby镜像时,将使用–platform=linux/arm64创建。虽然在本地运行容器时没有问题,但在部署到ECS时不受支持,因此在这里进行了指定。详情请参阅下面的文章。

参考文章:
M1 Mac 的基本知识

在第一行和倒数第二行的描述中,当Gemfile被更新时,bundle install将会被执行。

Gemfile -> 文件名为Gemfile

source "https://rubygems.org"
gem "rails", "~> 7.0.3"

docker-compose.dev.yml 的中文释义

version: '3'

services:
  web:
    build: .
    tty: true
    stdin_open: true
    ports:
      - '3000:3000'
    volumes:
      - .:/myapp-miguchi #任意のアプリ名

因為我想要在開發環境中將本地文件掛載到容器中來使用,所以我使用volumes來進行掛載。將容器掛載的好處是,一方面能夠「只在執行命令時使用文件,從而避免容器變得過大」,另一方面能夠「直接反應容器內的更改到本地文件中」。

完成上述三个文件的编辑后,使用以下命令启动容器。

docker-compose -f docker-compose.dev.yml up -d

2. Rails的配置设置

我将进入之前用docker-compose构建的web容器中。

docker-compose -f docker-compose.dev.yml exec web bash

接下来,我们将在这个Web容器中生成一个新的Rails项目。

rails new . --force --database=mysql --skip-bundle

由于数据库使用MySQL,因此在–database中进行了指定。
为了在容器启动时执行bundle install,此处跳过了该步骤。

每个Rails应用程序的文件都被添加,并且Gemfile也被更新。
在Gemfile被更新之后,我们会添加unicorn gem并稍后进行配置。

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.1.2"
+ gem "unicorn"

~以下省略~

当工作完成后,脱离容器一次。

exit

3. 每个设置文件的编辑

这里会编辑三种类型的文件:
编辑database.yml文件
创建Nginx的Dockerfile和配置文件
创建Unicorn的配置文件

编辑database.yml文件

由于database.yml是在刚刚使用rails new命令生成的config文件夹下,因此我们需要编辑那个文件。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch("DB_USERNAME", "root") %>
  password: <%= ENV.fetch("DB_PASSWORD", "password") %>
  host: <%= ENV.fetch("DB_HOST", "db") %>

development:
  <<: *default
  database: myapp_miguchi_development

test:
  <<: *default
  database: myapp_miguchi_test

production:
  <<: *default
  database: <%= ENV["DB_DATABASE"] %>
  username: <%= ENV["DB_USERNAME"] %>
  password: <%= ENV["DB_PASSWORD"] %>
  host: <%= ENV['DB_HOST'] %>

使用ENV.fetch来设置默认值。如果环境变量没有值,则返回第二个参数的值(如果是用户名,则为root)。当我们稍后重新编辑docker-compose时,我们将在环境变量中添加值。生产环境的环境变量将在AWS这个生产环境中设置。(生产环境章节)

参考文章:
ENV[]和ENV.fetch()的区别【Rails/Ruby】

使用Dockerfile和配置文件创建Nginx。

在Nginx的配置中,我们将创建一个用于Nginx的Dockerfile和nginx.conf文件。

cd ~/Desktop/myapp-miguchi
mkdir nginx && cd nginx
touch Dockerfile nginx.conf
FROM --platform=linux/x86_64 nginx:stable
#デフォルトのnginxファイルを削除して作成したものコンテナ内にコピー
RUN rm -f /etc/nginx/conf.d/*
#自分のapp名.confを記述
COPY nginx.conf /etc/nginx/conf.d/myapp-miguchi.conf
#-c以降の設定ファイルを指定して起動 daemon offでフォアグラウンドで起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

Nginx用DockerfileもRails用同様にCPUアーキテクチャを指定しています。

upstream unicorn {
  #ユニコーンソケットの設定
  server unix:/myapp-miguchi/tmp/sockets/.unicorn.sock fail_timeout=0;
}

server {
  #IPとポートの指定
  listen 80 default;
  #サーバーネームの指定
  server_name localhost;
    #アクセスログとエラーログの出力先
  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;
  #ドキュメントルートの指定
  root /myapp-miguchi/public;

  client_max_body_size 100m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @unicorn;
  keepalive_timeout 5;

  location @unicorn {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://unicorn;
  }
}

NginxとUnicornの接続方法はソケット通信を採用しています。そのため最初のupstream unicornにおいて、Unicornと接続するファイルの場所を指定しています。

参考文章:
连接Unicorn和Nginx有两种方法:UNIX域套接字和反向代理。

创建Unicorn的配置文件

在Unicorn中的配置中,我们会在config文件夹下创建配置文件。

cd ~/Desktop/myapp-miguchi/config
touch unicorn.rb
# frozen_string_literal: true

#ワーカーの数。後述
$worker = 2
#何秒経過すればワーカーを削除するのかを決める
$timeout = 30
#自分のアプリケーション名。
$app_dir = '/myapp-miguchi'
#リクエストを受け取るポート番号を指定。後述
$listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
#PIDの管理ファイルディレクトリ
$pid = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
#エラーログを吐き出すファイルのディレクトリ
#$std_log = File.expand_path 'log/unicorn.log', $app_dir

#上記で設定したものが適応されるよう定義
worker_processes  $worker
working_directory $app_dir
stderr_path $std_log
stdout_path $std_log
timeout $timeout
listen  $listen
pid $pid

#ホットデプロイをするかしないかを設定
preload_app true

#fork前に行うことを定義。後述
before_fork do |server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      Process.kill 'QUIT', File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

在Unicorn的配置文件中,在部署应用程序的过程中我对日志输出的目标位置感到困惑。在上述文件中,我将输出错误日志的文件目录设置注释掉了。起初我指定了日志输出的位置,但在生产环境中无法查看日志。看来需要将日志发送到标准输出,通过注释掉配置然后才能成功输出日志,所以现在我就采用了这个配置。

参考文章:
由于在DOCKER上运行UNICORN需要将日志输出到标准输出,因此现在可以在ECS上将日志输出到Amazon CloudWatch Logs上,下面是相关教程。

4. 编辑Rails的Dockerfile和docker-compose.dev.yml。

Note: The provided phrase appears to be a mix of Japanese and English.

为了在本地主机上显示Rails应用程序并将其部署到生产环境,我们将在Dockerfile和docker-compose中添加一些配置。

FROM --platform=linux/x86_64 ruby:3.1

#環境変数
ENV APP="/myapp-miguchi"  \
        CONTAINER_ROOT="./" 

#ライブラリのインストール
RUN apt-get update && apt-get install -y \
        nodejs \
        mariadb-client \
        build-essential \
        wget \
        yarn 

#実行するディレクトリの指定
WORKDIR $APP
COPY Gemfile Gemfile.lock $CONTAINER_ROOT
RUN bundle install
#↓懸念点(開発環境ではCOPYをしたくないが、本番環境でする必要がある)
COPY . .

#DB関連の実行
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

#nginxコンテナからrailsコンテナの以下のファイルをマウントすることでソケット通信を可能にする
VOLUME ["/myapp-miguchi/public"]
VOLUME ["/myapp-miguchi/tmp"]

#railsアプリ起動コマンド
CMD ["unicorn", "-p", "3000", "-c", "/myapp-miguchi/config/unicorn.rb", "-E", "$RAILS_ENV"]

在每个环境下,COPY部分的行为是不同的。在开发环境中,可以从本地挂载文件,所以这段描述是不需要的。然而,在生产环境中,无法从本地挂载文件,所以需要将文件复制到容器内部。虽然无法利用docker-compose.dev.yml中提到的volume的优点,但这是不得已的办法。。

在启动Rails应用程序的命令中,通过将环境部分指定为$RAILS_ENV和环境变量来实现在开发环境中启动devlopment,在生产环境中启动production。

version: '3'

services:
  web:
    build:
      context: .
    tty: true
    stdin_open: true
    ports:
      - '3000:3000'
    volumes:
      - .:/myapp-miguchi #任意のアプリ名
    depends_on:
      - db
    links:
      - db
    environment:
      RAILS_ENV: development
      DB_USER: root
      DB_PASSWORD: root
      DB_HOST: db

  db:
    platform: linux/x86_64 #M1チップ対応のため追記
    restart: always
    image: mysql:8
    ports:
      - 3306:3306
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      TZ: Asia/Tokyo

  nginx:
    build:
      context: ./nginx
    ports:
      - 80:80
    restart: always #明示的にstopさせるまでリスタートする。(失敗するたび遅延あり)
    depends_on:
      - web 

volumes:
  mysql-data:

docker-compose主要包括对DB和Nginx容器的描述,以及添加环境变量等信息。

最终,我们要创建entrypoint.sh来执行pid删除和DB创建的操作。

cd ~/Desktop/myapp-miguchi
touch entrypoint.sh
#!/bin/bash
set -e

#pidの削除&ディレクトリの作成
rm -f tmp/pids/server.pid
mkdir -p tmp/sockets
mkdir -p tmp/pids

#DBコンテナが起動するまで待機する処理
until mysqladmin ping -h $DB_HOST -P 3306 -u root --silent; do
  echo "waiting for mysql..."
  sleep 3s
done
echo "success to connect mysql"

#DB作成コマンド
bundle exec rails db:create
bundle exec rails db:migrate
bundle exec rails db:migrate:status
bundle exec rails db:seed
#本番環境のみ実行したいが、現状開発環境でも実行されてしまう。
bundle exec rails assets:precompile RAILS_ENV=production

#Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

我最想在这里执行的是等待DB容器启动的过程。在初次创建应用程序时,我们启动了Rails和MySQL容器,并执行了db:create操作,但遇到了无法连接MySQL的问题。问题的原因是“DB容器已经启动,但需要一些时间来完成启动”。
尽管MySQL还在启动中且无法连接,但是Rails容器仍然尝试执行db:create操作。因此,我们进行了“确认能够连接到MySQL后执行db:create”的处理。
assets:precompile仅在生产环境中需要执行,但当前在开发环境中也会运行。虽然没有特别的影响,但这也需要改进。

参考文献:
使用DockerCompose在Rails上等待MySQL启动并进行db:migrate
【Rails】在生产环境中配置资产预编译

编辑完毕后,我会在本地主机上显示Rails的首页界面。

docker-compose -f docker-compose.dev.yml up --build -d
rails-top.png

5. 开发Rails的Web应用程序

我們現在要創建一個簡單的樣例應用程序,然後在本地主機上進行確認。
剛剛我們使用docker-compose重新構建了web容器,現在我們要進入它內部。

docker-compose -f docker-compose.dev.yml exec web bash

执行应用程序生成命令。

rails g scaffold product name:string price:integer vender:string

当生成完成后,我们将离开容器。

exit

我将使用Docker Compose重新启动容器。

docker-compose -f docker-compose.dev.yml up --build -d
products for fix.png

请参考相关文章。

学习Docker:
这是一门非常易懂且推荐的Docker课程。

 

参考文章:
M1 Mac 的基础知识
ENV[]和ENV.fetch()的区别【Rails/Ruby】
Unicorn和Nginx的连接方法有两种:UNIX域套接字和反向代理
在DOCKER中运行UNICORN需要将日志输出到标准输出
由于在ECS中可以将日志输出到Amazon CloudWatch Logs,因此提供了教程
在DockerCompose上的Rails中,等待MySQL启动后再进行db:migrate
【Rails】关于生产环境中资产预编译的设置
在DockerCompose上的Rails中,等待MySQL启动后再进行db:migrate
【Rails】关于生产环境中资产预编译的设置

bannerAds