🌊

Nginx Using Docker - シリーズ投稿3/7

2022/12/26に公開

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リクエスト内の宛先名に応じて通信の流し先を変えるよう構築することができます。

https://hub.docker.com/_/nginx

https://hub.docker.com/_/ghost

図にすると以下の通りとなります。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

https://hub.docker.com/_/nginx

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のページにあるサンプル通りです。ここの例で変更している点はこれらです:

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

https://hub.docker.com/_/ghost

早速実行しましょう。

$ 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つのコンテナ間ですが、お互いにghostdbという名前で名前解決、通信できるようになっています。docker-compose.ymlファイル上ではdbに関してportsの設定はなく、つまりホストマシン上のポートからはdbコンテナにアクセスできるようになっていないのですが、ghostコンテナにとってはdbコンテナは同一Dockerネットワーク上にいて互いにアクセスできるようになっています。

https://docs.docker.com/compose/networking/

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-1ghost-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というネットワーク上におり、ghost192.168.160.3/20, db192.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コンテナ、ネットワークを図示してみるとこのような感じです。

next: Enabling https access on Nginx reverse proxy

Discussion