Phusion PassengerでMastodonを運用する

[Mastodon](https://github.com/tootsuite/mastodon)を「お一人様インスタンス[^singleuser]」で運用して気になることの一つに、「もっと消費メモリを圧縮できないかな……」ということがあると思います。
そこで[Phusion Passenger](https://www.phusionpassenger.com/)[^raptor]。

Mastodonを立ち上げる際の選択肢として言及はされているものの、その方法自体は見掛けない気がするこのPassengerに載せ替えてみたところ、どうやら良い感じになりました。

以下は私が実際に運用している「お一人様インスタンス」で、負荷に応じてPassengerが適当にWorkerを増減させてくれている様子(が見える消費メモリの推移)です。

![Mastodon with Passenger](https://qiita-image-store.s3.amazonaws.com/0/41695/3749d25c-dd07-70f1-c450-d234d07f08bc.png)

メモリ1GBの手軽なVPSでも心の余裕がうまれるグラフです:-)

この構成では結局Mastodonだけでサーバを占有している状態ですが、他のアプリケーションなどと同居させる場合に威力を発揮しそうです。

この記事では、[Production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md)に従った環境、すなわちDockerを利用していない、`mastodon-web`及び`mastodon-streaming`をPassenger(+nginx)に置き換える方法を扱います。
Mastodonの立ち上げ方そのものについては、先達が沢山いらっしゃるのでそちらが参考になると思います。

## 前提条件
* [Production guide(a3097c9)](https://github.com/tootsuite/documentation/blob/a3097c94e51920d8f3cf1ac76b72eec83c287b1b/Running-Mastodon/Production-guide.md)に準拠する形でCentOS7.3上に構築[^spec]、利用可能な状態の[Mastodon(v1.3.3)](https://github.com/tootsuite/mastodon/releases/tag/v1.3.3)を出発点にしています
* 以降の手順のコマンドは、全て`mastodon`ユーザとして実行します
  * 設定ファイルの編集コマンド等は省略しています

### 既にPassengerの準備ができている場合

以下の項目がお役に立つかも知れません

* [Passengerで動作させるためのアプリケーション準備](http://qiita.com/h-izumi/items/9dac6cbbf5e2586f2155#passenger%E3%81%A7%E5%8B%95%E4%BD%9C%E3%81%95%E3%81%9B%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E6%BA%96%E5%82%99)
* [Passenger設定](http://qiita.com/h-izumi/items/9dac6cbbf5e2586f2155#passenger%E8%A8%AD%E5%AE%9A)

## Passengerインストール
[Installing Passenger + Nginx on Red Hat 7 / CentOS 7 (with RPM) - Passenger Library](https://www.phusionpassenger.com/library/install/nginx/install/oss/el7/)といった手順がありますが、この方法では異なるバージョンのRubyが不必要にインストールされてしまったり、ChromeでHTTP/2が有効にならなかったりします。
Production guideに従うとrbenvを持ったmastodonユーザーがいるはずですので、今回はこれを利用してGemインストール+ビルド、という手段をとることにします。

### gemインストール

gemを利用してpassengerをインストールします。

```shell-session
cd ~/live
gem install passenger --no-ri --no-rdoc
```

### ソースコード準備
ビルドに利用するnginx、opensslのソースコードを用意します。

* nginxのバージョンは、Passenger5.1.3から1.10.3が推奨されていますので、それ以降が良いと思います
* HTTP/2(ALPN)に対応するには、opensslのバージョンは1.0.2以上が必要です

```shell-session
cd /usr/local/src
curl -L -O https://nginx.org/download/nginx-1.12.0.tar.gz
sudo tar xofz nginx-1.12.0.tar.gz
curl -L -O https://www.openssl.org/source/openssl-1.1.0e.tar.gz
sudo tar xofz openssl-1.1.0e.tar.gz
```

### ビルド用パッケージ準備
`libcurl-devel`をインストールします。
他のディストリビューションの場合、読み替えて同等のパッケージを導入してください。

```shell-session
sudo yum install -y libcurl-devel
```

### ビルド
以下のコマンドを実行してビルドします。
この環境では単純にsudoしてgemインストールしたコマンドを利用することはできないため、冗長な形になっています。

また、環境に応じて次の箇所を適宜修正した上で実行してください。

* `versions/2.4.1` rbenvでインストールしたRubyのバージョン
* `passenger-5.1.3` gemでインストールしたPassengerのバージョン
* `--prefix=/opt/nginx` これからビルドするnginxのインストール先
* `--nginx-source-dir` nginxのソースコードの場所
* `--with-openssl` opensslのソースコードの場所

```shell-session
sudo PATH=/home/mastodon/.rbenv/versions/2.4.1/bin:$PATH \
/home/mastodon/.rbenv/versions/2.4.1/bin/ruby \
/home/mastodon/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/passenger-5.1.3/bin/passenger-install-nginx-module --auto \
--prefix=/opt/nginx \
--nginx-source-dir=/usr/local/src/nginx-1.12.0 \
--extra-configure-flags="--with-ipv6 --with-openssl='/usr/local/src/openssl-1.1.0e'"
```

それなりに時間が掛かるので待ちます。
正常終了すると`Nginx with Passenger support was successfully installed.`と表示されます。

![インストール完了](https://qiita-image-store.s3.amazonaws.com/0/41695/8d2eccf3-fb51-cf73-d227-3b1dd32ef497.png)

### 設定ファイルを準備
既存のnginxから設定を適宜移行します。
上記の手順でnginxをインストールした場合、デフォルトで読み込まれる設定ファイルが`conf`ディレクトリ下になっていることに注意してください。

一通り移行したところで、設定に誤りが無いことを確認しておきます。

```shell-session
sudo /opt/nginx/sbin/nginx -t
```

```shell-session:出力結果
nginx: the configuration file /opt/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /opt/nginx/conf/nginx.conf test is successful
```

### systemd対応
[NGINX systemd service file | NGINX](https://www.nginx.com/resources/wiki/start/topics/examples/systemd/)を参考にファイルを作成し、今回インストールしたnginxを`systemctl`で操作できるようにします。

環境に応じて、`PIDFile`の場所及び`/opt/nginx`を適宜修正してください。
また、`PrivateTmp`を`true`とすると、そのままでは「Passenger Instance Registry」を取得することが難しくなり後々困るため、ここでは`false`にしておきます。

```/etc/systemd/system/nginx-passenger.service
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/opt/nginx/sbin/nginx -t
ExecStart=/opt/nginx/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=false

[Install]
WantedBy=multi-user.target
```

## Passengerで動作させるためのアプリケーション準備
### `babel-node` → `node`
Passengerでは`babel-node`相当の機能が無さそうなため、事前にビルドする必要があります。

`package.json`の`scripts`内に`build`を追記します。

```/home/mastodon/live/package.json
{
  ...
  "scripts": {
    ...
    "start": "babel-node ./streaming/index.js --presets es2015,stage-2",
    "build": "babel streaming -d build --presets es2015,stage-2",
    "storybook": "start-storybook -p 9001 -c storybook",
    ...
  }
  ...
}
```

コマンドを実行し、ビルドします。

```shell-session
cd ~/live
npm run build
```

`build/index.js`が出力されます。

### `.env.production`
試した限りでは、どうしてもうまく`.env.production`の内容を反映させられませんでした。
やむを得ませんので、`build/index.js`を修正し`.env.production`の絶対パスを記述して対応します。

```js:/home/mastodon/live/build/index.js修正前
_dotenv2.default.config({
  path: env === 'production' ? '.env.production' : '.env'
});
```

```js:/home/mastodon/live/build/index.js修正後
_dotenv2.default.config({
  path: env === 'production' ? '/home/mastodon/live/.env.production' : '.env'
});
```

## Passenger設定
詳細は[リファレンス](https://www.phusionpassenger.com/library/config/nginx/reference/)を読みましょう。

ここでは、各ディレクティブ毎に最低限必要な設定を記述していきます。
各設定ファイルは環境に応じて適宜読み替えてください。

### `http`ディレクティブ
```nginx:/opt/nginx/conf/nginx.conf
http {
    passenger_root /home/mastodon/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/passenger-5.1.3;
    passenger_ruby /home/mastodon/.rbenv/versions/2.4.1/bin/ruby;
    passenger_log_file /var/log/nginx/passenger.log;
    passenger_max_pool_size 4;
    passenger_max_instances_per_app 3;
}
```

#### `passenger_root` `passenger_ruby`
インストール時に自動的に追記されているはずです。
設定ファイルを他のnginxなどから移行してきた場合、これらを追記します。

#### `passenger_log_file`
Mastodonアプリケーションのログが出力される先になります。
設定しない場合、これらはnginxの`error_log`に出力されるため、設定しておいた方が良いと思います。

#### `passenger_max_pool_size` `passenger_max_instances_per_app`
それぞれ、サーバ全体で許可するアプリケーションWorkerインスタンスの最大数、及び各アプリケーション毎のWorkerインスタンスの最大数です。
ここがチューニングの要ですが、ひとまず適当な値を入れておきます。
(後述の[動作状態の確認](http://qiita.com/h-izumi/items/9dac6cbbf5e2586f2155#%E5%8B%95%E4%BD%9C%E7%8A%B6%E6%85%8B%E3%81%AE%E7%A2%BA%E8%AA%8D)を参照)

`max_pool_size`のデフォルト値は6ですが、メモリが1GBしか無いような場合超えてしまいますので下げておきます。
`max_instances_per_app`は、`max_pool_size`以下で各アプリケーションにうまく割り振られる値を検討します。
今回は、streaming用は1つあれば十分ですので、`max_pool_size - 1`としておきます。

### `server`ディレクティブ - web

#### `passenger_enabled` `passenger_app_group_name`

port443をlistenしているserverディレクティブ内に追記します。
`root /home/mastodon/live/public;`の上あたりが間違いが無いと思います。

```nginx:/opt/nginx/conf.d/Mastodon用.conf
server {
  passenger_enabled on;
  passenger_app_group_name web;
  root /home/mastodon/live/public;
}
```

#### リバースプロキシ設定を削除

pumaに対するリバースプロキシ設定は不要となるため、削除またはコメントアウトします。
`Production guide`の設定例では以下の部分が該当します。

```nginx:削除対象
location / {
  try_files $uri @proxy;
} 
```

```nginx:削除対象
location @proxy {
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto https;
  proxy_set_header Proxy "";
  proxy_pass_header Server;
    
  proxy_pass http://127.0.0.1:3000;
  proxy_buffering off;
  proxy_redirect off;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  tcp_nodelay on;
}
```

### `location`ディレクティブ - streaming

nodeに対するリバースプロキシ設定は不要となるため、削除またはコメントアウトします。
`Production guide`の設定例では以下の部分が該当します。

```nginx:削除対象
location /api/v1/streaming {
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto https;
  proxy_set_header Proxy "";

  proxy_pass http://localhost:4000;
  proxy_buffering off;
  proxy_redirect off;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  tcp_nodelay on;
}
```

新しい設定は以下の通りです。

```nginx:/opt/nginx/conf.d/Mastodon用.conf
location /api/v1/streaming {
  root /home/mastodon/live;
  passenger_enabled on;
  passenger_app_group_name streaming;
  passenger_app_type node;
  passenger_startup_file /home/mastodon/live/build/index.js;
  tcp_nodelay on;
}
```

一通り記述が済みましたら、設定に誤りが無いことを確認しておきます。

```shell-session
sudo /opt/nginx/sbin/nginx -t
```

```shell-session:出力結果
nginx: the configuration file /opt/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /opt/nginx/conf/nginx.conf test is successful
```

## 既存のサービス類を停止
この時点から、元の環境ではアクセスできなくなります。

元々試用していたnginxを停止します。

```shell-session
sudo systemctl stop nginx
sudo systemctl disable nginx
```

`mastodon-web`及び`mastodon-streaming`を停止します。

```shell-session
sudo systemctl stop mastodon-web
sudo systemctl stop mastodon-streaming
sudo systemctl disable mastodon-web
sudo systemctl disable mastodon-streaming
```

## Passenger + nginxを起動
systemd対応で設定した名前を使用して、起動します。
start/restart/reloadなど、nginxの場合と同様に実行できます。

```shell-session
sudo systemctl enable nginx-passenger
sudo systemctl start nginx-passenger
```

ブラウザからMastodonにアクセスします。
エラー無く利用できれば、作業完了です。お疲れさまでした:-)

## 動作状態の確認

`passenger-status`コマンドを利用すると、Passengerの動作状態を確認できます。

```shell-session
cd ~/live
passenger-status
```

```shell-session:出力結果
Version : 5.1.3
Date    : 2017-05-07 07:49:31 +0900
Instance: dqfV6I6e (nginx/1.12.0 Phusion_Passenger/5.1.3)

----------- General information -----------
Max pool size : 4
App groups    : 2
Processes     : 2
Requests in top-level queue : 0

----------- Application groups -----------
streaming:
  App root: /home/mastodon
  Requests in queue: 0
  * PID: 27481   Sessions: 2       Processed: 4       Uptime: 1h 2m 27s
    CPU: 0%      Memory  : 21M     Last used: 30m 26s a

web:
  App root: /home/mastodon/live
  Requests in queue: 0
  * PID: 28733   Sessions: 0       Processed: 3       Uptime: 30m 26s
    CPU: 0%      Memory  : 120M    Last used: 23m 8s ago
```

必要に応じて、Passengerにより`web`のWorkerインスタンスが増減します。

```shell-session:出力結果2
Version : 5.1.3
Date    : 2017-05-07 07:51:07 +0900
Instance: dqfV6I6e (nginx/1.12.0 Phusion_Passenger/5.1.3)

----------- General information -----------
Max pool size : 4
App groups    : 2
Processes     : 4
Requests in top-level queue : 0

----------- Application groups -----------
streaming:
  App root: /home/mastodon
  Requests in queue: 0
  * PID: 27481   Sessions: 2       Processed: 9       Uptime: 1h 4m 3s
    CPU: 0%      Memory  : 21M     Last used: 2s ago

web:
  App root: /home/mastodon/live
  Requests in queue: 0
  * PID: 28733   Sessions: 0       Processed: 10      Uptime: 32m 2s
    CPU: 0%      Memory  : 120M    Last used: 3s ago
  * PID: 29839   Sessions: 0       Processed: 1       Uptime: 6s
    CPU: 2%      Memory  : 46M     Last used: 3s ago
  * PID: 29849   Sessions: 0       Processed: 0       Uptime: 3s
    CPU: 0%      Memory  : 4M      Last used: 3s ago
```

ここでは詳細は割愛しますが、この状態を見ながら環境に合わせて[設定をチューニング](http://qiita.com/h-izumi/items/9dac6cbbf5e2586f2155#passenger_max_pool_size-passenger_max_instances_per_app)することになると思います。

## バージョンアップ
基本は`Production guide`の通りですが、以下の作業が追加となります。

### streaming/index.json
このファイルに修正が入った場合、[ビルド作業及びビルド後ファイルへの修正が必要](http://qiita.com/h-izumi/items/9dac6cbbf5e2586f2155#passenger%E3%81%A7%E5%8B%95%E4%BD%9C%E3%81%95%E3%81%9B%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E6%BA%96%E5%82%99)です。

### アプリケーション再起動
リバースプロキシで運用していたときと同様、アプリケーションはメモリ上で保持されているため再起動するまで更新は反映されません。

次のコマンドで、Passengerが読み込んでいるMastodonを再起動できます。

```shell-session
cd ~/live
passenger-config restart-app /home/mastodon/live
```

## 元に戻したくなった……
元のnginxを削除していない場合、今回インストールしたPassenger+nginxを停止し、再び`systemctl`で設定すれば元に戻るはずです。

Passengerが不要になった場合、以下の手順で削除できます。
ファイルパスは適宜読み替えてください。

```shell-session
sudo rm /etc/systemd/system/nginx-passenger.service
sudo rm -rf /opt/nginx
cd ~/live
gem uninstall passenger
git checkout package.json
rm -rf build
```

[^singleuser]: [Mastodon は自分のドメインでIDを持つことが大事。「リモートフォロー」の価値を最大化するべし。 ｜ 諸多日記](https://isid.ai/diary/2017/04/14/1179/)
[^raptor]: [Raptor](http://www.rubyraptor.org/)とも呼ばれていたもの
[^spec]: 具体的にはConoHa VPS メモリ1GBプランです