さくらVPS(Ubuntu)でDokku x Rails5.2構築

Herokuは便利なうえに安い(使い方次第で)のですが、個人で何個もサイトを立ち上げるとさすがにコストが高くなってきてしまいます。
そこで、HerokuクローンのDokkuを活用して、Herokuのような使いやすさを維持しつつ、何個もサイトを立ち上げた場合のサーバー運営費をおさえるために、さくらVPSでDokku x Railsで構築しました。
本ページに記載しているコマンドをそのまま打って構築したため、基本的にコピペでいけるはずです。

はじめに

用語

以下で設定しました。適宜読み替えてください。

  • データベース名:gnote_db
  • アプリ名:gnote
  • ドメイン名:ujull.com
  • アプリのURL:gnote.ujull.com
  • local$:ローカルのターミナル(Mac)
  • ubuntu$:さくらVPSサーバーのターミナル

環境

  • さくらVPS 1Gプラン
  • Ubuntu 18.04
  • Rails 5.2.3
  • Dokku v0.15.5 (Nginx, Puma, PostgreSQL)
  • お名前.com(DNS設定)

サーバー/DNS準備

サーバー設定

  1. さくらVPS申し込み
  2. Ubuntu16.04(さくらVPSのスタートアップスクリプトは利用しない)でOSインストール
  3. SSH設定
  4. Ubuntuをアップグレード → 18.04
    【参考】Ubuntuのアップグレードのやり方

DNS設定

お名前.comのAレコードを2件、さくらVPSサーバーのIPアドレスを設定します。
(お名前.comからさくらVPSのネームサーバーを参照させる方法もありますが、今回はお名前.com側だけの設定にしました)

  • [ドメイン名] [TTL] IN A [さくらVPSサーバーIP]
  • [*.ドメイン名] [TTL] IN A [さくらVPSサーバーIP]

実際の設定例は以下参照。

【参考】Dokku - DNS Configuration

ポート設定

httpとSSLのポートが空いていないため、以下の手順通りにポート設定します。

さくらVPS x Ubuntu のiptables設定(Dokkuインストール直後) - myMemoBlog by 256hax

Dokkuインストール

基本は Getting Started with Dokku に沿って対応するだけ。

以下コマンドを実行。なお、以下はDokku v0.15.5になっているため、念のため 最新バージョン をご確認ください。

local$ wget https://raw.githubusercontent.com/dokku/dokku/v0.15.5/bootstrap.sh;
local$ sudo DOKKU_TAG=v0.15.5 bash bootstrap.sh

DokkuにSSH登録

ブラウザにアクセスしてDokkuの設定を完了させます。

  • Hostname: ujull.comを入力する
  • Use virtual host naming dokku: チェック入れる

「Public SSH Keys」欄には、さくらVPSサーバーに登録済みの秘密鍵(/home/ubuntu/.ssh/authorized_keys)が表示されるため、通常は「Finish Setup」ボタンを押すだけで完了します。

完了するとdokkuというユーザーが自動的に作成されます。

【補足】DokkuのSSHについて

  • Dokkuへのデプロイ(プッシュ)はSSHベースで実行されるため、ローカル(Mac)とDokku側で認証できる公開鍵・秘密鍵が必須となります。逆にこれがないと認証できないためプッシュできません。
  • Macにはssh-agentに秘密鍵が登録されている必要があります。【参考】Qiita - ssh-agentの使い方
  • Macでssh-keygenを実行してから、さくらVPSサーバー(/home/ubuntu/.ssh/authorized_keys)に公開鍵を登録している場合は、Mac側に秘密鍵がssh-agentにすでに登録されているはずです。$ ssh-agent -l で確認可能。
  • Dokkuの設定画面で本当にさくらVPSサーバーの公開鍵が登録されているか確認したい場合は、「/home/ubuntu/.ssh/authorized_keys」のデータと照らし合わせることで確認可能。

【補足】Dokku公式サイトのデプロイ・SSHの説明

公式サイトで直接説明を確認したい場合は以下を参照。

Deploying to Dokku - Deploy the app

Note: Your private key should be registered with ssh-agent in local development.
If you get a permission denied error when pushing you can register your private key by running ssh-add -k ~/<your private key>.

Dokku設定

基本は Deploying to Dokku に沿って進めるだけですが、一部Rails 5.x用に対応が必要な箇所あり。

アプリの準備 or git clone

ローカル側でデプロイするアプリ(要PostgreSQL済み)を用意するか、試したい場合は以下のサンプルRailsをgit cloneします。

local$ git clone git@github.com:256hax/sample-rails.git

Dokku公式で説明しているHerokuサンプルだとバージョンの関係でうまく動かなかったため、上記はこちらでサンプルのRailsプログラム(PostgreSQL化済み)として準備したものになります。

Dokkuにアプリ追加

サーバーにログインしてDokkuにアプリを追加。

ubuntu$ dokku apps:create gnote

-----> Creating gnote... done

アプリがちゃんと作成されたか確認。

ubuntu$ dokku apps:list

=====> My Apps
gnote

DBサービス追加

DokkuのプラグインでPostgreSQLを追加。

ubuntu$ sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git

-----> Priming bash-completion cache

PostgreSQLにDBを作成。

ubuntu$ dokku postgres:create gnote_db

=====> Postgres container created: gnote_db
=====> Container Information

アプリとDBを紐付ける。

ubuntu$ dokku postgres:link gnote_db gnote

-----> Restarting app gnote
 !     App gnote has not been deployed

DokkuにRAILS_MASTER_KEY登録

RailsのCredentials(RAILS_MASTER_KEY)を使っている場合は、 config/master.key の値をDokkuに登録。(以下のKEYはダミー)

ubuntu$ dokku config:set gnote RAILS_ENV=production RAILS_MASTER_KEY=B6ZSTIWcxFvoJvlqnxlFjXEDOuVnZVBhw

-----> Restarting app gnote

デプロイ

Dokkuにデプロイ

ローカル側で以下のコマンドを実行。

local$ cd gnote
local$ git remote add dokku dokku@ujull.com:gnote
local$ git push dokku master

=====> Application deployed:
       http://gnote.ujull.com

To ujull.com:gnote
 * [new branch]      master -> master

デプロイ失敗の場合

もし、以下のエラーが表示される場合はデプロイ失敗です。ターミナルログを上までスクロールして、どこでコケたのか探り、エラーメッセージで検索してみてください。
下記エラーメッセージは「失敗した(リジェクトされた)」というだけしか書かれていないため、具体的になぜ失敗したのかはログから探る必要があります(プッシュしたときの実行ログに失敗原因が載っています)。

 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'dokku@ujull.com:gnote'

たとえば、以下の例はCoffeeScriptをGemfileから削除したあとにbundle installを実行していなかったため、[remote rejected]になっていました。

$ git push dokku master
Enumerating objects: 31, done.
Counting objects: 100% (31/31), done.
Delta compression using up to 8 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (20/20), 2.01 KiB | 2.01 MiB/s, done.
Total 20 (delta 13), reused 0 (delta 0)
-----> Cleaning up...
-----> Building gnote from herokuish...
-----> Adding BUILD_ENV to build environment...
-----> Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used.
       Detected buildpacks: ruby nodejs
-----> Ruby app detected
-----> Compiling Ruby/Rails
-----> Using Ruby version: ruby-2.4.1
-----> Installing dependencies using bundler 2.0.1
       Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
       You are trying to install in deployment mode after changing
       your Gemfile. Run `bundle install` elsewhere and add the
       updated Gemfile.lock to version control.

       You have deleted from the Gemfile:
       * coffee-rails (~> 4.2)
       Bundler Output: You are trying to install in deployment mode after changing
       your Gemfile. Run `bundle install` elsewhere and add the
       updated Gemfile.lock to version control.

       You have deleted from the Gemfile:
       * coffee-rails (~> 4.2)

       !
       !     Failed to install gems via Bundler.
       !
To ujull.com:gnote
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'dokku@ujull.com:gnote'

Railsマイグレーション

このままだとプログラムがデプロイされただけなので、サーバー側でマイグレーションします。

ubuntu$ dokku run gnote rails db:migrate

httpで動作確認

以下にアクセスすると動作していることがわかります。

http://gnote.ujull.com/

SSL化

無料のSSL証明書「Let's Encrypt」プラグインを利用します。

プラグインの公式サイト BETA: Automatic Let's Encrypt TLS Certificate installation for dokku を参考に設定していきます。

インストール

プラグインをインストール。

ubuntu$ sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

-----> Plugin letsencrypt enabled
Adding user dokku to group adm
[ ok ] Starting nginx (via systemctl): nginx.service.
-----> Priming bash-completion cache

メールアドレス設定

Let's Encryptに登録するメールアドレスを設定。証明書の期限切れ前にメール通知してくれたりするため、非常に重要。
通常は1ドメインごとにメールアドレスを登録しますが、globalオプションをつけると全体共通設定ができます。

ubuntu$ dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=your@email.tld

-----> Setting config vars
       DOKKU_LETSENCRYPT_EMAIL:  your@email.tld

Let's Encrypt適用

ubuntu$ dokku letsencrypt gnote

=====> Let's Encrypt gnote
-----> Updating letsencrypt docker image...
latest: Pulling from dokkupaas/letsencrypt-simp_le
2fdfe1cd78c2: Pull complete
14c299a8553e: Pull complete
9c300f8e499f: Pull complete
Digest: sha256:XXX
Status: Downloaded newer image for dokkupaas/letsencrypt-simp_le:latest
       done updating
-----> Enabling ACME proxy for gnote...
-----> Getting letsencrypt certificate for gnote...
        - Domain 'gnote.ujull.com'
darkhttpd/1.12, copyright (c) 2003-2016 Emil Mikulic.
listening on: http://0.0.0.0:80/
2019-05-05 06:20:06,241:INFO:__main__:1211: Generating new account key
2019-05-05 06:20:09,190:INFO:__main__:1305: gnote.ujull.com was successfully self-verified
2019-05-05 06:20:09,444:INFO:__main__:1313: Generating new certificate private key
2019-05-05 06:20:13,494:INFO:__main__:391: Saving account_key.json
2019-05-05 06:20:13,496:INFO:__main__:391: Saving fullchain.pem
2019-05-05 06:20:13,496:INFO:__main__:391: Saving chain.pem
2019-05-05 06:20:13,497:INFO:__main__:391: Saving cert.pem
2019-05-05 06:20:13,498:INFO:__main__:391: Saving key.pem
-----> Certificate retrieved successfully.
-----> Installing let's encrypt certificates
-----> Configuring gnote.ujull.com...(using built-in template)
-----> Creating https nginx.conf
-----> Running nginx-pre-reload
       Reloading nginx
-----> Configuring gnote.ujull.com...(using built-in template)
-----> Creating https nginx.conf
-----> Running nginx-pre-reload
       Reloading nginx
-----> Disabling ACME proxy for gnote...
       done

これで、 https://gnote.ujull.com/ にアクセスができて、さらに http://gnote.ujull.com/ にアクセスするとhttpsにリダイレクトしてくれるようになります。

証明書の自動更新設定

このままだと有効期限が過ぎると証明書が失効してしまうため、有効期限が近くなったら自動的に再発行されるようにします。
プラグイン公式サイトに載っている以下のコマンドを打つと、dokkuのcrontabに追加されます。

ubuntu$ dokku letsencrypt:cron-job --add

no crontab for dokku
-----> Added cron job to dokku's crontab.

【補足】crontab側の状態

コマンド実行後のdokkuのcrontabは以下のような状態になります。

ubuntu$ sudo cat /var/spool/cron/crontabs/dokku

# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (- installed on Sun May  5 21:52:14 2019)
# (Cron version -- $Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp $)
@daily /var/lib/dokku/plugins/available/letsencrypt/cron-job
buntu$ sudo cat /var/lib/dokku/plugins/available/letsencrypt/cron-job

#!/usr/bin/env bash

PATH=$PATH:/usr/local/bin
dokku letsencrypt:auto-renew &>> /var/log/dokku/letsencrypt.log

Nginx設定

Nginxのデフォルトの仕様により、適当なサブドメインを打っても、すべて同じサイト(最初にHITしたサイトが呼び出される)が表示されてしまいます。
たとえば、 https://test.ujull.com/ も https://ujull.com もすべて同じ表示になります。サブドメインとして切ったURL以外は表示させたくないため、Nginx側で設定をしていきます。

SSL証明書の必要性

期待する動作としては、前項のようにhttpsで適当なURLを打たれた場合に、410 Goneエラーを表示させたいです。そのときに、SSL証明書を用意しておかないと、Nginx側でうまくリダイレクト(410 Gone)することができません。
SSL証明書を用意せずに、特定URLにリダイレクトさせる(たとえば適当URLを打たれたら、常に https://gnote.ujull.com/ にリダイレクト)といった設定も行いましたがうまくいかず、前提としてSSL証明書が必要になるようです。

【参考】

SSL証明書の用意

趣味の個人サイトになるため、今回は自己署名のSSL証明書を用意します。
まずは証明書一式を格納するためのディレクトリを作成して移動。

ubuntu$ sudo mkdir /etc/nginx/ssl
ubuntu$ cd /etc/nginx/ssl

以降は権限の関係で、「sudo sh -c "[コマンド]"」と打つ必要があります。

秘密鍵を作成。

ubuntu$ sudo sh -c "openssl genrsa 2048 > cert.key"

CSRを作成。エラー対応用のSSL証明書となるため、実行すると聞かれる質問はすべてEnter連打でかまいません。(もし、仕事やお客様案件で世に出す場合は、認証された第三者が発行する正式な証明書の取得をされるのがよいと思います。いわゆる有料のSSL証明書ですね。)

ubuntu$ sudo sh -c "openssl req -new -key cert.key > cert.csr"

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

SSL証明書を作成。有効期限は100年。

ubuntu$ sudo sh -c "openssl x509 -days 36500 -req -signkey cert.key < cert.csr > cert.crt"

Signature ok
subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
Getting Private key

Nginxデフォルト設定ファイルの追加

デフォルト設定を行うため、以下を実行して 00-default-vhost.conf - GitHub Gist 256hax の内容をコピペ。

ubuntu$ sudo vi /etc/nginx/conf.d/00-default-vhost.conf

ファイルの反映

Nginxの設定ファイルが正しいか構文チェック。

ubuntu$ sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

設定ファイルを読み込み直す。

ubuntu$ sudo service nginx reload

[ ok ] Restarting nginx (via systemctl): nginx.service.

以上で完了です。
以降は細かいセキュリティ対策やバックアップ対応などを適宜実施していく形になるかと思います。

検証時によく使うコマンド

検証したり、サンプル実装するときに以下コマンドが役立つかと思います。

  • Application Management
    $ dokku apps:list <- 作成したアプリの一覧
    $ dokku apps:destroy gnote <- アプリgnoteを削除
    $ dokku apps:create gnote <- アプリgnoteを作成

  • Domain Configuration
    $ dokku domains:report <- アプリとドメインの紐付け一覧
    $ dokku domains:remove gnote gonte.ujull.com <- ドメイン削除。間違えてgonte.ujull.comと打ってしまったので削除した。
    $ dokku domains:add gnote gnote.ujull.com <- 正しいドメインに紐付け

  • dokku-letsencrypt
    $ dokku letsencrypt:revoke gnote <- SSL証明書の取り消し。間違ってgonte.ujull.comで申請してしまったため。
    $ dokku letsencrypt gnote <- domainsコマンドで正しいドメインに紐付けたあとにSSL証明書を再度発行

参考