CentOS7, Ruby on Rails 5, PumaでProducrion環境構築@VPS + Capistranoでのデプロイ

# はじめに

プライベートでサービスを立ち上げるときには、今まではいつもherokuを使っていました。herokuはすべてがサービス化されていてすごい楽なんですけど、反面、コスパが悪くて、ちゃんとプライベートのサービスやるには向かねーなあって思ってました。

そんなわけで重い腰を上げて今回からは自分でVPSを使って環境構築してみることにしました。自分はサーバ設定まわりがとても苦手で、かつ、まとまった情報もあまりネットになく、けっこう苦労したので、普段qiitaとか書かないのですが、せっかくなのでまとめておきます。

CentOS7 + Ruby on Rails 5 + Pumaで動作。Capistranoでのデプロイができるというところまで。

# おおまかな流れ

* Railsを動作させる@ローカルMac
    * プロジェクト作成
    * MySQLユーザ作成
    * git, github初期設定
* VPS初期設定
    * ユーザ作成
    * firewall設定
    * swap領域作成
* MySQLインストール
* Rubyインストール
    * rbenv
    * ruby
    * bundler
* Nginxインストール
* Capistranoでデプロイ

# Railsを動作させる@ローカルMac

まずはローカルでrailsプロジェクトが動くようにします。最終的にローカルからCapistranoでdeployを行います。RubyやMySQLなどはインストールされているものとします。

## プロジェクト作成

```
$ rails new アプリ名 -d mysql
```

## MySQLユーザの作成

```
$ mysql -u root
> CREATE USER 'DBユーザ名'@'localhost' IDENTIFIED BY 'パスワード';
> GRANT ALL PRIVILEGES ON DB名.* TO 'DBユーザ名'@'localhost';
> FLUSH PRIVILEGES;
> exit;
```

DBユーザ名、パスワード、DB名はあとでdatabase.ymlに書く必要があるのでメモっておく。

## git, githubの設定

gitを初期し、githubへアップします。


```
$ cd アプリ名
$ git init
```

.gitignoreを編集して不要なファイルがgit管理されないようにしましょう。

```
/.bundle
/vendor/bundler/
/log/*.log
/tmp
/uploads/tmp/
/config/settings.local.yml
/config/settings/*.local.yml
/config/environments/*.local.yml
/config/secrets.yml
/config/database.yml

.pryrc
.DS_Store
```

コミットしてプッシュします

```
$ git add .
$ git commit -m "first commit"
$ git remote add origin git@github.com:eiei19/アプリ名とか.git
$ git push -u origin master
```

## database.yml と sercrets.ymlの編集

こんな感じになるはずです。

```databese.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  host: 127.0.0.1

development:
  <<: *default
  database: DB名
  username: DBユーザ名
  password: DBパスワード

test:
  <<: *default
  database: dos_test

production:
  <<: *default
  database: DB名（production）
  username: DBユーザ名（production）
  password: DBパスワード（production）
```

```sercrets.yml
development:
  secret_key_base: xxx

test:
  secret_key_base: xxx

production:
  secret_key_base: xxx
```

Production環境のDBパスワードなどは、あとでVPS側で設定するときに決めます。

## railsを動かす

### DBの作成と起動

```
$ bin/rake db:create
$ bin/rails server
```

http://localhost:3000 にアクセスしてRailsのWelcome画面が表示されればOK。

### 適当にコントローラーとか作る

```app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
  end
end
```

```app/views/posts/index.html.erb
<h1>Hello World</h1>
```

```config/routes.rb
Rails.application.routes.draw do
  get '/posts', to: 'posts#index'
end
```

http://localhost:3000/posts にアクセスしてHello Worldが表示されればOK。commit&pushしておきましょう。

# VPS初期設定

https://vultr.com/

今回はvultrというVPSサービスを使ってみました。よくわかってないですが、コンパネから一発でVPSを立ち上げることができて、snapshotとかもとれて、Tokyoリージョンもあります。ミニマムで$5/monthからと値段もお手頃。

## SSHの設定

サーバをCentOS7で立ち上げます。まずはssh周りを設定します。

```
$ root@IPアドレス

--  ここからVPS --

# adduser ei
# passwd ei
# gpasswd -a ei wheel
# mkdir /home/ei/.ssh
# vi /home/ei/.ssh/authorized_keys
> 公開鍵を貼り付ける
# chmod 700 /home/ei/.ssh/authorized_keys
# chown -R ei:ei /home/ei/.ssh/

```

```
-- ローカルで違うターミナルを開く --

$ ssh ei@IPアドレス #接続できることを確認する

--  ここからVPS --

$ sudo vi /etc/ssh/sshd_config

> 以下を変更
PermitRootLogin no
PasswordAuthentication no

$ sudo systemctl reload sshd
```

これでrootログインとパスワードログインができなくなった。

## Timezone設定

```
$ sudo ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
$ date
```

## Firewall設定

次にiptablesによるFirewallの設定をします。

```
$ sudo yum remove -y firewalld
$ sudo yum install -y iptables-services
$ sudo systemctl start iptables
$ sudo systemctl enable iptables
$ sudo /usr/libexec/iptables/iptables.init save
$ sudo vi /etc/sysconfig/iptables

> こんな感じにする。Web用に８０と433をあける。
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [48:7196]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT

$ sudo systemctl reload iptables
```

## swap領域の確保

初期設定だとswap領域が確保されていないので、メモリを使いすぎると、使いすぎてるプロセスがキルされます。それだと困るのでswap領域を確保しておきます。

```
# dd if=/dev/zero of=/swapfile bs=1M count=1024
# mkswap /swapfile
# swapon /swapfile
# vi /etc/fstab 

> 以下追記

/swapfile swap swap defaults 0 0
```

ここまでできたら自動マウントされるか試すためにサーバを再起動する。topコマンドswap領域が確保されてればOK。

# MySQLインストール

つぎはミドルウェアとかを入れていきます。まずはMySQLから。

## インストール

```
# yum -y remove mariadb-libs
# rm -rf /var/lib/mysql/
# yum -y localinstall http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm
# yum -y install mysql-community-server mysql-devel
# systemctl enable mysqld.service
# systemctl start mysqld.service
# cat /var/log/mysqld.log | grep "A temporary password"

> 表示されるパスワードを使ってインストールする

# mysql_secure_installation

> 新しいパスワードを決める。質問は全部YESでOK.

# vi /etc/my.cnf

> 以下追記
character-set-server = utf8
default_password_lifetime = 0
```

## ユーザ作成

```
# mysql -u root -p

> CREATE USER 'DBユーザ名'@'localhost' IDENTIFIED BY 'パスワード';
> GRANT ALL PRIVILEGES ON DB名.* TO 'DBユーザ名'@'localhost';
> FLUSH PRIVILEGES;
> exit;
```

# Rubyインストール

Rubyをインストールします。今回はrbenvを使います。

```
# yum -y install git gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel bzip2
# cd /usr/local
# git clone git://github.com/sstephenson/rbenv.git rbenv
# git clone git://github.com/sstephenson/ruby-build.git rbenv/plugins/ruby-build

# vi /etc/profile.d/rbenv.sh

> 以下を記述
export RBENV_ROOT="/usr/local/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init --no-rehash -)"

# source /etc/profile.d/rbenv.sh
# rbenv install 2.3.0
# rbenv global 2.3.0
# rbenv rehash
# gem install bundler --no-ri --no-rdoc

最後にnodejsをいれておく。railsで必要。
# yum -y install nodejs
```

# Nginxインストール

```
# vi /etc/yum.repos.d/nginx.repo

> 以下記載
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

# yum -y update
# yum -y install nginx
# systemctl start nginx
# systemctl enable nginx
```

# Capistranoでデプロイ

## デプロイ準備

デプロイ用のディレクトリとユーザを作成します。

```
# mkdir /var/www
# adduser www
# chown -R www:www /var/www
# chmod -R 770 /var/www/
# adduser deploy
# gpasswd -a deploy www
# gpasswd -a nginx www
# mkdir /home/deploy/.ssh
# vi /home/deploy/.ssh/authorized_keys
> デプロイ用ユーザの公開鍵を貼り付ける。最初のユーザとは変えておく。macでは、ssh-keygen -t rsa でキーペアを作れる。
# chown -R deploy:deploy /home/deploy/.ssh
# chmod -R 700 /home/deploy/.ssh
```

```
$ ssh -i ~/.ssh/さっき貼り付けた公開鍵とペアの秘密鍵 deploy@IPアドレス
> ログインできるか確認する
```

## Capistrano設定

あともう一息です。ローカルのRailsプロジェクトに戻ります。gemファイルに追記。

```
group :development do
  gem 'capistrano',         require: false
  gem 'capistrano-rails',   require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano3-puma',   require: false
  gem 'capistrano-rbenv',   require: false
end
```

```
$ bundle install --path vendor/bundle
$ bundle exec cap install
```

```Capfile.
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rails'
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/puma'
```

```deploy.rb
server 'IPアドレス', port: 22, roles: [:app, :web, :db], primary: true
set :repo_url,        'git@github.com:eiei19/アプリ名とか.git'
set :application,     'アプリ名'
set :user,            'deploy'
set :ssh_options,     {
  forward_agent: true,
  user: fetch(:user),
  keys: %w(~/.ssh/deployユーザの秘密鍵)
}
set :puma_threads,    [4, 16]
set :puma_workers,    0
set :pty,             true
set :use_sudo,        false
set :stage,           :production
set :deploy_via,      :remote_cache
set :deploy_to,       "/var/www/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.access.log"
set :puma_error_log,  "#{release_path}/log/puma.error.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true
set :rbenv_type, :system
set :rbenv_ruby, '2.3.0'
set :linked_dirs, fetch(:linked_dirs, []).push(
  'log',
  'tmp/pids',
  'tmp/cache',
  'tmp/sockets',
  'vendor/bundle',
  'public/system',
  'public/uploads'
)
set :linked_files, fetch(:linked_files, []).push(
  'config/database.yml',
  'config/secrets.yml'
)

namespace :puma do
  desc 'Create Directories for Puma Pids and Socket'
  task :make_dirs do
    on roles(:app) do
      execute "mkdir #{shared_path}/tmp/sockets -p"
      execute "mkdir #{shared_path}/tmp/pids -p"
    end
  end
  before :start, :make_dirs
end

namespace :deploy do
  desc "Make sure local git is in sync with remote."
  task :check_revision do
    on roles(:app) do
      unless `git rev-parse HEAD` == `git rev-parse origin/master`
        puts "WARNING: HEAD is not the same as origin/master"
        puts "Run `git push` to sync changes."
        exit
      end
    end
  end

  desc 'Initial Deploy'
  task :initial do
    on roles(:app) do
      before 'deploy:restart', 'puma:start'
      invoke 'deploy'
    end
  end

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke 'puma:restart'
    end
  end

  before :starting,     :check_revision
  after  :finishing,    :compile_assets
  after  :finishing,    :cleanup
end
```

```
$ cap production deploy:initial
```

database.ymｌがないとか言われるはずなので、サーバにdeployユーザでログインして配置します。ホントはここも自動化するべき。

## nginx設定

最後にnginxを設定して終了。
/etc/nginx/conf.d/xxxx.conf とか置く。

```
upstream アプリ名 {
    server unix:/var/www/アプリ名/shared/tmp/sockets/アプリ名-puma.sock fail_timeout=0;
}

server {
    listen 80;
    server_name サーバのIPもしくはドメイン;
    root /var/www/アプリ名/current/public;

    location ~ ^/assets/ {
      root /var/www/アプリ名/current/public;
    }

    try_files $uri/index.html $uri @アプリ名;

    location / {
        proxy_pass http://アプリ名;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
}
```