Cuộc sống Nhật Bản
Yuto Blog

Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2へデプロイ方法のメモ

Nội dung chính
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2へデプロイ方法のメモ

AWS EC2 インスタンスの準備

AWS コンソールにアクセスする

「Launch Instance」をクリックする

Step 1: Enter nand and tags

インスタンス名とタグを入力する
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2

Step 2: Choose an Amazon Machine Image (AMI)

「Ubuntu Server 22.04 LTS (HVM), SSD Volume Type 64bit (x86」を選択する
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2

Step 3: Choose an Instance Type and Key pair

インスタンスタイプは「t2.micro」とSSH用のキーペアを選択する
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2

Step 4: Network settings

バブネットやセキュリティグループなどを選択する
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2

Step 5: Configure storage

無料利用枠だと最大30GBまで利用できる
Ruby on Rails + Docker + Capistrano + Puma + Nginx AWS EC2

そして、「Launch instance」をクリックしてインスタンスを立てる

インスタンスへアクセス

先ほどダウンロードしたキーペアを.ssh/key.pemとします。

権限を与える

Copy
1
sudo chmod 400 .ssh/key.pem
サーバーにアクセス
Copy
1
ssh -i .ssh/key.pem ubuntu@xx.xx.xxx.xxx
xx.xx.xxx.xxxは先作成したインスタンスのIPv4 Public IPです。

デプロイ用のユーザーを作成

「deploy」というユーザー名とすると

Copy
1
sudo adduser deploy
ユーザーの権限を与える
Copy /etc/sudoers
1
%deploy ALL=(ALL) NOPASSWD:ALL

deployユーザーに切り替える

Copy
1
sudo su - deploy
authorized_keysを追加
Copy
1
2
3
4
mkdir .ssh
sudo chmod 700 .ssh
touch ~/.ssh/authorized_keys
sudo chmod 600 ~/.ssh/authorized_keys
Copy ~/.ssh/authorized_keys
1
2
<ローカル環境で「cat ~/.ssh/id_rsa.pub」の内容>
もしまだなければ「ssh-keygen -t rsa -C」で作成する

これからローカル環境でssh deploy@xx.xx.xxx.xxxでインスタンスにアクセスできます。

サーバーで環境を準備

デプロイ用フォルダとファイルを作成

例えばアプリ名はmyappとすると:

Copy
1
2
3
4
5
6
7
8
9
10
sudo mkdir /deploy
sudo mkdir /deploy/apps
sudo mkdir /deploy/apps/myapp
sudo mkdir /deploy/apps/myapp/shared
sudo mkdir /deploy/apps/myapp/shared/config

sudo touch /deploy/apps/myapp/shared/config/application.yml
sudo touch /deploy/apps/myapp/shared/config/database.yml

sudo chown -R deploy:deploy /deploy/apps/
Ruby、Rails、Gitなどをインストール
  • rbenv
  • ruby
  • rails
  • libmysqlclient-dev / libpq-dev
  • git
  • bundler

Rubyをインストールする方法

Dockerをインストール

Dockerをインストールする方法は以下のリンクにあります。ベトナム語ですが、簡単なコマンドをコピーするだけでインストールできます。

Ubuntu 22.04でDockerをインストールする方法

dockerをビルド

Copy
1
cd /deploy/apps/myapp/shared/config
Dockerfileとdocker-compose.ymlをこちらにペースト

以下はmysqlredisを使うdocker-compose.ymlの例:

Copy /deploy/apps/myapp/shared/config/docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '3.3'

services:
  myapp_mysql:
    container_name: myapp_mysql
    image: mysql:8.2.0
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    restart: always
    volumes:
      - myapp_mysql:/var/lib/mysql
    environment:
      MYSQL_DATABASE: myapp_production
      MYSQL_ROOT_PASSWORD: admin
      TZ: 'Asia/Tokyo'
    ports:
      - '3306:3306'

  myapp_redis:
    container_name: myapp_redis
    image: redis:latest
    restart: always
    volumes:
      - myapp_redis:/data
    ports:
      - '6379:6379'

volumes:
  myapp_mysql:
    external: true
  myapp_redis:
    external: true

そして、ビルド!

Copy
1
docker-compose build
起動する

Copy
1
docker-compose up -d
データベース設定
Copy /deploy/apps/myapp/shared/config/database.yml
1
2
3
4
5
6
7
8
production:
  adapter: mysql2
  encoding: utf8mb4  
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: myapp_production
  username: admin
  password: admin
  host: "127.0.0.1"

秘密キー設定

ローカル環境で秘密キーを取得してから、サーバーのapplication.ymlに貼ります。

Copy
1
RAILS_ENV=production rails secret
Copy /deploy/apps/myapp/shared/config/application.yml
1
SECRET_KEY_BASE: '<secret_key>'

ENV定数管理Gemをインストール

figaro を使うと application.yml が使えます。

Githubページ

Copy Gemfile
1
gem "figaro"
Copy
1
figaro install
Capistrano設定

Capistrano gemを追加

Copy Gemfile
1
2
3
4
5
6
7
8
9
group :development do
  gem 'capistrano'
  gem 'capistrano3-puma'
  gem 'capistrano-rails'
  gem 'capistrano-rails-collection' # rails:rake:db:migrate などを使いたい場合
  gem 'capistrano-rails-console' # cap production rails:c でコンソールにアクセスしたい場合
  gem 'capistrano-rake' # cap production invoke:rake TASK=paperclip:refresh などを使いたい場合
  gem 'capistrano-rbenv'
end

Capistranoインストール

Copy
1
cap install
Capfile設定
Copy Capfile
1
2
3
4
5
6
7
8
9
10
11
require 'capistrano/rails'
require 'capistrano/rails/collection'
require 'capistrano/rails/console'
require 'capistrano/rake'
require 'capistrano/rbenv'
require 'capistrano/puma'
require 'capistrano/sidekiq'

# Install puma plugins
install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Systemd

デプロイファイル設定

Copy config/deploy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
lock '~> 3.17.1'

set :application, 'myapp'
set :repo_url, 'git@github.com:xxxxxxxxxxxxx.git'
set :deploy_to, '/deploy/apps/myapp'
set :pty, true
set :init_system, :systemd
set :keep_releases, 3

# rbenv
set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, File.read('.ruby-version').strip # set ruby version from the file
set :rbenv_prefix,
    "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a.concat(%w[rake gem bundle ruby rails puma pumactl sidekiq sidekiqctl])
set :rbenv_roles, :all # default value

# bundle
set :bundle_flags, '--deployment' # Show log when install gems

# puma
set :puma_init_active_record, true

append :linked_files, 'config/database.yml', 'config/application.yml'
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'node_modules'
Copy config/deploy/production.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
set :stage, :production
set :rails_env, :production
set :branch, 'main'
server 'xxx.xx.xx.xxx', user: 'deploy', roles: %w[web app db]

# nginx
set :nginx_config_name, 'myapp.com' # アプリのドメイン

# SSLを設定する場合
set :nginx_server_name, "localhost #{fetch(:nginx_config_name)}"
set :nginx_ssl_certificate, "/etc/letsencrypt/live/#{fetch(:nginx_config_name)}/fullchain.pem"
set :nginx_ssl_certificate_key, "/etc/letsencrypt/live/#{fetch(:nginx_config_name)}/privkey.pem"
set :nginx_use_ssl, true

Puma設定

Puma設定ファイルを自動でアップロードする場合

Copy
1
2
cap production puma:config
cap production puma:systemd:config
それから、サーバーでPuma設定ファイルを修正する

Copy /etc/systemd/system/puma_myapp_production.service
1
ExecStart=/home/deploy/.rbenv/shims/bundle exec --keep-file-descriptors puma -C /deploy/apps/ityogo/shared/puma.rb

Puma設定ファイルを手動で作成する場合

puma.rbファイルを作成

以下のような内容を/deploy/apps/myapp/shared/puma.rbに入れます。

Copy /deploy/apps/myapp/shared/puma.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env puma

directory '/deploy/apps/myapp/current'
rackup "/deploy/apps/myapp/current/config.ru"
environment 'production'

tag ''

pidfile "/deploy/apps/myapp/shared/tmp/pids/puma.pid"
state_path "/deploy/apps/myapp/shared/tmp/pids/puma.state"
stdout_redirect '/deploy/apps/myapp/shared/log/puma_access.log', '/deploy/apps/myapp/shared/log/puma_error.log', true

threads 0,16

bind 'unix:///deploy/apps/myapp/shared/tmp/sockets/puma.sock'

workers 0

restart_command 'bundle exec puma'

prune_bundler

on_restart do
  puts 'Refreshing Gemfile'
  ENV["BUNDLE_GEMFILE"] = ""
end

puma serviceファイルを作成

サーバーの/etc/systemd/systemに遷移して、sudo vi puma_myapp_production.serviceで以下のような内容を保存する

Copy /etc/systemd/system/puma_myapp_production.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Unit]
Description=Puma HTTP Server for myapp (production)
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/deploy/apps/myapp/current
# Support older bundler versions where file descriptors weren't kept
# See https://github.com/rubygems/rubygems/issues/3254
ExecStart=/home/deploy/.rbenv/shims/bundle exec --keep-file-descriptors puma -C /deploy/apps/myapp/shared/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
StandardOutput=append:/deploy/apps/myapp/shared/log/puma_access.log
StandardError=append:/deploy/apps/myapp/shared/log/puma_error.log

Restart=always
RestartSec=1

SyslogIdentifier=puma

[Install]
WantedBy=multi-user.target

それから作成したファイルを起動する

Copy zsh
1
sudo systemctl enable puma_myapp_production.service

nginx設定

nginxをインストール

Copy
1
sudo apt-get install nginx
因みに、nginxを完全に削除するコマンドは:

Copy
1
2
sudo apt-get purge nginx nginx-common
sudo apt-get autoremove
nginx設定ファイルを自動でアップロードする場合

Capfileに以下を追加する

Copy Capfile
1
install_plugin Capistrano::Puma::Nginx

それから以下のコマンドを実行して、自動でnginx設定ファイルを作成され、サーバーにアップロードされる

Copy zsh
1
cap production puma:nginx_config

nginx設定ファイルを手動で作成する場合

サーバーの/etc/nginx/sites-availableに遷移する

例えばサイト名はmyapp.comであれば、sudo vi myapp.comコマンドで以下のような内容を保存する

Copy /etc/nginx/sites-available/myapp.com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
upstream puma_myapp.com {
  server unix:/deploy/apps/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
  listen 80;
  server_name localhost myapp.com;
  return 301 https://$host$1$request_uri;
}

server {
  listen 443 ssl http2;
  ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
  server_name localhost myapp.com;
  root /deploy/apps/myapp/current/public;
  try_files $uri/index.html $uri @puma_myapp.com;

  client_max_body_size 4G;
  keepalive_timeout 10;

  error_page 500 502 504 /500.html;
  error_page 503 @503;

  location @puma_myapp.com {
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
    proxy_redirect off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass http://puma_myapp.com;
    # limit_req zone=one;
    access_log /deploy/apps/myapp/shared/log/nginx.access.log;
    error_log /deploy/apps/myapp/shared/log/nginx.error.log;
  }

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location = /50x.html {
    root html;
  }

  location = /404.html {
    root html;
  }

  location @503 {
    error_page 405 = /system/maintenance.html;
    if (-f $document_root/system/maintenance.html) {
      rewrite ^(.*)$ /system/maintenance.html break;
    }
    rewrite ^(.*)$ /503.html break;
  }

  if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
    return 405;
  }

  if (-f $document_root/system/maintenance.html) {
    return 503;
  }
}

ssl_certificatessl_certificate_keyの設定はSSL証明書の設定で、以下の記事を参考にするとSSL証明書が発行できます。

UbuntuでLet's Encryptを使用してSSL証明書を作成する方法

それからシンボリックリンクを作成する

Copy zsh
1
sudo ln -s /etc/nginx/sites-available/myapp.com /etc/nginx/sites-enabled/myapp.com

デプロイ!

Copy
1
cap production deploy
capistrano コマンド一覧
Copy
1
cap --tasks
NotImplementedError: OpenSSH keys only supported if ED25519 is availableのエラーが発生した場合
Copy
1
2
3
4
5
6
7
8
(Backtrace restricted to imported tasks)
cap aborted!
NotImplementedError: OpenSSH keys only supported if ED25519 is available
net-ssh requires the following gems for ed25519 support:
 * ed25519 (>= 1.2, < 2.0)
 * bcrypt_pbkdf (>= 1.0, < 2.0)
See https://github.com/net-ssh/net-ssh/issues/565 for more information
Gem::LoadError : "ed25519 is not part of the bundle. Add it to your Gemfile."
必要なGemを追加大丈夫だと思います。

Copy Gemfile
1
2
3
4
group :development do 
  gem 'ed25519'
  gem 'bcrypt_pbkdf'
end

platforms [x86_64-darwin-20] のエラーが発生する場合

Copy
1
2
3
remote:        Your bundle only supports platforms ["x86_64-darwin-20"] but your local platform
remote:        is x86_64-linux. Add the current platform to the lockfile with `bundle lock
remote:        --add-platform x86_64-linux` and try again.
以下のコマンドを実行すれば解決

Copy
1
bundle lock --add-platform x86_64-linux
assets precompile が失敗した場合

メモリが不足するかもしれないので、以下の記事を参考にしてください。

assets precompileがメモリ不足で失敗する

カスタムRakeタスク

assetsをローカルでコンパイルする

デプロイする時、assetsをコンパイルするのに凄く時間がかかる為、ローカルでコンパイルしておいてからサーバーにアップロードするのがおすすめです。

Viteの場合

Copy lib/capistrano/tasks/assets.rake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# frozen_string_literal: true

namespace :deploy do
  namespace :assets do
    Rake::Task['deploy:assets:precompile'].clear_actions

    desc 'Precompile assets locally and upload to servers'
    task :precompile do
      on roles(fetch(:assets_roles)) do
        # Local assets precompile
        run_locally do
          with rails_env: fetch(:rails_env) do
            execute 'rm -rf public/assets'
            execute 'rm -rf public/vite'
            execute 'rails assets:precompile RAILS_ENV=production'
          end
        end

        # Upload to server
        within release_path do
          with rails_env: fetch(:rails_env) do
            execute "rm -rf #{shared_path}/public/assets"
            upload!('./public/assets/', "#{shared_path}/public/", recursive: true)
            upload!('./public/vite/', "#{release_path}/public/", recursive: true)
          end
        end
      end
    end
  end
end

Webpackの場合

Copy lib/capistrano/tasks/assets.rake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# frozen_string_literal: true

namespace :deploy do
  namespace :assets do
    Rake::Task['deploy:assets:precompile'].clear_actions

    desc 'Precompile assets locally and upload to servers'
    task :precompile do
      on roles(fetch(:assets_roles)) do
        # Local assets precompile
        run_locally do
          with rails_env: fetch(:rails_env) do
            execute 'rails assets:clobber'
            execute 'rails assets:precompile'
            execute 'bin/webpack'
          end
        end

        # Upload to server
        within release_path do
          with rails_env: fetch(:rails_env) do
            execute "rm -rf #{shared_path}/public/assets"
            upload!('./public/assets/', "#{shared_path}/public/", recursive: true)
            upload!('./public/packs/', "#{release_path}/public/", recursive: true)
          end
        end
      end
    end
  end
end

デプロイの前に自動Gitプッシュ

デプロイの前に毎回Gitにプッシュするのが面倒なので、タスクを作ります。

Copy lib/capistrano/tasks/github.rake
1
2
3
4
5
6
7
8
desc 'Push source code to Github'
task :github_push do
  run_locally do
    execute "git push origin #{fetch(:branch)}"
  end
end

before 'deploy:starting', :github_push
Updated at 2024-01-05
Nếu bài viết có ích thì các bạn hãy chia sẻ nhé
Rate this article: 4.9/5 (27 ratings)
You didn't rate yet
Le Minh Thien Toan

Tác giả:Yuto Yasunaga

Xin chào các bạn. Mình là kỹ sư IT đang làm việc ở Nhật Bản. Mình tạo blog này để chia sẻ về cuộc sống và những kinh nghiệm trong quá trình học tập và làm việc.
Hy vọng bài viết này sẽ có ích cho bạn.