とりあえずのdocker-compose upから入って、Web serverの基礎設計を学びながらDockerを学ぶ①
はじめに
以下のような方とって有益な内容になればと思っています
- これから初めてのwebアプリを作成する
- webアプリを作成し、これからデプロイする
- 初めてデプロイまで到達したが、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は補足的内容となっています
その項で知ることのできる内容を初めに書いておきましたので、
改めて知る必要のない内容でしたら読み飛ばして頂いて結構です
もし知らない内容でしたら、実際に手を動かして頭の片隅に留めておくことで、後々役に立つ物があるかもしれません
必要なもの、スキル
-
エディタ(VS Codeで検証しています)
Visual Studio Code - Code Editing. Redefined -
Linuxの基礎コマンドの知識(cd, ls, vi...くらい、なくてもコピペでなんとかなります)
-
Docker desktop
Docker Desktop for Mac and Windows | Docker -
Dockerって何?っていう方は以下がおすすめです
【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その1:コンテナってなに? 〜 | SIOS Tech. Lab
概念をさらっと理解していただき、ここでは手を動かしてみるというのがおすすめです
アプリの部分は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まで)がすぐに構築できます!
少しづつ改善していますので、改善コメントもお待ちしております。
上記をベースに今回の記事のために用意したソースコード
ソースコードを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/test
はdocker-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におけるボリュームには以下のタイプがありますが、コンテナ内のデータを永続化するという点では同じです
- host volume ?(ちょっと名前がわからないです)
- anonymous volume (匿名ボリューム?anonymous volumeで通っている気がします)
- 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