課題
本番はブラウザ→Nginx→バックエンドという構成を予定しているが、開発環境はNginxなしでアプリケーションコンテナへ直接アクセスしている。本番構築フェーズで初めてNginx設定を書いて動かすのはリスクが高いが、開発環境の日常フローをNginx前提に変えると既存の手順が壊れる。
この記事では、既存のdocker-compose.ymlに手を入れずに、Nginxのリバースプロキシ構成だけをオプトインで事前検証できるようにした実装パターンと、実際に踏んだハマりどころを紹介する。
全体構成
-
nginxはDocker Composeのprofiles機能で「明示的に指定したときだけ起動する」検証専用サービスにした -
apiはfrontend-netに参加させず、ブラウザ経路からは名前解決すらできないようにした(IPベースのallow/denyより壊れにくいアクセス制御)
services:
nginx:
profiles: ["nginx-verify"] # 通常の `docker compose up -d` では起動しない
networks: [frontend-net, backend-net]
depends_on: [web-ui, api] # 依存の向きに注意(後述)
web-ui:
networks: [frontend-net, backend-net]
api:
networks: [backend-net] # frontend-netには入れない
networks:
frontend-net:
backend-net:
- 通常起動(
docker compose up -d)ではNginxは作られず、今まで通り - 検証時だけ
docker compose --profile nginx-verify up -d
依存の向きに注意: profile指定ありのサービス(nginx)がprofile指定なしのサービスに依存するのは問題ないが、逆(profile指定なしがprofile指定ありに依存)は通常起動時にエラーになる。depends_onはNginx側にだけ書く。
なお、dbへの接続が複数コンテナから必要な都合上、web-uiとapiを完全に別ネットワークへ分けて相互到達を禁止するところまでは実現できていない。「バックエンドの入口をNginx経由の相手だけに絞る」という目的自体は満たせているが、何を守りたいかによって分割の粒度は調整が必要。
ハマりどころ: proxy_passの静的名前解決
一見自然だが問題のある書き方:
location / {
proxy_pass http://api:8000;
}
Nginxはホスト名を直接書くと起動時に一度だけDNS解決し、以降そのIPを使い続ける。このためDocker Compose環境では、
-
apiコンテナがまだ無い状態だとnginx -t自体が失敗する -
apiコンテナが再作成されIPが変わっても、Nginxは古いIPへ送り続ける(再起動するまで気づけない)
実機で単体検証すると、実際に設定チェックから失敗した。
nginx: [emerg] host not found in upstream "api" in /etc/nginx/nginx.conf:17
対処はresolverにDockerのDNS(127.0.0.11)を指定し、proxy_passの宛先を変数化してリクエストごとに動的解決させること。
resolver 127.0.0.11 valid=10s;
location / {
set $upstream_api http://api:8000;
proxy_pass $upstream_api;
}
修正後は、apiが未起動でも設定チェックが通り、コンテナ再作成でIPが変わってもvalidで指定した秒数ごとに再解決されて疎通が維持されることを確認した。
まとめ
-
profilesで既存の開発フローを変えずに「オプトインの検証パス」を追加できる - ネットワーク分割はIPアドレスに依存しないアクセス制御になるが、共有リソース(DB等)があると完全分離は難しいことがある
-
proxy_passへのホスト名直書きは、起動時1回だけの名前解決という落とし穴がある。resolver+変数化で再作成に強くできる
TLS証明書や実ホスト名など本番でしか検証できない要素は残るが、ルーティング・アクセス制御といった「ロジックの部分」は、この構成で本番投入前にかなり潰しておける。