さくらサーバで環境構築ゼロから。Railsアプリをデプロイするまで

# 準備
http://vps.sakura.ad.jp/
上にアクセスし、VPSに申し込みを済ませる。
今回メモリは2GB。ストレージの種類　SSD（50GB）。サーバー台数1台。
支払い情報を入力して、申し込み完了！

# １、カスタムOSのインストール
[参考サイト](https://help.sakura.ad.jp/hc/ja/articles/206055622-%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0OS%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%82%AC%E3%82%A4%E3%83%89-Debian-7-8)
## OSを選択
今回はubuntu20を選択。

## ~~VNCコンソールを開き、インストール作業を開始。~~
~~Installを選択し、日本語を選択。~~
~~ホスト名、ドメイン名、rootパスワード、ユーザーアカウントを入力。~~
↓
~~ディスクパーティショニング　”ガイドーディスク全体を使う”→”全ての。。（初心者。。）”を選択→”パーティショニングの終了とディスクへの変更の書き込み”を選択~~
↓
~~完了したらxで閉じ、さくらのコンソールの緑の起動ボタンを押してサーバーを起動。~~


# ２、ターミナルでsshログインし、鍵認証でログインする方法
[参考サイト](http://knowledge.sakura.ad.jp/beginner/2751/)
[ssh鍵認証参考サイト](http://www.tooyama.org/ssh-key.html)
※windowsの場合はSSHクライアントソフトをインストールする必要がある。

## ターミナルを開き以下のコマンドを入力

```
ssh 上で設定したuser名@IPアドレス
```
yesと上で設定したパスワードを入力し、sshログイン完了。

## sshログイン先で以下のコマンドを実行
※手元で自分の公開鍵をコピーしておく。ない人は作成。

```
$ mkdir .ssh
$ chmod 700 .ssh
$ vi .ssh/authorized_keys
自分の公開鍵をcopy&paste
$ chmod 600 .ssh/authorized_keys

```
sshログインの設定が完了したので、一旦ログアウトし、もう一度ログインしてみて、パスワード入力も求められずにログインできたらOK。

## sudoをインストール
sshログイン先で

```
$ su -
# apt-get install sudo
# usermod -G sudo [ユーザー名]
# su [ユーザー名]
```

## パスワード認証によるsshアクセスを禁止する方法
[参考サイト](http://www.atmarkit.co.jp/flinux/rensai/linuxtips/430dnypsswdacces.html)

```
$ sudo su
# vi /etc/ssh/sshd_config
PasswordAuthentication no
# service sshd restart
```

## ~~不要なポート番号を閉じる~~

さくらの[パケットフィルター](https://manual.sakura.ad.jp/vps/network/packetfilter.html)で簡単に設定可能になったようです。

~~[こちら](http://qiita.com/osktak/items/778287711f7ee0ccec3b)を参考に進める。~~

~~上記の通り実行したら以下のように変更。~~

```/etc/iptables/rules.v4
-A INPUT -p tcp -m state --state NEW --dport 1234 -j ACCEPT
↓
-A INPUT -p tcp -m state --state NEW --dport 22 -j ACCEPT
```
```/etc/iptables/rules.v6
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
```



# ３、必要なものを諸々インストール
## 下記のコマンドをsshログイン先で実行。（必要、不要はご判断ください）

```
$ sudo apt-get update
$ sudo apt-get install vim git gcc g++ make nginx nodejs libssl-dev libreadline-dev zlib1g-dev
```

## nginxのステータスを確認

```
$ sudo service nginx status
Active: active (running) since Sun 2017-08-27 16:18:48 JST; 35s ago
```
activeになっていればOK。
IPアドレスにアクセスしてみると、Welcome to nginx!の文字が。
(※[さくらVPS パケットフィルタ](https://manual.sakura.ad.jp/vps/network/packetfilter.html)に注意)

# ４、rubyをrbenvでインストール
## rbenvをインストール

```
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ cd ~/.rbenv && src/configure && make -C src
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
一度ログインし直す。
$ ~/.rbenv/bin/rbenv init
```

## rubyをインストール

```
$ rbenv install -l
インストールできるバージョンの一覧が表示される
$ rbenv install 2.4.1
$ rbenv global 2.4.1
$ rbenv exec gem install bundler
```

## Mysql8.0をインストール
[こちら](https://self-development.info/ubuntu-20-04%E3%81%AB%E6%9C%80%E6%96%B0%E7%89%88%E3%81%AEmysql-8-0%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/)を参考にUbuntu20にMysql8.0をインストール
### Resolve dpkg status database is locked by another processエラー
インストール中に`Resolve dpkg status database is locked by another process`エラーが出る場合は[こちら](https://linuxhint.com/resolve-locked-dpkg-status-database/)を参考に解決
### bundle install中にエラー
bundle install中にパッケージ不足でエラーが出る場合は[こちら](https://qiita.com/GandT/items/bee105afe228a564a4da)を参考に解決


# Capistranoの導入
## 手元での設定
### Gemfile

```ruby:Gemfile
group :development do
  gem 'capistrano', '~> 3.9'
  gem 'capistrano-rails'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano3-puma'
end

gem 'puma'
```

```
$ bundle install
```

```
$ bundle exec cap install
```
### Capfile

```ruby:Capfile
require "capistrano/setup"
require "capistrano/deploy"

require 'capistrano/rails'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/puma'
require 'whenever/capistrano' # wheneverを利用している場合

install_plugin Capistrano::Puma  # Default puma tasks
install_plugin Capistrano::Puma::Systemd

require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
```

### config/deploy.rb


```ruby:config/deploy.rb
# require 'whenever/capistrano'

lock "3.9.0"

set :application, "my_app_name" #自分のapp_nameに変更
set :repo_url, "git@example.com:me/my_repo.git" #自分のリポジトリURLに変更
set :deploy_to, '/var/www/my_app_name' #自分のapp_nameに変更
set :keep_releases, 3

set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, '2.4.1'

set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default value

set :linked_dirs, %w{log tmp/backup tmp/pids tmp/cache tmp/sockets vendor/bundle}
set :linked_files, %w{config/credentials/production.key}

set :bundle_jobs, 4

namespace :deploy do
  task :restart do
    on roles(:web, :app, :db) do |host|
      execute "source /etc/environment"
    end
    invoke 'puma:restart'
  end
  after :finishing, 'deploy:cleanup'
end

```
### config/deploy/production.rb

```ruby:config/deploy/production.rb
set :branch, 'master'

role :app, %w{user_name@IPアドレス}
role :web, %w{user_name@IPアドレス}
role :db,  %w{user_name@IPアドレス}

server 'IPアドレス', user: 'user_name', roles: %w{web app db}

```

### config/puma/production.rbを新規作成

```ruby:config/puma/production.rb
rails_root = "/var/www/my_app_name/current" #自分app_nameに変更
shared_path = "/var/www/my_app_name/shared" #自分app_nameに変更

worker_processes 4
working_directory rails_root

#listen "#{rails_root}/tmp/puma.sock"
#pid "#{rails_root}/tmp/puma.pid"
listen File.expand_path('tmp/sockets/puma.sock', shared_path)
pid File.expand_path('tmp/pids/puma.pid', shared_path)

stderr_path "#{rails_root}/log/puma_error.log"
stdout_path "#{rails_root}/log/puma.log"

preload_app true

before_fork do |server, worker|
 ENV['BUNDLE_GEMFILE'] = File.expand_path('Gemfile', rails_root)
 old_pid = "#{server.config[:pid]}.oldbin"
 if File.exists?(old_pid) && server.pid != old_pid
   begin
     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
     Process.kill(sig, File.read(old_pid).to_i)
   rescue Errno::ENOENT, Errno::ESRCH
     # someone else did our job for us
   end
 end
end

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

```ruby:database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: 5
  username: root
  password:
  socket: <%= ["/run/mysqld/mysqld5.6.sock",
               "/tmp/mysqld.sock",
               "/var/run/mysqld/mysqld.sock",
               "/var/lib/mysql/mysql.sock",
               "/opt/local/var/run/mysql5/mysqld.sock"].detect {|s| File.exist?(s) } %>

production:
  adapter: mysql2
  encoding: utf8mb4
  pool: 5
  database: my_app_name_production
  username: <%= ENV["DB_USER"] %>
  password: <%= ENV["DB_PASSWORD"] %>
```

### ~~config/initializers/ar_innodb_row_format.rbの作成~~

~~以下のファイルを作成。[参考サイト](http://qiita.com/xend/items/4d5c3333cbae53888f37)~~

```ruby:config/initializers/ar_innodb_row_format.rb
ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters
    module CreateTableWithInnodbRowFormat
      def create_table(table_name, options = {})
        table_options = options.merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
        super(table_name, table_options) do |td|
          yield td if block_given?
        end
      end
    end

    class AbstractMysqlAdapter
      prepend CreateTableWithInnodbRowFormat
    end
  end
end
```

~~上記作成後、migrate。~~

```
$ bundle exec rake db:migrate
```

deployする

```
$ bundle exec cap production deploy
```

## puma.serviceの設置
新規ファイルとして作成。以下内容を記述。[参考](https://qiita.com/AQeNku/items/81ddae388d8615125a24)
`/etc/systemd/system/puma.service`
```
[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/var/www/my_project_name/current
Environment=RAILS_ENV=production
Environment=USING_WEB_SERVER=true
ExecStart=/bin/bash -lc 'bundle exec puma -C config/puma/production.rb'
Restart=always

[Install]
WantedBy=sockets.target
```

ここで色々エラーを起こすと思いますが、エラー文に従って修正していけば完了。（投げやり


以下にエラーが出て追加で行ったことを記しておきます。
sshログイン後
### yarnをinstall
[こちら](https://riyoo.biz/articles/yarn-install)を参考にubuntu20にyarnをinstall

### 権限エラー
```
$ chown -R [ユーザー名]:[ユーザー名] /var/www/
```

### nodeバージョンエラー
[こちら](https://blog.mintsu-dev.com/posts/2020-07-22-install-nodenv-linux/#linuxubuntu%E3%81%ABnodenv%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B)を参考にnodenvをinstallし、nodeをバージョンアップ
バージョンアップ後、deploy時にnodeのバージョンが変わらない場合は以下を試す。
```
$ yarn config set ignore-engines true
```

### master.key配置
[こちら](https://qiita.com/katsu105/items/88675da5119762d73d92)を参考にmaster.keyを設定。

### mysqlにログインし、Database作成
[こちら](http://www.goofoo.jp/2011/11/1457)を参考にmysqlにログインできるようにしておく。

### Rails7 Tailwindを利用している場合のエラー
以下のようなエラーが出た場合
```
TypeError: Object.fromEntries is not a function
```

```
$ sudo apt-get install npm
$ sudo npm i -g n
$ cd /var/www/my_project_name
$ n latest
```
[参考](https://stackoverflow.com/questions/68613313/how-do-i-fix-tailwindcss-cli-from-throwing-typeerror-object-fromentries-is-not)


```
$ mysql -u root
mysql> CREATE DATABASE my_app_name_production;
```

 ~~MariaDBに以下の設定を記載~~

 ```
$vi /etc/mysql/mariadb.conf.d/50-server.cnf
[mysqld] の中に以下を記載

innodb_file_per_table = 1
innodb_file_format = Barracuda
innodb_large_prefix = 1
innodb_default_row_format = DYNAMIC # >= 5.7.9
```
## Letsencryptをインストール
デプロ完了後、SSL化対応。
AレコードでDNS設定を済ませておく。
Ubuntu20用
1. ssh先でcertbotをinstall
```
$ sudo apt install certbot python3-certbot-nginx
```
2. localでnginxファイルを管理可能にするため、nginxフォルダを作成。
```
$ mkdir config/nginx
$ touch config/nginx/nginx.conf
$ touch config/nginx/my_project_name.conf
```

```nginx.conf
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

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

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

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

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
#
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }
#}
```

```my_project_name.conf
upstream puma {
    server unix:/var/www/my_project_name/current/tmp/sockets/puma.sock;
}

server {
    listen 80;
    root   /var/www/my_project_name/current/public;
    server_name www.my_project_domain_name;
    client_max_body_size 50M;

    # For letsencrypt
    location /.well-known/ {
        alias /var/www/my_project_name/.well-known/;
    }

    location / {
        return 301 https://my_project_domain_name$request_uri;
    }
}

server {
    listen 80;
    root   /var/www/my_project_name/current/public;
    server_name my_project_domain_name;
    client_max_body_size 50M;

    # For letsencrypt
    location /.well-known/ {
        alias /var/www/my_project_name/.well-known/;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443;
    server_name my_project_domain_name www.my_project_domain_name;
    root   /var/www/my_project_name/current/public;
    client_max_body_size 50M;

    if ($http_host = www.my_project_domain_name) {
        rewrite  (.*)  https://my_project_domain_name$1;
    }

    ssl on;
    ssl_certificate      /etc/letsencrypt/live/my_project_domain_name/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/my_project_domain_name/privkey.pem;
    ssl_session_timeout 5m;

    # Use https always

    add_header Strict-Transport-Security 'max-age=31536000;';

    # # 1. 暗号方式の設定
    # # (デフォルト) ssl_ciphers  HIGH:!aNULL:!MD5;
    # ssl_prefer_server_ciphers  on;
    # ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    # ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

    # # 2. Logjam攻撃対策
    # ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    # # 3. Enable OCSP (Online Certificate Status Protocol) Stapling
    # ssl_stapling on;
    # ssl_stapling_verify on;
    # resolver 8.8.4.4 8.8.8.8 valid=300s;
    # resolver_timeout 10s;

    location ~ ^/sitemap.xml.gz {
      root /var/www/my_project_name/current/public;
    }

    location ^~ /assets/ {
        # to serve pre-gzipped version
        gzip_static on;
        # for CloudFlare
        gzip_proxied any;
        gzip_vary on;
        expires 1y;
        add_header Cache-Control public;
        add_header ETag "";
        add_header Access-Control-Allow-Origin *;
        break;
    }

    location ~* \.(css|ico)$ {
        expires 24h;
        if ($args ~ [0-9]+) {
           expires max;
        }
        #access_log off;
    }

    try_files $uri @app;

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

    location ^~ /blog {
        alias          /var/www/wordpress;
        index index.php index.html index.htm;
        try_files try_files $uri $uri/ /blog/index.php;
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
            fastcgi_index  index.php;
            fastcgi_param SCRIPT_FILENAME $request_filename;
        }
    }

    set $spambot 0;

    # リファラスパムボットそれぞれの判定
    if ($http_referer ~* (4webmasters.org) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (76brighton.co.uk) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (7makemoneyonline.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (7secretsearch.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (acads.net) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (floating-share-buttons.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (forum.topic65617124.darodar.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (free-social-buttons.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (www.event-tracking.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (www.Get-Free-Traffic-Now.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (chinese-amezon.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (e-buyeasy.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (erot.co) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (sa-live.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (get-free-social-traffic.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (free-floating-buttons.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (darodar.com) ) {
      set $spambot 1;
    }
    if ($http_referer ~* (snip.to) ) {
      set $spambot 1;
    }
    if ($spambot = 1) {
      return 403;
      break;
    }
}
```
git pushとデプロイを完了させる。
その後sshログイン先でシンボリックリンクを貼る。
```
$ sudo ln -sf /var/www/my_project_name/current/config/nginx/my_project_name.conf /etc/nginx/conf.d/my_project_name.conf
$ sudo ln -sf /var/www/my_project_name/current/config/nginx/nginx.conf /etc/nginx/nginx.conf
```
その後 `my_project_name.conf`の`ssl on`から`ssl_session_timeout 5m;`までを一度コメントアウトし、nginxをリスタート
```
$ sudo systemctl restart nginx
```

一度以下`--dry-run`コマンドでテストし、問題なければ`--dry-run`オプションを外し、実行。
```
$ sudo certbot certonly --webroot -w /var/www/my_project_name/ -d my_project_domain_name -d www.my_project_domain_name --dry-run
```
成功したら、コメントアウトを元に戻し、nginxをrestartすれば完了。

## puma.sock failed (111: Connection refused系のエラーが出た場合
SSL化対応後にNginxで`502 bad gateway`が出てしまった場合に確認する箇所。
```
$ vim /etc/systemd/system/puma.service
$ sudo systemctl enable /etc/systemd/system/puma.service
# pumaとnginxを再起動
```

# 他参考
[ubuntuでopenssl最新版ダウンロード方法](https://text.baldanders.info/remark/2020/06/installing-openssl-in-ubuntu/)
[ubuntu22.04でnginxインストールする方法](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-22-04#step-5-%E2%80%93-setting-up-server-blocks-(recommended))
[ubuntu22.04でLet's Encryptインストール](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-22-04#step-5-%E2%80%93-setting-up-server-blocks-(recommended))