Mastodon を CentOS にインストールする (Docker未使用)

## Mastodon
**[Mastodon](https://github.com/tootsuite/mastodon)** はイエナ大学出身の24歳のドイツ人 [Eugen Rochko](https://www.linkedin.com/in/gargron/) によって2016年10月に公開された [Rails](http://rubyonrails.org/) アプリケーションで、フロントエンドには [React](https://facebook.github.io/react/) と [Redux](http://redux.js.org/) が使用されている。

Docker を利用せずに CentOS 7 に Mastodon をインストールする手順をまとめた。当該手順は Mastodon 1.1.2 時点の内容のため、最新版では当初と異なる部分があるので注意すること。

## ドキュメント
「[Mastodon 関連情報まとめ](http://qiita.com/bezeklik/items/0b71eade799648431bf1)」を参照。

## 検証環境
* VirtualBox 5.1.18
    * CentOS 7.3.1611 (minimal)
        * Node.js 6.10.2 (nodesource)
            * Yarn 0.23.2 (yarn)
        * PostgreSQL 9.6.3 (pgdg96)
        * Redis 3.2.3 (epel)
        * rbenv 1.1.0 (GitHub)
            * Ruby 2.4.1 (rbenv)
                * Bundler 1.14.6 (gem)
        * nginx 1.12.0 (nginx)
        * ImageMagick 6.7.8.9 (base)
        * FFmpeg 2.8.11 (rpmfusion-free-updates)

## 環境構築
```bash
yum clean all && yum -y update && reboot
```

### ImageMagick
```bash
yum install ImageMagick
```
[ImageMagick](https://www.imagemagick.org/) は画像処理に利用される。
remi リポジトリで 6.9.8.3 が、[公式サイト](https://www.imagemagick.org/script/download.php)で 7.0.5-4 の rpm パッケージが入手可能だが、最新である必要はないと思われるのでメンテナンスのしやすい base リポジトリの 6.7.8.9 で充分だろう。

### FFmpeg
```bash
yum install https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
yum install ffmpeg
```
[FFmpeg](https://ffmpeg.org/) は動画処理に利用される。
rpmfusion-free-updates リポジトリから 2.8.11 が、nux-dextop リポジトリから 2.6.8 がインストールできる。
また [John Van Sickle のサイト](https://www.johnvansickle.com/ffmpeg/)から最新バージョンの 3.3 などのスタティックビルドも入手可能。

### Redis
```bash
yum -y install epel-release && yum install redis
systemctl start redis && systemctl enable $_
```
[Redis](https://redis.io/) はデータ構造サーバーを実装する NoSQL データベース。一般的に全てのデータはメモリ上に格納される。[^redis]
Sidekiq で要求される。
remi リポジトリが最新安定版の 3.2.8 を、 RHSCL が rh-redis32 パッケージで 3.2.4 を提供しているが、epel の 3.2.3 でも充分だろう。
ちなみに remi-test リポジトリでベータ版最新の 4.0.0 RC2 が試せる。

[^redis]: https://ja.wikipedia.org/wiki/Redis

### PostgreSQL
```bash
yum install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
yum install yum-utils
yum-config-manager --enablerepo=pgdg96
yum install postgresql96-{contrib,devel,server}
ln -s /usr/pgsql-9.6/bin/* /usr/local/bin/
postgresql96-setup initdb
systemctl start postgresql-9.6 && systemctl enable $_
```
base リポジトリだと 9.2 がインストールされるので、パフォーマンスが影響するか不明だが公式リポジトリから最新安定版の 9.6 をインストール。

#### DB ユーザーの作成
```bash
sudo su - postgres -c 'psql -c "CREATE USER mastodon CREATEDB;"'
```

#### pg_stat_statements の有効化
```bash
sed -i.org '/shared_preload_libraries/ s/^#//' /var/lib/pgsql/9.6/data/postgresql.conf
sed -i "/shared_preload_libraries/ s/''/'pg_stat_statements'/" /var/lib/pgsql/9.6/data/postgresql.conf
sed -i "/shared_preload_libraries/a pg_stat_statements.track = all" /var/lib/pgsql/9.6/data/postgresql.conf
systemctl restart postgresql-9.6
```

デフォルトでは [PgHero](https://github.com/ankane/pghero) に `Query stats must be enabled for slow queries` と warning されるので [pg_stat_statements](https://www.postgresql.jp/document/9.6/html/pgstatstatements.html) モジュールをロードする。この際に postgresql96-contrib パッケージに含まれる pg_stat_statements.so が必要。
詳細は PgHero のガイド [Query Stats](https://github.com/ankane/pghero/blob/master/guides/Query-Stats.md) に説明がある。

### nginx
```bash
cat << "_EOF_" > /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1
_EOF_
yum install nginx
```
[nginx](https://nginx.org/en/) は Web サーバー。他に [Apache HTTP Server](https://httpd.apache.org/) や [lighttpd](https://www.lighttpd.net/) などがある。
epel リポジトリで 1.10.2 がインストールできるが、公式リポジトリがあるのでそちらで最新安定版の 1.12.0 をインストールする。

### Node.js
```bash
curl -sL https://rpm.nodesource.com/setup_6.x | bash
yum install nodejs
```
[Node.js®](https://nodejs.org/ja/) は、Chrome の V8 JavaScript エンジン で動作する JavaScript 環境。
公式では 4.x を提示しているが、LTS 最新安定版の 6.10.2 をインストールしても今のところ問題は見当たらない。
7.x や 8.x もあるようだが Nightly Build のようだ。

### Yarn
```
curl -sL https://dl.yarnpkg.com/rpm/yarn.repo -o /etc/yum.repos.d/yarn.repo
yum install yarn
```
[Yarn](https://yarnpkg.com/) は npm に代わる Node パッケージマネージャー。
package.json に記述されたパッケージおよびそれに依存関係のあるパッケージを自動インストールする。
公式リポジトリから最新安定版の 0.23.2 がパッケージインストール可能。

### Ruby
```bash
yum install bzip2 gcc-c++ git {openssl,readline,zlib}-devel
useradd mastodon
su - mastodon
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src && cd ~
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile && source ~/.bash_profile
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install 2.4.1 && rbenv global $_ && rbenv rehash
```
[Ruby](https://www.ruby-lang.org/ja/) のパッケージインストールは RHSCL の rh-ruby23 パッケージで Ruby 2.3.1 までしかインストールできないので、公式ドキュメントに従って Ruby のバージョン管理ツール [rbenv](https://github.com/rbenv/rbenv) で最新安定版の Ruby 2.4.1 をインストールする。他に [RVM](http://rvm.io/) がある。
CPU にもよると思うが `rbenv install` は7分程度かかった。
本番環境で rbenv 使うのどうなの的な意見も見受けられるが、Ruby に詳しくないので具体的にどのようなデメリットがあるのかはよく分からない。

### Bundler
```bash
gem install bundler
```
[Bundler](http://bundler.io/) は Gem パッケージマネージャー。
Gemfile ファイルに記述されたパッケージおよびそれと依存関係のあるパッケージを自動インストールする。

### Mastodon
#### インストール
```bash
cd ~ && git clone https://github.com/tootsuite/mastodon.git live && cd live
git checkout $(git tag | tail -n 1)
bundle install --deployment --without development test
yarn install
```
時間帯にもよると思うが `bundle install` は4~6分程度、`yarn install` は2分程度かかった。
公式ドキュメントを確認すると `git checkout` が追加されている。

#### 設定
```bash
cp .env.production.sample .env.production
sed -i '/^REDIS_HOST=/ s/redis/localhost/' .env.production
sed -i '/^DB_HOST=/ s/db//' .env.production
sed -i '/^DB_USER=/ s/postgres/mastodon/' .env.production
sed -i '/^DB_NAME=/ s/postgres/mastodon/' .env.production
sed -i '/^LOCAL_DOMAIN=/ s/example.com/192.168.56.101:3000/' .env.production
sed -i '/^LOCAL_HTTPS=/ s/true/false/' .env.production
sed -i "/^PAPERCLIP_SECRET=$/ s/$/`rake secret`/" .env.production
sed -i "/^SECRET_KEY_BASE=$/ s/$/`rake secret`/" .env.production
sed -i "/^OTP_SECRET=$/ s/$/`rake secret`/" .env.production
```

pg_hba.conf がデフォルトの状態だと `DB_HOST` に `localhost` や `127.0.0.1` を指定すると `PG::ConnectionBad: FATAL:  ユーザ"mastodon"のIdent認証に失敗しました` というエラーが出て DB に接続できない。空欄にするか pg_hba.conf を書き換える必要がある。

`LOCAL_DOMAIN` および `LOCAL_HTTPS` はいったんローカル確認のために一時的に変更。

#### セットアップ
```bash
bundle exec rails db:setup RAILS_ENV=production
bundle exec rails assets:precompile RAILS_ENV=production
```

### サービス化
#### mastodon-web (Puma)
```ini
cat << "_EOF_" > /etc/systemd/system/mastodon-web.service
[Unit]
Description=mastodon-web
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3000"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
TimeoutSec=15
Restart=always

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

[Puma](http://puma.io/) は Ruby アプリケーションサーバー。
他に [Unicorn](https://bogomips.org/unicorn/), [Rainbows!](http://bogomips.org/rainbows/), [Passenger](https://www.phusionpassenger.com/), [Raptor](http://www.rubyraptor.org/) (Passenger 5), [TorqueBox](http://torquebox.org/), [Thin](http://code.macournoyer.com/thin/) などがあるらしい。

#### mastodon-sidekiq (Sidekiq)
```ini
cat << "_EOF_" > /etc/systemd/system/mastodon-sidekiq.service
[Unit]
Description=mastodon-sidekiq
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target
_EOF_
```
[Sidekiq](http://sidekiq.org/) は Rails アプリで非同期処理を行うためのライブラリ。[^sidekiq]
他に [Resque](https://github.com/resque/resque) や [Delayed::Job](https://github.com/collectiveidea/delayed_job) などがあるらしい。

[^sidekiq]: http://dev.classmethod.jp/server-side/ruby-on-rails-sidekiq/

#### mastodon-streaming
```ini
cat << "_EOF_" > /etc/systemd/system/mastodon-streaming.service
[Unit]
Description=mastodon-streaming
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
ExecStart=/usr/bin/npm run start
TimeoutSec=15
Restart=always

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

#### サービスの起動と自動起動設定
```
systemctl daemon-reload && \
systemctl start mastodon-{web,sidekiq,streaming} && \
systemctl enable $_
```

### SELinux
```
setenforce 0
```
[SELinux](http://selinuxproject.org/) は Linux のカーネルに強制アクセス制御 (MAC) 機能を付加するモジュール。[^selinux]
とりあえず検証ということで一時無効に。
ConoHa の CentOS 7 テンプレートではデフォルトで無効になっていた。
コメント欄での情報提供[^comment]によると m.kagucho.net で利用されている SELinux のポリシーが [Gist](https://gist.github.com/akihikodaki/b47dcbdc51c9d95bc0748418442850c8) で公開されているとのこと。

[^selinux]: https://ja.wikipedia.org/wiki/Security-Enhanced_Linux
[^comment]: http://qiita.com/bezeklik/items/1a8530d530613acd665c#comment-4f330b87df7725888f04

### firewalld
```
firewall-cmd --permanent --add-service={http,https} && firewall-cmd --reload
```
[firewalld](http://www.firewalld.org/) はファイアーウォールデーモン。RHEL 7 から iptables の代わりにデフォルトでインストールされるようになった。[^firewalld]

[^firewalld]: https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Security_Guide/sec-Using_Firewalls.html#sec-Comparison_of_Firewalld_to_system-config-firewall_and_iptables

#### Query stats の有効化
```bash
sudo su - postgres -c 'psql -d mastodon -c "CREATE extension pg_stat_statements;"'
sudo su - postgres -c 'psql -d mastodon -c "SELECT pg_stat_statements_reset();"'
```

### 動作確認
```bash
firewall-cmd --add-port=3000/tcp
```
3000番ポートを一時的に開けて http://192.168.56.101:3000/ などで動作しているか確認。

```bash
firewall-cmd --reload
```
リロードして3000番ポートを閉じる。 [^reload]

[^reload]: Chrome だとセッションが維持されるのか、ポートを閉じて再読み込みしても表示される場合がある。

## 管理者設定
```bash
RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=admin
RAILS_ENV=production bundle exec rails mastodon:confirm_email USER_EMAIL=admin@example.com
```
`mastodon:make_admin` の `USERNAME` は任意のアカウント名を指定する。
`mastodon:confirm_email` でメール送信なしにアカウントの有効化が行える。

### Let's Encrypt
```bash
yum install certbot
certbot certonly --standalone -d example.com
```

#### 設定
```bash
cat << "_EOF_" > /etc/nginx/conf.d/mastodon.conf
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers EECDH+AESGCM:EECDH+AES;
  ssl_ecdh_curve prime256v1;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate         /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  ssl_stapling            on;
  ssl_stapling_verify     on;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 0;

  root /home/mastodon/live/public;

  gzip on;
  gzip_disable "msie6";
  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;

  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
#  add_header Content-Security-Policy "default-src 'none'; style-src 'self'; script-src 'self' https://example.com; img-src 'self' https://example.com; connect-src 'self' wss://example.com; font-src 'self'; frame-ancestors 'none'; media-src 'self';";

  location / {
    try_files $uri @proxy;
  }

  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 /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;
  }

  error_page 500 501 502 503 504 /500.html;
}
_EOF_
```

```bash
systemctl start nginx && systemctl enable $_
```

### Cronjobs
```bash
cat << "_EOF_" | crontab -
RAILS_ENV=production
@daily cd /home/mastodon/live && /home/mastodon/.rbenv/shims/bundle exec rake mastodon:daily > /dev/null
_EOF_
```
Cronjobs が `mastodon:daily` 1つに統合された模様。

## メール設定
「[VALUE-DOMAIN での Mailgun の DNS 設定](http://qiita.com/bezeklik/items/a714d50f85a386fb89d9)」などを参考に。

## セキュリティ対策
「[Linuxセキュリティ監査ツール Lynis](http://qiita.com/bezeklik/items/cf9be2deb546c69123e7)」がお手軽なので参考に。

## 参照情報