🐳

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

2020/09/28に公開

はじめに

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

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

必要なもの、スキル

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

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

アーキテクチャ(設計)概要

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

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

よく見る構成です(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://github.com/naokit-dev/try_nginx_on_docker.git

ソースコードをgit clone

#アプリを配置するディレクトリを作成(アプリケーションルート)
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 

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

.(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つのコンテナが定義されています

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:

環境構築!

source setup.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環境)

docker ps

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

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

docker-compose down 

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

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

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

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

  • Nginx単独で単純な静的コンテンツ(HTML)を配信してみる

最もシンプルなNginxの設定

Nginxの設定を変更するためdocker-compose.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の設定が記述されており、中身は以下のようになっています

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: neginxのみ)
-d: バックグラウンドでコンテナを起動、シェルは入力を継続できます
-p: ポートマッピング<host>:<container>
web: composeで定義されたnginxコンテナです

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

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

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

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

docker ps

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

HTMLコンテンツを作成

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

docker-compose run --no-deps web bash

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

# 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として配信されているのが確認できていると思います

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

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

docker-compose down 

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

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

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

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

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

server_name undefined_server;

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

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

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

listen      80 default_server;

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

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

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

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

docker-compose down 

Appendix - Dockerのvolumeを少し理解する

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

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

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

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

docker-compose run --no-deps web bash

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

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

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

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

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

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

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

docker-compose run --no-deps web bash

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

cat /var/www/test/index.html
ls /var/www

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

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

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

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

docker-compose down

volumeの種類

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

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

docker-compose.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ファイル作成の手順を繰り返してみます

dokcer-compose.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として起動

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

シェルを呼び出します

docker-compose run --no-deps web bash

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

docker ps 

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

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

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

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

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

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

tail -f 20 /var/log/nginx/error.log
...directory index of "/var/www/html/" is forbidden...

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

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

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

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

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

docker-compose down 

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

...


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

②に続く

Discussion