サブドメインと複数のWebアプリをつなげるプロキシサーバをnginxとDcokerで構築する

インフラ

前回、独自ドメインとローカルのWebアプリをつなげるプロキシサーバをnginxとDcokerで構築するでnginxを利用して独自ドメインでコンテナに接続する方法について紹介しました。

前回は接続対象のアプリケーションが1つだけでしたが、今回はサブドメインを利用して複数のアプリケーションに対応する方法について紹介します。
なお、localhost:3000でWebアプリケーションが、localhost:3001でAPIサーバが起動している前提とします。

今回は例としてhttp://web.local.example-dev.comにWebアプリケーション、http://api.local.example-dev.comでAPIサーバに接続することをゴールとします。

下準備

ローカル環境の/etc/hostsを編集する

localhost127.0.0.1を指すホスト名です。
任意のドメインでローカル環境を参照するには/etc/hosts127.0.0.1にドメインをマッピングします。

/etc/hosts

# 以下を追加する
127.0.0.1   web.local.example-dev.com
127.0.0.1   api.local.example-dev.com

WebアプリをRailsで実装している場合: ドメインによるアクセスをアプリケーション側で許可する

Rails 6以降1、DNSリバインディング攻撃からの保護のためデフォルトでは.localhost0.0.0.0::からのアクセスのみ許可されています。

ですので、設定ファイルにconfig.hosts << "web.local.example-dev.com;"config.hosts.clearを追加してドメイン名でコンテナへアクセスできるようにします。
設定をしないと[ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked host: web.local.example-dev.com;というエラーが発生します。

詳細解説はlocalhost以外のホスト名でローカル環境のRailsに接続する方法で紹介しています。

docker-compose.ymlの作成

nginxコンテナを起動するdocker-compose.ymlを作成します。

docker-compose.yml

version: '3'
services:
  nginx:
    image: nginx:1.21
    ports:
      - '80:80'
    command: [nginx-debug, '-g', 'daemon off;']
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf

volumesで指定した設定ファイルについて補足説明をします。
nginxの設定ファイルは/etc/nginx/nginx.confです。設定ファイルにはinclude /etc/nginx/conf.d/*.conf;という記述があり、/etc/nginx/conf.d/配下の.confという拡張子の設定が読み込まれます。

今回はnginxの/etc/nginx/conf.d/ディレクトリとローカルディレクトリをバインドマウントさせて設定ファイルを配置します。

nginxの設定ファイルの修正

今回は以下の2つの方法について紹介します。

  • 各ドメインに応じたserverブロックを用意する
  • サブドメインに応じて動的にプロキシ先を変更する

方法1: 各ドメインに応じたserverブロックを用意する

server_nameディレクティブにドメイン名(サブドメインを含む)を記述し、各ドメインに応じたserverブロックを用意する方法です。

nginxの設定ファイルは以下のようになります。

nginx/conf.d/default.conf

### web.local.example-dev.comに関するリクエストの処理
server {
  listen 80;

  ### web.local.example-dev.comのリクエストを受け取る
  server_name web.local.example-dev.com;

  ### "/"にアクセスがあったときの処理
  location / {

    ### リクエストヘッダに$hostをセット
    # $host: Hostリクエストヘッダがあればそのサーバ名、なければプライマリサーバ名(server_name)
    proxy_set_header Host $host;

    ### ローカル環境(host.docker.internal)の3000番ポートへプロキシする
    proxy_pass http://host.docker.internal:3000;
  }
}

### api.local.example-dev.comに関するリクエストの処理
server {
  listen 80;

  ### api.local.example-dev.comのリクエストを受け取る
  server_name api.local.example-dev.com;

  ### "/"にアクセスがあったときの処理
  location / {

    ### リクエストヘッダに$hostをセット
    # $host: Hostリクエストヘッダがあればそのサーバ名、なければプライマリサーバ名(server_name)
    proxy_set_header Host $host;

    ### ローカル環境(host.docker.internal)の3001番ポートへプロキシする
    proxy_pass http://host.docker.internal:3001;
  }
}

たとえば、web.local.example-dev.comからのリクエストはserver_name web.local.example-dev.com;のserverブロックで受け取られ、host.docker.internalを利用してコンテナからローカル環境の3000番ポートへプロキシされます。
ローカル環境の3000番ポートではWebアプリケーションが起動しているため、web.local.example-dev.comはローカル環境のWebアプリケーションを指すことになります。

host.docker.internalはDocker Desktopで提供されている、コンテナからローカル環境を参照する際のホスト名です。

方法2: サブドメインに応じて動的にプロキシ先を変更する

さきほど紹介した『方法1』をよりスマートにした方法です。

サブドメインとポート番号の対応をmapディレクティブを利用してマッピングし、プロキシするローカル環境のURLを動的に変更する方法です。
server_nameディレクティブは共通するドメイン部分のみ記述します。

nginxの設定ファイルは以下のようになります。

nginx/conf.d/default.conf

### $http_host(リクエストヘッダのHTTP_HOSTの値)の評価に応じて$port変数の値を変更する
map  $http_host $port {
  ### サブドメインの文字列を評価して$portに値をセット
  # ~: 大文字・小文字を区別する正規表現
  # デフォルト値を用意したい場合は『default xxx』を定義
  ~web 3000;
  ~api 3001;
}

server {
  listen 80;
  server_name ~local.example-dev.com;

  ### "/"にアクセスがあったときの処理
  location / {

    ### コンテナのDNSサーバ
    resolver 127.0.0.11;

    ### リクエストヘッダに$hostをセット
    # $host: Hostリクエストヘッダがあればそのサーバ名、なければプライマリサーバ名(server_name)
    proxy_set_header Host $host;

    ### ローカル環境(host.docker.internal)へリクエスト。ポート番号はサブドメインに応じて動的に変更する
    proxy_pass "http://host.docker.internal:$port";
  }
}

上記の設定ファイルによってweb.local.example-dev.comからのリクエストはhttp://host.docker.internal:3000に、api.local.example-dev.comからのリクエストはhttp://host.docker.internal:3001にプロキシされます。

参考:『resolver 127.0.0.11;』の役割と必要性について

プロキシ先を動的に変更する場合はresolverディレクティブでDNSサーバ(ネームサーバ)を指定する必要があります。
resolverディレクティブの記述がないと名前解決ができないためno resolver defined to resolve host.docker.internalというエラーが発生してしまいます。
127.0.0.11はコンテナで利用されているDNSサーバのIPアドレスです。IPアドレスはdigコマンドで確認できます。

[docker]
$ dig localhost;

(略)
;; SERVER: 127.0.0.11#53(127.0.0.11)
(略)

参考: マッピング情報を別ファイルに切り出す方法

nginxはincludeを利用することで外部ファイルの設定を読み込めます。これを活用することでマッピング情報を切り出せます。

nginx/conf.d/default.conf

map $http_host $port {
  include /etc/nginx/conf.d/domain_list;
}

server {
  listen 80;
  server_name ~local.example-dev.com;

  location / {
    resolver 127.0.0.11;

    proxy_set_header Host $host;
    proxy_pass "http://host.docker.internal:$port";
  }
}

nginx/conf.d/domain_list

~web 3000;
~api 3001;

ファイルごとにバインドマウントしている場合はdocker-compose.ymlも修正します。

docker-compose.yml

version: '3'
services:
  nginx:
    (略)
    volumes:
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
+     - ./nginx/conf.d/domain_list:/etc/nginx/conf.d/domain_list

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料