Docker🐳でMastodon🐘のインスタンスを立てるドン (リバースプロキシにnginx-proxy + letsencrypt-nginx-proxy-companionを使う)

[Mastodon](https://github.com/tootsuite/mastodon)が流行っているので、自分のVPSでインスタンスを立ち上げてみました。こちらです。

[mstdn\.brdr\.jp \- BORDER/mstdn](https://mstdn.brdr.jp/about)

サーバにDockerとNginxをインストールして、という感じのやり方をされている人が多い印象ですが、NginxもDockerで構築してみます。
また、[nginx-proxy](https://github.com/jwilder/nginx-proxy)と[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion)を使ったリバースプロキシを別に立ち上げ、1つのサーバで複数のサービスを運用できるようにしました。

メールの配信サービスは[SparkPost](https://www.sparkpost.com/)を利用してます。


## ネットワークを作成する
色々試したんですが、`front`と`back-mstdn`という2種類のネットワークをあらかじめ作っておくといい感じに動作しました。

* `front`: リバースプロキシと、Mastodon(や他のサービス)を繋ぐためのネットワーク
* `back-mstdn`: Mastodonの各コンテナを繋ぐためのネットワーク

サービスが増えると`back-hoge`、`back-fuga`のように`back`が増えていくことを想定してみました。

```
# ネットワークを作成する
$ docker network create --driver bridge front
$ docker network create --driver bridge back-mstdn
```


## リバースプロキシのdocker-compose.ymlを作成する

```yaml:proxy/docker-compose.yml
version: '2'
services:
  proxy:
    image: jwilder/nginx-proxy:alpine
    container_name: proxy-nginx
    ports:
      - 80:80
      - 443:443
    restart: always
    tty: false
    privileged: true
    volumes:
      - ./certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - /etc/nginx/vhost.d
      - /usr/share/nginx/html
    networks:
      - front

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: proxy-letsencrypt
    restart: always
    tty: false
    privileged: true
    volumes:
      - ./certs:/etc/nginx/certs:rw
      - /var/run/docker.sock:/var/run/docker.sock:ro
    volumes_from:
      - proxy
    networks:
      - front

networks:
  front:
    external: true
```

リバースプロキシの方はこれだけです。
letsencrypt-nginx-proxy-companionが、Let's Encryptを使ったSSL証明書の発行や更新をいい感じにおこなってくれます。

発行されたSSL証明書などは、docker-compose.ymlと同じ階層の`certs`ディレクトリ以下に保存されます。


## Mastodonのdocker-compose.ymlを編集
Mastodonのリポジトリをcloneして、`docker-compose.yml`を編集していきます。
何を編集するかというと、

* Nginxコンテナを新たに追加する
    * ポートは適当に`9090`番で
* 各コンテナを`back-mstdn`ネットワークで繋ぐ
    * Nginxはリバースプロキシとやり取りするので、`front`と`back-mstdn`の両方のネットワークに繋ぐ
* データを永続化するために、`db`と`redis`の`volumes`のコメントアウトを解除

という感じです。

```yaml:service/mastodon/docker-compose.yml
version: '2'
services:
  nginx:
    image: nginx:1.11.10-alpine
    container_name: mstdn-nginx
    ports:
      - 9090:9090
    restart: always
    tty: false
    env_file: .env.production
    links:
      - web
      - streaming
    volumes:
      - ./setting/nginx/conf.d:/etc/nginx/conf.d:ro
      - ./setting/nginx/conf:/etc/nginx/conf/:ro
    volumes_from:
      - container:proxy-nginx
    networks:
      - front
      - back-mstdn

  db:
    restart: always
    image: postgres:alpine
    container_name: mstdn-db
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - back-mstdn

  redis:
    restart: always
    image: redis:alpine
    container_name: mstdn-redis
    volumes:
      - ./redis:/data
    networks:
      - back-mstdn

  web:
    restart: always
    build: .
    image: gargron/mastodon
    container_name: mstdn-web
    env_file: .env.production
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
    volumes:
      - ./public/assets:/mastodon/public/assets
      - ./public/system:/mastodon/public/system
    networks:
      - back-mstdn

  streaming:
    restart: always
    build: .
    image: gargron/mastodon
    container_name: mstdn-streaming
    env_file: .env.production
    command: npm run start
    ports:
      - "4000:4000"
    depends_on:
      - db
      - redis
    networks:
      - back-mstdn

  sidekiq:
    restart: always
    build: .
    image: gargron/mastodon
    container_name: mstdn-sidekick
    env_file: .env.production
    command: bundle exec sidekiq -q default -q mailers -q pull -q push
    depends_on:
      - db
      - redis
    volumes:
      - ./public/system:/mastodon/public/system
    networks:
      - back-mstdn

networks:
  front:
    external: true
  back-mstdn:
    external: true
```

### Mastodonに追加したNginxの、volumes_fromの設定

```
volumes_from:
  - container:proxy-nginx
```

こんな設定がMastodonのNginxのところにあります。これは、リバースプロキシで作成したSSLの証明書と秘密鍵をMastodon Nginxからもアクセスできるようにするための設定です。

Mastodon Nginxの中に入って確認すると、`/etc/nginx/certs`というディレクトリができていて、リバースプロキシの`certs`ディレクトリと内容が同期されるようになります。


## Nginxの設定ファイルを追加
`mastodon/setting/nginx/conf.d/default.conf`を作成して、[Production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md)にあるような感じで、Nginxの設定をやっていきます。
SSLの設定をやっておかないとリモートフォローが動かないので、ちゃんと設定していきます。

Production Guideの例では、HTTPでのアクセス(80番ポート)をHTTPS(443番ポート)にリダイレクトするような記述があります。ただ、今回はそういうのはリバースプロキシのnginx-proxyが全部やってくれるので、9090番ポートにいきなりHTTPSでのアクセスが飛んでくることを想定して設定を変更していきます。

```mastodon/setting/nginx/conf.d/default.conf
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 9090 ssl;
  server_name mstdn.brdr.jp;

  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/nginx/certs/mstdn.brdr.jp/fullchain.pem;
  ssl_certificate_key /etc/nginx/certs/mstdn.brdr.jp/key.pem;

  # 以下省略
```

```
ssl_certificate     /etc/nginx/certs/mstdn.brdr.jp/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/mstdn.brdr.jp/key.pem;
```

`ssl_certificate`と`ssl_certificate_key`は、リバースプロキシのNginxに保存されている証明書と秘密鍵をそれぞれ指定します。


## .env.productionを編集
`.env.production.sample`をコピー&リネームした、`.env.production`を編集していきます。Mastodonの設定もなんですが、リバースプロキシ用の環境変数も追記していきます。
編集すべきところを抜粋していきます。

```mastodon/.env.production
# リバースプロキシ用の設定
# 一番上とかに追記
VIRTUAL_HOST=mstdn.brdr.jp
VIRTUAL_PORT=9090
VIRTUAL_PROTO=https
LETSENCRYPT_HOST=mstdn.brdr.jp
LETSENCRYPT_EMAIL=mail@example.com
LETSENCRYPT_TEST=false

# 探して編集
LOCAL_DOMAIN=example.com
LOCAL_HTTPS=true

# 探して編集
# `docker-compose run --rm web rake secret`を3回実行して、出力された文字列をそれぞれ入れる
PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=

# 探して編集
# SparkPostの場合です
SMTP_SERVER=smtp.sparkpostmail.com
SMTP_PORT=587
SMTP_LOGIN=SMTP_Injection
SMTP_PASSWORD=<API Key>
SMTP_FROM_ADDRESS=<好きなアドレス>@mail.example.com
```

こんな感じです。

nginx-proxyは超便利で、`VIRTUAL_HOST`に設定したドメインへのアクセスがあった場合、自動で`VIRTUAL_PORT`で設定したポート番号へのアクセスを待ち受けているサーバに転送してくれます。

`VIRTUAL_PROTO=https`の設定はとても大事で、リバースプロキシからバックエンドへの接続をHTTPSでやってくれるようになります。[ここ](https://github.com/jwilder/nginx-proxy#ssl-backends)に説明があります。

`VIRTUAL_PROTO=https`を設定しないと、MastodonへのアクセスがHTTP(9090番ポート)になってしまうので、先程のNginxの設定ではうまく処理できず、「400 Bad Request The plain HTTP request was sent to HTTPS」みたいなエラーが出てしまいます。
「じゃあProduction Guideの通りリダイレクトすればええやん」という感じなんですけど、うまく設定できずリダイレクトループが発生してしまったので、この形に落ち着きました。

### SparkPostの設定
下記の投稿の通りにやると、ばっちりです！（力尽きた）

[文系非エンジニアでも立てられるマストドン\(Mastodon\) CentOS7\+Docker\+SparkPost](http://qiita.com/mazeltov7/items/6fed319c81a486caa2b3#%E3%83%A1%E3%83%BC%E3%83%AB%E5%AF%BE%E5%BF%9C)


## リバースプロキシとMastodonを起動するドン
以上で設定ができたので、リバースプロキシとMastodonを起動しましょう。

```
# リバースプロキシの起動
$ cd ~/proxy
$ docker-compose up -d

# Mastodonの起動
$ cd ~/service/mastodon
$ docker-compose up -d
```

nginx-proxyは最高なので、今後、Mastodon以外のサービスを作って起動した場合も、`VIRTUAL_HOST`と`VIRTUAL_PORT`をバックエンドの環境変数に設定しておくだけで、起動や変更を検知して自動でいい感じにアクセスを振り分けてくれます。


## 以上
サーバサイドの知識が全然ない僕ですが、なんとかMastodonのインスタンスを立ち上げることができました！
[ドメインでアイデンティティを確立](https://isid.ai/diary/2017/04/14/1179/)していきたいと思います。ちなみに僕のアカウントは以下なので、良かったらフォローしてください。

[Ryo Nakae \- BORDER/mstdn](https://mstdn.brdr.jp/@ryonakae)

DockerやNginxのセキュリティに対する知見がないので、おいおいその辺りは勉強していこうと思います。


## 参考
* [CentOS7でmastodonを建てる \- Qiita](http://qiita.com/motiakoron/items/1e5b0679f7283bfee70b)
* [文系非エンジニアでも立てられるマストドン\(Mastodon\) CentOS7\+Docker\+SparkPost \- Qiita](http://qiita.com/mazeltov7/items/6fed319c81a486caa2b3#%E3%83%A1%E3%83%BC%E3%83%AB%E5%AF%BE%E5%BF%9C)
* [Docker × Nginx × Let'sEncrypt \| 俺の考えた最強のサーバー構築方法！ \| Hackers Log](https://hackerslog.net/post/labs/docker-letsencrypt-on-docker-and-nginx/)