Nginx Using Docker - シリーズ投稿3/7
Series Top: Dockerで作るおうちLAN遊び場
この投稿はシリーズ第三弾となり、今回はリバースプロキシとして用いるwebサーバをNginxで用意し、各サービスをDNS名で指してアクセスできるようにします。
Port 80 http access to service running on docker container
前回の投稿でカバーした通り、Jupyter NotebookサービスをDockerで走らせるのはとても簡単です。ポート番号としてはJupyter Notebookのデフォルトである8888を使いましたが、例えば以下のように、ホストマシンの80番ポートとコンテナの8888番ポートをつなげることも可能です。
services:
jupyter:
container_name: jupyter
image: jupyter/base-notebook:notebook-6.5.1
user: root
ports:
- "80:8888"
command: "start-notebook.sh --ServerApp.password='' --ServerApp.token='' --ip=0.0.0.0 --no-browser"
environment:
- "CHOWN_EXTRA=/home/jovyan"
- "CHOWN_EXTRA_OPTS=-R"
volumes:
- type: volume
source: jupyter_volume
target: /home/jovyan
volume:
nocopy: true
volumes:
jupyter_volume: {}
こうすることで、ブラウザでアクセスする際に:8888
を省いてhttp://jupyter.mylan.local
でアクセスできるようになります。
このやり方はこのやり方でOKではあるものの、他のサービスも立ち上げた時、Jupyter Notebookと同様に80番ポートでアクセスさせることはできなくなります。つまり、例えば55555番ポートでサービス提供するwebサービスを追加でDockerで走らせた時、このコンテナにもホストマシンの80番ポートでつなぐということができません。ホストマシンの80番ポートはすでにJupyter Notebookコンテナの8888番ポートへつなぐよう専有されているからです。
異なるサービスを次々とDockerコンテナとして実行させるのは簡単なのに、80番ポートを利用したコンテナへのアクセスにはこんな制限があります。どうすればよいでしょうか。
Reverse proxy
上の問題に対してはwebサーバを設けて、そこから特定のサービスに向けた通信を適切なDockerコンテナへ流すということをすれば解消できます。
仮にjupyter
コンテナは実行させつつも、別の用途でjupyter2
というコンテナで同じくJupyter Notebookを実行、そしてblog
というコンテナでghost
ブログサービスを実行させるとします。この場合でも、まず先頭にwebサーバを設け、受け取るhttpリクエスト内の宛先名に応じて通信の流し先を変えるよう構築することができます。
図にすると以下の通りとなります。webサービス3つが一番右側にあり、その手前に中継点としてリバースプロキシがあります。DNSも加えると合計5つのコンテナを走らせることになります。
では実際に手を動かしていきましょう。リバースプロキシ用のディレクトリとそのコンフィグファイルを入れるディレクトリを用意します。
mkdir -p $HOME/mylan/rp/conf.d
cd $HOME/mylan/rp
Dockerとして動かすdocker-compose.yml
ファイルの中身は以下の通りです。
services:
rp:
container_name: rp
image: nginx:1.23.2
ports:
- "80:80"
volumes:
- ./conf.d:/etc/nginx/conf.d
Unbound DNSでやったように、リバースプロキシとして利用するこのNginxウェブサーバにコンフィルを追加していきます。yml
ファイルで示している通り、ホストマシンの./conf.d
にコンフィグファイルを用意して、それらをコンテナ内でNginxが設定として読み込む/etc/nginx/conf.d
に積みたいわけです。
そしてjupyter.mylan.local
宛のhttpリクエストを扱うサーバ設定を以下の内容でjupyter.conf
ファイルとして$HOME/mylan/rp/conf.d
に用意します。
以下のコンフィグファイルにある通り、8888番ポートへ渡すよう設定しているので、最初に作ったjupyter
コンテナも、もしホストマシンの80番ポートに繋ぐよう変更してしまった場合は、ホストマシンの8888番ポートでアクセスできるよう戻しておきましょう。
# jupyter.conf
server {
listen 80;
server_name jupyter.mylan.local;
# docker resolver
resolver 127.0.0.11 valid=30s;
location / {
set $upstream 192.168.1.56:8888;
proxy_pass http://$upstream;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
それでは準備ができたのでdocker compose up -d
で実行させます。サービスが立ち上がると、ブラウザでhttp://jupyter.mylan.local
にアクセスすることでJupyter Notebookに接続できます。
❯ curl -v http://jupyter.mylan.local/
* Trying 192.168.1.56:80...
* Connected to jupyter.mylan.local (192.168.1.56) port 80 (#0)
> GET / HTTP/1.1
> Host: jupyter.mylan.local
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.23.2
Another Jupyter Notebook
それではもう一つ、jupyter2.conf
としてhttp://jupyter2.mylan.local
宛の通信に対処するサーバ設定を設けましょう。通信流し先のポートは8889番としておきます。
# jupyter2.conf
server {
listen 80;
server_name jupyter2.mylan.local;
# docker resolver
resolver 127.0.0.11 valid=30s;
location / {
set $upstream 192.168.1.56:8889;
proxy_pass http://$upstream;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
ファイルが用意できたら早速docker compose restart
でリスタートしてしまいましょう。問題なくNginxはコンフィグを読み込み実行します。このコンテナでnginx -T
コマンドを実行することでNginxサーバが設定を読み込んでいることを確認できます。
$ docker exec rp nginx -T
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user nginx;
worker_processes auto;
--- rest is omitted for brevity ---
ただもちろんDNSレコードの用意もまだですし、2つ目のJupyter Notebookコンテナも走らせていないので、ブラウザからのアクセスはまだ失敗します。
仮にDNSレコードがjupyter2.mylan.local
分もすでにできている場合の出力は以下です。
# access will fail even if the name resolution is already there
❯ curl -v http://jupyter2.mylan.local/
* Trying 192.168.1.56:80...
* Connected to jupyter2.mylan.local (192.168.1.56) port 80 (#0)
> GET / HTTP/1.1
> Host: jupyter2.mylan.local
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 502 Bad Gateway
それでは$HOME/mylan/jupyter/docker-compose.yml
ファイルに戻り、2つ目のJupyter Notebookを追加、実行しましょう。2つ目は簡単に、ボリューム追加などもせずただただそのまま立ち上げさせましょう。仮に2つ目のJupyter Notebookでファイルを作っても、コンテナ立ち上げ直しの度にデータはなくなります。このシリーズとしては今回限りの例として追加するコンテナなので良しとしましょう。
services:
jupyter:
container_name: jupyter
image: jupyter/base-notebook:notebook-6.5.1
user: root
ports:
- "8888:8888"
command: "start-notebook.sh --ServerApp.password='' --ServerApp.token='' --ip=0.0.0.0 --no-browser"
environment:
- "CHOWN_EXTRA=/home/jovyan"
- "CHOWN_EXTRA_OPTS=-R"
volumes:
- type: volume
source: jupyter_volume
target: /home/jovyan
volume:
nocopy: true
jupyter2:
container_name: jupyter2
image: jupyter/base-notebook:notebook-6.5.1
ports:
- "8889:8888"
command: "start-notebook.sh --ServerApp.password='' --ServerApp.token='' --ip=0.0.0.0 --no-browser"
volumes:
jupyter_volume: {}
こうして、1つ目は8888番ポート、2つ目のJupyter Notebookは8889番ポートでアクセスできるようになります。
$ docker compose up -d
[+] Running 2/2
⠿ Container jupyter2 Started 0.7s
⠿ Container jupyter Running 0.0s
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
jupyter "tini -g -- start-no…" jupyter running (healthy) 0.0.0.0:8888->8888/tcp, :::8888->8888/tcp
jupyter2 "tini -g -- start-no…" jupyter2 running (healthy) 0.0.0.0:8889->8888/tcp, :::8889->8888/tcp
DNS records for the second Jupyter Notebook and Ghost
DNSレコードの準備もしていきます。jupyter2
の分と、ついでにblog
の分も$HOME/mylan/dns/config/a-records.conf
内に追加していきます。追加できたらDNSのコンテナをリスタートします($HOME/mylan/dns
ディレクトリでdocker compose restart
実行)。
$ cat dns/config/a-records.conf
# A Record
#local-data: "somecomputer.local. A 192.168.1.1"
local-data: "jupyter.mylan.local. A 192.168.1.56"
local-data: "jupyter2.mylan.local. A 192.168.1.56"
local-data: "blog.mylan.local. A 192.168.1.56"
# PTR Record
#local-data-ptr: "192.168.1.1 somecomputer.local."
local-data-ptr: "192.168.1.56 jupyter.mylan.local."
webサーバは先程用意したのでhttp://jupyter2.mylan.local
で2つ目のJupyter Notebookにアクセスできるようになっています。
❯ curl -v http://jupyter2.mylan.local/
* Trying 192.168.1.56:80...
* Connected to jupyter2.mylan.local (192.168.1.55) port 80 (#0)
> GET / HTTP/1.1
> Host: jupyter2.mylan.local
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
なお、$HOME/mylan/rp
ディレクトリに移動してdocker compose logs
を実行するとNginxのアクセスログが確認できます。
Ghost blog
続いて3つ目のコンテナ、ブログサービス立ち上げに、$HOME/mylan/ghost
ディレクトリと必要なファイルを用意します。2つ目のJupyter Notebook同様、シリーズ通して今回限りの例として実行させるサービスですが、ぜひサービスを走らせて試してください。
docker-compose.yml
ファイルは以下の通りで、ほぼDocker Hub上のGhostのページにあるサンプル通りです。ここの例で変更している点はこれらです:
- host port - 2368
- url - http://blog.mylan.local
- NODE_ENV: development line uncommented
services:
ghost:
image: ghost:4-alpine
restart: always
ports:
- 2368:2368
environment:
# see https://ghost.org/docs/config/#configuration-options
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: example
database__connection__database: ghost
# this url value is just an example, and is likely wrong for your environment!
url: http://blog.mylan.local
# contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
NODE_ENV: development
db:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
早速実行しましょう。
$ docker compose up -d
[+] Running 22/22
⠿ db Pulled 17.6s
--- omitted ---
⠿ ghost Pulled 31.7s
--- omitted ---
[+] Running 3/3
⠿ Network ghost_default Created 0.2s
⠿ Container ghost-db-1 Started 1.6s
⠿ Container ghost-ghost-1 Started 1.5s
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
ghost-db-1 "docker-entrypoint.s…" db running 3306/tcp, 33060/tcp
ghost-ghost-1 "docker-entrypoint.s…" ghost running 0.0.0.0:2368->2368/tcp, :::2368->2368/tcp
docker-compose.yml
内の記載および上の出力の通り、Ghostブログサービスは2368番ポートでサービス提供しています。それではブログサービス用のwebサーバコンフィグファイルを$HOME/mylan/rp/conf.d/blog.conf
として用意しましょう。通信流し込み先のポート番号は2368です。
$ cat rp/conf.d/blog.conf
server {
listen 80;
server_name blog.mylan.local;
# docker resolver
resolver 127.0.0.11 valid=30s;
location / {
set $upstream 192.168.1.56:2368;
proxy_pass http://$upstream;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
例のごとく以下はただのcurl
コマンドでアクセスした際の出力ですが、実際にブラウザでhttp://blog.mylan.local
を開くとカッコイイGhostブログのデフォルトのサイトにアクセスできます。
❯ curl -v http://blog.mylan.local/
* Trying 192.168.1.56:80...
* Connected to blog.mylan.local (192.168.1.56) port 80 (#0)
> GET / HTTP/1.1
> Host: blog.mylan.local
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
Some notes on ghost docker-compose.yml and docker network
Jupyter Notebookで2つ目のサービスコンテナを追加したように、一つのdocker-compose.yml
ファイルで複数のサービスを実行できます。
また、Ghostブログ用に用意したdocker-compose.yml
を見てみると環境変数database__connection__host
の値はdb
とされています。このdb
は同ファイル内のGhostではない2つ目の、mysql:8.0
イメージで実行するサービスの名前です。
つまりghost
コンテナが走り、このコンテナはdb
という名前のデータベースに接続するようになっています。この2つのコンテナ間ですが、お互いにghost
、db
という名前で名前解決、通信できるようになっています。docker-compose.yml
ファイル上ではdb
に関してports
の設定はなく、つまりホストマシン上のポートからはdb
コンテナにアクセスできるようになっていないのですが、ghost
コンテナにとってはdb
コンテナは同一Dockerネットワーク上にいて互いにアクセスできるようになっています。
services:
ghost:
image: ghost:4-alpine
restart: always
ports:
- 2368:2368
environment:
database__client: mysql
# here the database to use is set to "db"
# which is the mysql:8.0 database container
# defined in this same file
database__connection__host: db
database__connection__user: root
database__connection__password: example
database__connection__database: ghost
url: http://blog.mylan.local
NODE_ENV: development
db:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
# no ports section
# ports:
# - 1234:5678
またその他、これまでの他のdocker-compose.yml
ファイルと異なる点として、コンテナ名が指定されていません。従ってdocker compose up -d
を実行して作られるコンテナ名はghost-db-1
とghost-ghost-1
となっています。もちろんcontainer_name
行を追加して指定の名前でコンテナを作るようにすることもできます。
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
ghost-db-1 "docker-entrypoint.s…" db running 3306/tcp, 33060/tcp
ghost-ghost-1 "docker-entrypoint.s…" ghost running 0.0.0.0:2368->2368/tcp, :::2368->2368/tcp
And a little bit more on docker network ...
前回の投稿で少し触れていますが、docker container inspect コンテナ名
でコンテナに関する詳細情報が確認できます。そこでghost
, db
, rp
コンテナのネットワーク情報を見てみると、最初の2つはghost_default
というネットワーク上におり、ghost
は192.168.160.3/20
, db
は192.168.160.2/20
というIPアドレスで動いています。一方rp
コンテナの情報を見るとrp_default
ネットワーク上で192.168.144.2/20
というIPアドレスで動いているのがわかります。
$ docker container inspect ghost-ghost-1 -f '{{ json .NetworkSettings.Networks }}' | python3 -m json.tool
{
"ghost_default": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"ghost-ghost-1",
"ghost",
"537d3c9e7019"
],
"NetworkID": "19471798da39dafa7200ad3dd763d2cbb3ffd9cb4d85301f973c9a10764294a5",
"EndpointID": "6b85e1569fa020bf9d7a2786281bee71146223f057c6723f4ee27ba602a3e82f",
"Gateway": "192.168.160.1",
"IPAddress": "192.168.160.3",
"IPPrefixLen": 20,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:c0:a8:a0:03",
"DriverOpts": null
}
}
$ docker container inspect ghost-db-1 -f '{{ json .NetworkSettings.Networks }}' | python3 -m json.tool
{
"ghost_default": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"ghost-db-1",
"db",
"3af25adfee9a"
],
"NetworkID": "19471798da39dafa7200ad3dd763d2cbb3ffd9cb4d85301f973c9a10764294a5",
"EndpointID": "b125f53ef96ecffb49b47d2f20e65238d7dde95eba1817dd9a3973cb733ce2d1",
"Gateway": "192.168.160.1",
"IPAddress": "192.168.160.2",
"IPPrefixLen": 20,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:c0:a8:a0:02",
"DriverOpts": null
}
}
$ docker container inspect rp -f '{{ json .NetworkSettings.Networks }}' | python3 -m json.tool
{
"rp_default": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"rp",
"rp",
"1bc59a14613e"
],
"NetworkID": "56f10f4877b46d50d728b0353acc7a933a85e7320825173d277f51a249d22bdd",
"EndpointID": "376da992e7a9d19581f8d25ff805820f83cf715abf9862f083a4eba6ceda4100",
"Gateway": "192.168.144.1",
"IPAddress": "192.168.144.2",
"IPPrefixLen": 20,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:c0:a8:90:02",
"DriverOpts": null
}
}
これらのDockerコンテナ、ネットワークを図示してみるとこのような感じです。
Discussion