dockerでnginx導入と複数サービスの展開

dockerでのnginx導入から外部に複数サービスのを公開するやり方のメモです。
ローカル、サーバ（VPS）ともにUbuntuです。

## ローカルで起動

ひとまず何も設定せずnginxコンテナを作成して起動できることを確認します。
以下のように記述して`docker-compose up -d`します。

```docker-compose.yml
version: "2"
services:
  app:
    image: nginx:latest
    container_name: "app"
    ports:
      - "8080:80"
```

ローカルの開発環境で動かしていますが、ブラウザで`localhost:8080`にアクセスしてみると初期ページが表示されました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/267555/69f0ae7f-99f7-a987-c2d5-6c259f10dd87.png)

## 独自ページを表示


デフォルトの設定では初期ページが表示されるようになっているので、別途confファイルを用意して指定したHTMLファイルが表示されるように設定します。
この設定ファイルは`/etc/nginx/nginx.conf.d`以下に置くことで機能します。

```default.conf
server {
    listen       80;
    listen       443;
    server_name  localhost;

    location / {
        root   /app;
        index  index.html index.htm;
    }

    error_page  404 /404.html;
    location = /40x.html {
        root   /usr/share/nginx/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

表示用のHTMLファイルを用意します。

```index.html
<html>
    <body>
        This is index.html.<br />
        <a href="pageA.html">next</a>
    </body>
</html>
```

```pageA.html
<html>
    <body>
        This is pageA.html.
    </body>
</html>
```

`docker-compose.yml`に`volumes`の記述を追加して、上記の設定ファイルとHTMLファイルをマウントします。
先述したとおり、設定ファイルは`/etc/nginx/nginx.conf.d`以下に配置されるようします。
HTMLファイルについては、`deafult.conf`で指定したrootのパスに置いてください。

```docker-compose.yml
version: "3"
services:
  app:
    image: nginx:latest
    container_name: "app"
    ports:
      - "8080:80"
    volumes:
      - ./nginx/html:/app
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf 
```

これで`localhost:8080`にアクセスすると先程のHTMLが表示されるようになりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/267555/01fbee28-8191-428c-74a3-741a07d65d68.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/267555/31a27d06-2a44-72ad-1220-5a219a0ef591.png)

## ドメインの取得

外部公開に向けて事前にどこかのレジストラでドメインの取得をしておきます。
それぞれの紐付けとして以下の作業が必要ですが、利用サービスによって違うので各々のマニュアルを参照してください。

- ドメインにVPSのネームサーバを設定
- VPS側でドメイン、DNSレコードの登録

## リバースプロキシとSSL

今回は１つのサーバ上で複数サービスを稼働させる想定をしています。
構成としてはリバースプロキシを用意してアクセスしてきたリクエストを各々のDockerコンテナに振り分けます。
また例のごとくLet's Encryptで無料のSSL対応も行います。

詳しくはここらへんのサイトを参考していただければ分かると思うのですが、要点だけ書きます。

- [VPSにdockerで複数サイトをホスティングするには？](https://suin.io/561)
- [Docker上でnginx-proxy他を使ってSSL対応マルチドメインサーバ環境の構築](https://hogehuga.net/linux/116/#docker-letsencrypt-nginx-proxy-companion)

### サブドメインの設定

事前にVPS側でDNSレコードを追加しサブドメインの設定を行います。
Aレコードの設定でいけると思います。
これもVPS等のマニュアルに書いてあると思います。

### ポート開放

ポートの`80`と`443`が開いていない場合、開放しておきます。

```
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables-save > /etc/iptables.rules
```

### リバースプロキシの設定

Dockerで行うに当たって以下の2つのイメージを使うことで実現します。
前者がnginxのリバースプロキシで、後者はLet's EncyptのSSLサーバ証明書を自動取得・更新してくれるものです。
SSLサーバ証明書は手動でもできますがせっかくなので楽なやり方を採用します。

- [nginx-proxy](https://github.com/jwilder/nginx-proxy)
- [docker-letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion)


以下のような`docker-compose.yml`になりました。
基本的には公式のGitHubに書いてある通りです。

なお`volumes_from`を使っていますが、`version: "3"`から廃止されたそうなので、`version: "2"`にしておかないと動きません。（今後のことを考えると何らかの対策が必要ですが、今回は試験的な試みなのでスルーします・・・）


>補足（2019/09/08）
最近思ったのですがリバースプロキシをするnginxはわざわざDockerコンテナである必要はないかもしれません。
この時は複数のサブドメインの証明書の取得するのが面倒で全部面倒見てくれる上記のコンテナ２つを使いました。しかしワイルドカード証明書を使えば証明書は１組で済むし自動更新はcertbotやlegoを使えばそこまで手間ではないです。
またリバースプロキシのためにDockerネットワークの記述をそれぞれのdocker-compose.ymlに記述するのは本質的ではない気がするし外部のライブラリにがっつり依存するのもどうかなという思いがあります（分かっていて使うのであれば問題ない）。
そういうわけで今自分がやるのであればサブドメインに割り当てるサービスは独立したコンテナとして立ち上げてリバースプロキシはホスト側にnginxを建てて`127.0.0.1:{PORT}`を見に行くのが良いかなという感じです。

```proxy/docker-compose.yml
version: "2"
services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    container_name: nginx-proxy
    privileged: true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /srv/docker/nginx-proxy-with-encrypt/certs:/etc/nginx/certs:ro
      - /etc/nginx/vhost.d
      - /usr/share/nginx/html
      - /var/run/docker.sock:/tmp/docker.sock:ro
    restart: always

  letsencrypt-nginx:
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: letsencrypt-nginx
    privileged: true
    volumes:
      - /srv/docker/nginx-proxy-with-encrypt/certs:/etc/nginx/certs:rw
      - /var/run/docker.sock:/var/run/docker.sock:ro
    volumes_from:
      - nginx-proxy
    restart: always

networks:
  default:
    external:
      name: shared

```

Docker内で通信に使うネットワークを作成します。上で設定している`networks`で使います。

```
$ docker network create --driver bridge shared
```


### アプリ作成

振り分け先としてサイトAとサイトB用の`docker-compose.yml`を用意します。

#### サイトA

ベースは一番最初に作成した`docker-compose.yml`です。

```app1/docker-compose.yml
version: "2"
services:
  app1:
    image: nginx:latest
    container_name: "app1"
    environment:    
        VIRTUAL_HOST: app1.example.com
        LETSENCRYPT_HOST: app1.example.com
        LETSENCRYPT_EMAIL: example@mail.com
    restart: always
    volumes:
      - ./app/html:/app
      - ./app/default.conf:/etc/nginx/conf.d/default.conf

networks:
  default:
    external:
      name: shared
```

`port`の設定がなくなり、代わりに`VIRTUAL_HOST`, `LETSENCRYPT_HOST`, `LETSENCRYPT_EMAIL`を追加しました。
`VIRTUAL_HOST`には用意したサブドメインを設定します。
`LETSENCRYPT_HOST`, `LETSENCRYPT_EMAIL`にはLet's EncryptでSSLを取得する時に使用する情報です。
また`networks`も上で作ったものを設定して通信を疎通させます。

#### サイトB

ほぼ同様です。`app1`の部分を`app2`に変えています。

```app2/docker-compose.yml
version: "2"
services:
  app1:
    image: nginx:latest
    container_name: "app2"
    environment:    
        VIRTUAL_HOST: app2.example.com
        LETSENCRYPT_HOST: app2.example.com
        LETSENCRYPT_EMAIL: example@mail.com
    restart: always
    volumes:
      - ./app/html:/app
      - ./app/default.conf:/etc/nginx/conf.d/default.conf

networks:
  default:
    external:
      name: shared
```

### 起動してみる

順番に`docker-compose up -d`して起動していきます。
`volume`の設定で`/var/run/docker.sock`をマウントすることによって、Dockerコンテナの起動/終了等を検知して自動的に振り分けの設定を行ってくれます。具体的にはnginxの`default.conf`を都度書き換えているようです。

またSSLサーバ証明書の取得に多少時間がかかるので少し待ったほうがいいと思います。

```
cd proxy
$ docker-compose up -d
cd app1
$ docker-compose up -d
cd app2
$ docker-compose up -d
```
さて、これで`app1.example.com`などでアクセスし、ページが振り分けられていれば成功です。
またhttps通信が成功していればリダイレクトされ、ブラウザの`保護されていない通信`という表示も消えていると思います（Chromeの場合）。

## その他、困ったこと
- カーネルのバージョン
  - 3.1以上じゃないとdockerが動かない（`uname -r`で確認）
  - 古いバージョンだった場合に利用しているVPSがOpenVZ方式だったら利用者側でアップデートできないので詰み。
- dockerおよびdocker-composeは公式サイトに従って入れる。
  - apt-getしたら古かったので動かなかった。
- `sudo: unable to resolve host hostname`で怒られた。
  - `/etc/hosts`に`127.0.1.1 hostname`を追加で消える。