とりあえずのdocker-compose upから入って、Web server(Nginx)の基礎設計を学びながら、Dockerを学ぶ①

①環境構築〜最もシンプルなWeb server構築編です

分かりづらい点、不正確な点はコメントいただけましたらモチベーションに繋がります
今回こちらに先立ってZennというサービスで記事を書いて見ました
元記事
[とりあえずのdocker-compose upから入って、Web serverの基礎設計を学びながらDockerを学ぶ① | Zenn](https://zenn.dev/naokit_dev/articles/1f358cf75fab30c55009)


## はじめに

以下のような方とって有益な内容になればと思っています

1. これから初めてのwebアプリを作成する
2. webアプリを作成し、これからデプロイする
3. 初めてデプロイまで到達したが、Nginxが何をしているかとか、設定内容はコピペでよくわかっていない

私は独学プログラミング5ヶ月目で3の状態に近いかと思います
個人的なメッセージとしては是非1.の状態の方により多く、この記事の内容が届くといいなと思っています
**Dockerがなんとなくわかる、便利な気がする！**、**webアプリが動く仕組みに関心を広げる**、そんな気づきを共有できたらいいなと思っています

この記事と同じ内容は、AWS上のEC2を利用したり、契約したVPSを利用するよることで再現可能ですが、dockerを利用すれば**完全無料で挑戦可能**です！より手軽で、予定外の課金に怯える必要はありません

---

## この記事で知ることができる内容

### ネットワークに視野を広げる

- webアプリが最低限動作するために必要な構成を知る
- Nginx（エンジンエックスと読みます）の3つの重要な役割, webサーバー, ロードバランサー, リバースプロキシの役割に触れる
- Nginxの基本的な設定を知る

### Dockerの基本を知る

- 既存の開発環境を簡単に再現できることを知る
- Docker上で開発を行うために最低限必要なコマンドを試すことができる
- docker-compose.ymlに記述された内容や、volumeの仕組みを手を動かして知ることができる
- 複数のdocker-compose.ymlを用意して、異なる環境をシミュレートする (development -> production)

### webアプリの開発環境 - 本番環境での違いを知る

- 本番環境でアセットコンパイルが必要な理由を知る
- 開発環境でアセットコンパイルが必要でない理由を知る

よって、この記事の最後ではDocker上で、仮想の本番環境で開発環境との違いに触れながら、アプリをデプロイしてみます

### Appendix

appendixは補足的内容となっています
その項で知ることのできる内容を初めに書いておきましたので、
改めて知る必要のない内容でしたら読み飛ばして頂いて結構です
もし知らない内容でしたら、実際に手を動かして頭の片隅に留めておくことで、**後々役に立つ物があるかもしれません**

---

## 必要なもの、スキル

- エディタ（VS Codeで検証しています）
  [Visual Studio Code - Code Editing. Redefined](https://code.visualstudio.com/)

- Linuxの基礎コマンドの知識（cd, ls, vi...くらい、なくてもコピペでなんとかなります）

- Docker desktop
  [Docker Desktop for Mac and Windows | Docker](https://www.docker.com/products/docker-desktop)

- Dockerって何？っていう方は以下がおすすめです

  [【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その1:コンテナってなに？ 〜 | SIOS Tech. Lab](https://tech-lab.sios.jp/archives/18811)
  概念をさらっと理解していただき、ここでは手を動かしてみるというのがおすすめです

アプリの部分はFW（フレームワーク）にRailsを使用しておりますが、
Railsの知識はなくても大丈夫です

（私自身Rails以外の開発経験がないため、他のFWにおいて不適切な内容があるかもしれません）

## 検証環境

macOS Catalina
docker desktop: 2.3.05
(docker engin: 19.03.12, docker-compose: 1.27.2)
Nginx: 1.18
Rails: 6.03
PostgreSQL: 11.0

## アーキテクチャ（設計）概要

これからDockerで構築する環境では
**Nginxはリバースプロキシとして機能していて、静的コンテンツを`app: Rails`に代わって代理（=プロキシ）配信しており、動的コンテンツへのリクエストのみ`app: Rails`に転送するようになっています**

というのを少しずつ理解していきたいと思います

![スクリーンショット 2020-09-28 2.53.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649386/8507e61e-cfbc-faf9-7a7e-6662db8b27e0.png)


よく見る構成です（Databaseほか一部省略）
docker上でweb(Nginx), app(rails)というサービスがそれぞれ独立したコンテナで動いていて
docker-composeによってそれぞれの依存関係等が定義されているような理解です

---

## 目標5分、DockerでRailsの環境構築

Nginx - Railsの環境を構築します
以下の素晴らしい記事を参考にします（笑）
Nginx, Rails 6, PostgreSQL環境（おまけにBootstrapまで）がすぐに構築できます！
少しづつ改善していますので、改善コメントもお待ちしております。

[コマンドひとつ、5分でRails6の開発環境構築 on Docker - Rails6 + Nginx + PostgreSQL + Webpack (Bootstrap install済) - Qiita](https://qiita.com/naokit-dev/items/96cb41361ebc4b7716c0)

上記をベースに今回の記事のために用意したソースコード
<https://github.com/naokit-dev/try_nginx_on_docker.git>

### ソースコードをgit clone

```sh
#アプリを配置するディレクトリを作成（アプリケーションルート）
mkdir try_nginx_on_docker

#アプリケーションルートへ移動
cd $_

#ソースコード取得
git clone https://github.com/naokit-dev/try_nginx_on_docker.git

#アプリケーションルートにソースコードを移動
cp -a try_nginx_on_docker/. .
rm -rf try_nginx_on_docker
```

以下のような構成になるかと思います

```sh
.(try_nginx_on_docker)
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── docker
│   └── nginx
│       ├── default.conf
│       ├── load_balancer.conf
│       └── static.conf
├── docker-compose.prod.yml
├── docker-compose.yml
├── entrypoint.sh
├── setup.sh
└── temp_files
    ├── copy_application.html.erb
    ├── copy_database.yml
    └── copy_environment.js
```

ソースコードの一部
`docker-compose.yml`
4つのコンテナが定義されています

```yml
version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
      - /var/www/html
    depends_on:
      - app

  db:
    image: postgres:11.0-alpine
    volumes:
      - postgres:/var/lib/postgresql/data:cached
    ports:
      - "5432:5432"
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
      TZ: Asia/Tokyo

  app:
    build:
      context: .
    image: rails_app
    tty: true
    stdin_open: true
    command: bash -c "rm -f tmp/pids/server.pid && ./bin/rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp:cached
      - rails_cache:/myapp/tmp/cache:cached
      - node_modules:/myapp/node_modules:cached
      - yarn_cache:/usr/local/share/.cache/yarn/v6:cached
      - bundle:/bundle:cached
      - public:/myapp/public
      - log:/myapp/log
      - /myapp/tmp/pids
    tmpfs:
      - /tmp
    ports:
      - "3000-3001:3000"
    environment:
      RAILS_ENV: ${RAILS_ENV:-development}
      NODE_ENV: ${NODE_ENV:-development}
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
      WEBPACKER_DEV_SERVER_HOST: webpacker
    depends_on:
      - db
      - webpacker

  webpacker:
    image: rails_app
    command: ./bin/webpack-dev-server
    volumes:
      - .:/myapp:cached
      - public:/myapp/public
      - node_modules:/myapp/node_modules:cached
    environment:
      RAILS_ENV: ${RAILS_ENV:-development}
      NODE_ENV: ${NODE_ENV:-development}
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    tty: false
    stdin_open: false
    ports:
      - "3035:3035"

volumes:
  rails_cache:
  node_modules:
  yarn_cache:
  postgres:
  bundle:
  public:
  log:
  html:
```

### 環境構築

```sh
source setup.sh
```

正常にセットアップが終われば
アプリケーションルートディレクトリで以下のコマンドでコンテナ立ち上げた後

```sh
docker-compose up

# バックグラウンドで起動させる場合 -dオプション
docker-compose up　-d
```

ブラウザから`localhost`もしくは`localhost:80`へアクセスすると

`Yay! You’re on Rails!`が確認できるかと思います
**誰でも簡単に開発環境を構築できます！Dockerのメリット1つ目**

ここで起動しているコンテナを確認してみます
(`-d`オプションを付けずに`docker-compose up`した場合には新しくターミナルを開きます。VS Codeなら`control`+`@`  \*mac環境）

```sh
docker ps
```

web (Nginx), app(Rails), webpacker(webpack-dev-server), db(PostgreSQL)の4つのコンテナが起動していることだけ確認してください

確認できたら一旦コンテナを終了させておきます

```sh
docker-compose down
```

---

## Nginxで静的コンテンツを配信してみる

まだRailsアプリは使用しません
ここでは以下に挑戦します

- Nginxの最小の設定を確認する

- Docker-composeを利用しつつ、コンテナを単独 (nginxのみ) で起動してみる

- Nginx単独で単純な静的コンテンツ（HTML）を配信してみる

### 最もシンプルなNginxの設定

Nginxの設定を変更するため`docker-compose.yml`を編集します

```yml
services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
    #ここを書き換える./docker/nginx/default.conf... -> ./docker/nginx/static.conf...
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
    depends_on:
      - app
...
```

**Dockerのvolumeについて少し**
`volumes:`の`./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf`は`<host側path>:<container側path>`になっていて
これによってホスト（ローカル）側の`static.conf`をボリュームとしてマウントし、コンテナ内の`default.conf`として扱えるようにしています
ここでは、**ホスト側とコンテナ内では、ストレージが独立して存在するように振る舞われるため、このようなvolumeマウントが必要**ということだけ心に留めてください

`docker/nginx/static.conf`にはNginxの設定が記述されており、中身は以下のようになっています

```yml
server { #ココから
      listen 80;　# must
      server_name _; # must
      root /var/www/html; # must
      index index.html;
      access_log /var/log/nginx/access.log;
      error_log  /var/log/nginx/error.log;
}　#ココまで、一つのserverブロック = Nginxが扱う一つの仮想サーバーの仕様
```

**server**: "{}"で囲われた内容(serverブロック)をもとに仮想サーバーを定義します

ここでは以下3項目が設定必須です

**listen**: 待ち受けるIP, portを指定（xxx.xxx.xxx.xxx:80, localhost:80, 80）
**server_name**: 仮想サーバに割り当てる名前。Nginxはリクエストに含まれるホスト名(example.com)やIP(xxx.xxx.xxx.xxx)に一致する仮想サーバを検索します。("_"はすべての条件で一致させるの意味です。その他、ワイルドカードや正規表現が利用可能)
**root**: ドキュメントルート、コンテンツが配置されたディレクトリを指定します

ちなみにlogについては、`etc/nginx/nginx.conf`というファイルで上記と同じパスが定義されているので、
ここで記述がなくても、error_logおよびaccess_logともに`/var/log/nginx/`以下に記録されるはずです
例えば`access_log /var/log/nginx/static.access.log;`とすることで、当該の仮想サーバ(serverブロック)固有のログを記録することもできるようです

### Nginxコンテナ単独で起動

先程の`docker-compose up`ではnginx, rails, webpack-dev-server, dbのすべてのコンテナが起動していますが、docker-composeのオプションを使用することで特定のコンテナだけを起動することも可能です

`--no-deps`: コンテナ間の依存関係を無視して起動 (ここではweb: nginxのみ)
`-d`: バックグラウンドでコンテナを起動、シェルは入力を継続できます
`-p`: ポートマッピング\<host>:\<container>
`web`: composeで定義されたnginxコンテナです

以下のコマンドでNginxコンテナを起動します

```sh
docker-compose run --no-deps -d -p 80:80 web
```

（ポートマッピングについてはcomposeでも指定しているのですが、改めて指定する必要があり、ホスト側のport 80をwebコンテナのport 80にマッピングしています）

docker-composeをオプション無しで実行したときとの違いを確認します

```sh
docker ps
```

先ほどと異なり、nginxのコンテナのみが起動していると思います

### HTMLコンテンツを作成

コンテナの中でシェルを呼び出します

```sh
docker-compose run --no-deps web bash
```

以下webコンテナ内での作業です

```sh
# index.htmlを作成
touch /var/www/html/index.html

# index.htmlの中身を追加
echo "<h1>I am Nginx</h1>" > /var/www/html/index.html

# index.htmlを確認
cat /var/www/html/index.html
<h1>I am Nginx</h1>
```

これでコンテナ内のドキュメントルート直下に`index.html`が作成できたので`exit`でシェルを閉じましょう

### 動作確認

ブラウザからlocalhostにアクセスすると、
 以下のようにHTMLとして配信されているのが確認できていると思います

![スクリーンショット 2020-09-22 11.25.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/649386/d21a5bd2-2d5e-382e-b911-fb90abed8aa4.png)


**ここでのNginxはリクエストに一致するコンテンツをドキュメントルートから探して、一致するものを返すというシンプルな挙動をしています**

確認できたら一旦コンテナを終了させておきます

```sh
docker-compose down
```

### Appendix - リクエストに一致する仮想サーバがない場合のNginxの挙動

- Nginxのデフォルトサーバーの概念を知る

Nginxはクライアントからのリクエストに含まれるHostフィールドの情報をもとに、どの仮想サーバーにルーティングするかを定義しています

**では、いずれの仮想サーバもリクエストと一致しない場合はどのような挙動をするのでしょうか？**
設計を考える上で重要そうだったので、ここではそれを確認してみます

先の設定ファイルのserverブロックで、いずれのリクエストに対しても該当するように`server name`を定義しましたが、これをリクエストと一致しないデタラメな名前に書き換えてみます

```json
server_name undefined_server;
```

再びNginxコンテナを起動します

```sh
docker-compose run --no-deps -p 80:80 web
```

ブラウザからlocalhostにアクセスすると、リクエストに一致する仮想サーバが存在しないにもかかわらず
予想に反して先ほどと同じ"I am Nginx"が表示されると思います

**default server**

Nginxはリクエストがいずれの仮想サーバにも該当しなかった場合、default serverで処理する使用になっており、一番最初、一番上に記述された仮想サーバをdefault serverとして扱う仕様になっています

> In the configuration above, the default server is the first one — which is nginx’s standard default behaviour. It can also be set explicitly which server should be default, with the default_server parameter in the listen directive:
> [How nginx processes a request](https://nginx.org/en/docs/http/request_processing.html)

またはlistenディレクティブに明示的に`default_server`を指定することも可能です

```json
listen      80 default_server;
```

今回の実験では"undefined_server"はリクエストに一致しないが、他に一致するものがないので
default serverとしてルーティングされたと考えられます

**いずれの仮想サーバもリクエストと一致しない場合 => default serverにルーティングされる**

うまくバックエンドのサーバーに接続されない場合など、エラーを切り分けるのに役立つ気がします

一旦コンテナも終了させておきましょう

```sh
docker-compose down
```

### appendix - Dockerのvolumeを少し理解する

- コンテナの独立性について知る
- コンテナ - コンテナ間でストレージを共有する（永続化して共有する）仕組みとしてvolume、ここでは特にnamed volume, anonymous volumeの違いについて知る

**そもそもvolumeが必要(= 永続化が必要)な意義について**
Dockerではコンテナ内のデータを永続化するためにvolumeを作成し管理します

よくわからないので確認してみます

webコンテナの中でシェルを呼び出します

```sh
docker-compose run --no-deps web bash
```

以下webコンテナ内での作業です

```sh
# 検証用のディレクトリを作成
mkdir /var/www/test

# 検証用のファイルを作成します
touch /var/www/test/index.html

# 存在確認
ls /var/www/test/
```

これで`/var/www/test`は`docker-compose.yml`の中でボリュームとして管理されていないパスであることがポイントです

一旦`exit`でシェルを閉じましょう（コンテナも終了します）

再度webコンテナを起動しシェルを呼び出します

```sh
docker-compose run --no-deps web bash
```

先程のファイルを探してみます

```sh
cat /var/www/test/index.html
```

```sh
ls /var/www
```

いかがでしょうか、
ディレクトリ`/var/www/test`、ファイル`/var/www/test/index.html`ともに見つからないと思います

**コンテナを終了すると、コンテナ内のデータは保持されない**これが原則であり
ボリュームはこの仕組を回避するために利用可能です

`exit`でターミナルを閉じます

すべてのコンテナを停止します

```sh
docker-compose down
```

**volumeの種類**

Dockerにおけるボリュームには以下のタイプがありますが、コンテナ内のデータを永続化するという点では同じです

1. host volume ?(ちょっと名前がわからないです)
2. anonymous volume (匿名ボリューム?anonymous volumeで通っている気がします)
3. named volume (名前付きボリューム)

docker-compose.ymlを見みてみます

```yml
version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf #host volume
      - public:/myapp/public # named volume
      - log:/var/log/nginx # named volume
      - html:/var/www/html # named volume

...

volumes: # ここで異なるコンテナ間での共有を定義
  public:
  log:
  html:
```

**host volume**
nginxの設定のパートで触れました
`./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf`の部分でホスト側のパスをボリュームとしてマウントします
**ホスト内のファイルをコンテナ側にコピーしているイメージ**です

**named volume**
`html:/var/www/html`の部分
"html"という名前をつけてボリュームをマウントしています
さらに、"services"ブロックと同列の"volumes"ブロックでこの名前をもって定義することで
**複数のコンテナ間でボリュームをシェアすることを可能**にしています

そして、**このボリュームはホスト側からは独立して永続化されます**

最後に**anonymous volume**
公式docではnamed volumeとの違いは**名前があるかないか**のみとありますが
実際に名前がないというより、**named volumeの名前に相当する部分がコンテナごとにハッシュで与えられている**そうです
ちょっとわかりにくいですが、ホスト側をマウントする必要がないが、永続化の必要がある、**かつ複数のコンテナでの共有を想定しない場合**に利用するケースが考えられます
(まだイメージし難いですが、この後のコンテンツでanonymous volumeでないといけない場面に遭遇します）

ここでは少し理解を深めるために検証してみます
もともとnamed volumeとして定義している`/var/www/html`をanonymous volumeに変更して
本項で実施したHTMLファイル作成の手順を繰り返してみます

`docker-compose.yml`

```yml
version: "3.8"

services:
  web:
    image: nginx:1.18
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/static.conf:/etc/nginx/conf.d/default.conf
      - public:/myapp/public
      - log:/var/log/nginx
      - /var/www/html # コンテナ側のpathのみ指定しanonymous volumeに変更

...

volumes:
  public:
  log:
  # html:　ここをコメントアウト
```

Nginxをweb serverとして起動

```sh
docker-compose run --no-deps -d -p 80:80 web
```

シェルを呼び出します

```sh
docker-compose run --no-deps web bash
```

ここが重要なのですが、別のターミナルでいま起動しているコンテナを確認すると

```sh
docker ps
```

**２つのコンテナが起動しており、シェルが動いているコンテナは、80:80でポートマッピングしているコンテナとは別であることがわかります**

このままコンテナ内でHTMLを作成

```sh
# index.htmlを作成
touch /var/www/html/index.html

# index.htmlの存在を確認
ls /var/www/html
```

さきほどと同様にブラウザから`localhost`にアクセスしてみましょう

するとブラウザは403エラーを示し
Nginxのエラーログを確認すると

```sh
tail -f 20 /var/log/nginx/error.log
```

```sh
...directory index of "/var/www/html/" is forbidden...
```

ディレクトリを見つけられないとエラーが記録されています

named volume -> anonymous volumeに変更したことで
**2つのコンテナ間で`/var/www/html/`以下の内容が共有されなくなり**
ローカルからport 80でリクエストを受けたコンテナからは`index.html`を参照することができなくなったことで
このようなエラーが生じていると考えられます

**永続化はするが、他のコンテナとボリュームを共有しない**、その特性に触れることができたかと思います

確認できたら`exit`でシェルを閉じ

毎度ですがコンテナを終了させておきましょう

```sh
docker-compose down
```

（変更したdocker-compose.ymlの内容はこのままでも構いません）

...


appendixの内容に思ったよりも熱が入ってしまい長くなったので、（私のモチベーション維持のために）一旦ここで区切ります

②に続く

---