はじめに
ExpressのAPIをコンテナで起動できたら、次はその手前にNginxを置いてみましょう。
ブラウザからNginxへアクセスし、NginxがExpressへリクエストを転送する構成を作ると、リバースプロキシの役割を具体的に理解できます。
この記事は、Docker ComposeでExpress APIとPostgreSQLを同時に起動する方法の続きです。前の記事で作成したAPIとDBに、Nginxを追加します。
今回の到達点は、次の3つです。
- APIへ直接アクセスする場合と、Nginx経由でアクセスする場合を比較する
proxy_passで、Composeのサービス名を使って転送する- 設定の検査・再読み込みと、接続できない場合の確認方法を覚える
参考PDFとの範囲について: リバースプロキシの役割と`location`・`proxy_pass`の概要は、参考PDFのp.1・10をもとに整理しています。第3節以降のExpress+Docker Composeの実習、具体的な設定、接続確認、URIの比較、エラー対処は、公式資料を確認して追加したPDF外の補足です。
1. リバースプロキシとは
リバースプロキシは、クライアントからリクエストを受け取り、バックエンドのサーバーへ転送し、その応答をクライアントへ返す仕組みです。
今回、クライアントはブラウザやcurl、バックエンドはExpressのAPIです。
| 段階 | リクエスト・応答の流れ |
|---|---|
| 1 | ブラウザがNginxへリクエストを送る |
| 2 | NginxがExpressへリクエストを転送する |
| 3 | Expressが`Hello, World!`を返す |
| 4 | Nginxがその応答をブラウザへ返す |
ブラウザがアクセスするURLはNginx側のURLです。HTTPリダイレクトでブラウザをExpressのURLへ移動させる動作とは異なります。NGINX Documentation
静的ファイルの配信との違い
| 使い方 | 応答の内容を用意する場所 | 主に使う設定 |
|---|---|---|
| 静的ファイルの配信 | Nginxが読み取るHTMLなどのファイル | root・`index` |
| HTTPリバースプロキシ | バックエンドのWebアプリ | proxy_pass |
今回は、Expressが返す内容をNginxが中継します。Nginx用の`index.html`は作成しません。
2. proxy_passの基本
NginxでHTTPリクエストを転送する場合は、`location`の中へ`proxy_pass`を記述します。
location / {
proxy_pass http://api:3000;
}
この例では、対象のリクエストを`api`というホスト名の3000番ポートへ転送します。
apiは、この後の実習で使うComposeのサービス名です。同じComposeネットワークにいるコンテナ同士は、サービス名で接続できます。Docker Docs
なお、`proxy_pass`はHTTP・HTTPSのバックエンドへの転送に使用します。PDFにあるFastCGIやuwsgiとの連携は別の通信方式で、対応する設定も異なります。今回はHTTPで応答するExpressを扱います。
3. 実習の前提と構成【PDF外の実習】
前の記事で作成した`docker-express-postgres`ディレクトリを使います。
| ファイル | 今回の扱い |
|---|---|
package.json | 前の記事のExpress依存定義を使用する |
app.js | 前の記事のAPIを使用する |
Dockerfile | 前の記事のAPI用Dockerfileを使用する |
compose.yaml | Nginxを含む構成へ変更する |
nginx/default.conf | 今回追加するNginxのサイト設定 |
前の記事をまだ実施していない場合は、先にExpress APIとPostgreSQLの構築を進めてください。
今回も、APIからPostgreSQLへ接続する処理は追加しません。DBは前の記事の構成を引き継ぎます。
ポートの対応
| 接続元と接続先 | 使用するアドレス |
|---|---|
| PCからAPIへ直接アクセス | http://127.0.0.1:3000/ |
| PCからNginxへアクセス | http://127.0.0.1:8080/ |
| NginxコンテナからAPIへ接続 | http://api:3000/ |
PCの3000番・8080番ポートが空いていることを確認してください。Ubuntuに直接インストールしたNginxなどが8080番を使用している場合も競合します。
今回はローカル実習のため、PC側の公開先を`127.0.0.1`に限定します。Dockerが手元のPC上で動く環境を前提とし、リモートのDockerホストは対象にしません。
既存の設定を退避する
以降のファイル操作は、macOS・Linux・WSLなどのシェルを想定しています。エディターでのファイル作成・コピーでも構いません。
作業ディレクトリへ移動し、現在の構成を停止します。
cd docker-express-postgres
docker compose down
すでにこのディレクトリにいる場合、`cd`は不要です。`down`では、前の記事の名前付きボリューム`pgdata`は残ります。
変更前のComposeファイルをコピーします。すでに同名の退避ファイルがある場合は、別名にしてください。
cp compose.yaml compose.before-proxy.yaml
mkdir -p nginx
以降も同じディレクトリ・Composeプロジェクトで作業します。
4. Expressの待受設定を確認する
前の記事の`app.js`は、次の内容です。
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, '0.0.0.0', () => {
console.log(`API is listening on port ${port}`);
});
コンテナのネットワーク経由で接続を受け付けるため、待受アドレスには`0.0.0.0`を指定しています。
APIコンテナ内の`127.0.0.1`だけで待ち受けると、別コンテナのNginxから接続できません。
5. Nginxの設定ファイルを作成する
エディターで`nginx/default.conf`を作成し、次の内容を保存します。
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://api:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
| 設定 | 今回の役割 |
|---|---|
listen 80; | Nginxコンテナ内の80番ポートで受け付ける |
server_name localhost; | サイト名を指定する。今回はこの待受先にサイト設定が1つだけある |
location / | /から始まるパスを処理する |
proxy_pass http://api:3000; | APIサービスの3000番ポートへ転送する |
proxy_http_version 1.1; | バックエンドへの通信にHTTP/1.1を指定する |
proxy_set_header Host $host; | リクエストのホスト名をHostヘッダーに設定して転送する |
今回の`Host`設定は、この小さなAPIの動作確認には必須ではありません。バックエンドへ渡すヘッダーをNginx側で指定できることを示しています。NGINX Documentation
server_nameは接続元を制限する設定ではありません。今回のPC側の接続範囲は、次のComposeファイルの`ports`で指定します。
localhostではなくapiを指定する理由
Nginxコンテナ内の`localhost`や`127.0.0.1`は、Nginxコンテナ自身を指します。
proxy_pass http://localhost:3000;
このように書くと、Nginxコンテナ自身の3000番へ接続しようとします。今回のExpressは別コンテナなので、転送先には`api:3000`を指定します。
PCからアクセスするときのアドレスと、コンテナ同士で使うアドレスを分けて考えてください。
6. ComposeにNginxを追加する
compose.yamlを、次の内容に変更します。
services:
nginx:
image: nginx:stable-alpine
ports:
- "127.0.0.1:8080:80"
volumes:
- ./nginx:/etc/nginx/conf.d:ro
depends_on:
- api
api:
build: .
ports:
- "127.0.0.1:3000:3000"
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: examplepassword
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
POSTGRES_PASSWORDは、前の記事と同じローカル実習用の値です。
設定ファイルのマウント
volumes:
- ./nginx:/etc/nginx/conf.d:ro
ホスト側の`nginx`ディレクトリを、コンテナ内の追加設定ディレクトリへマウントします。
| ホスト側 | コンテナ側 |
|---|---|
nginx/default.conf | /etc/nginx/conf.d/default.conf |
このディレクトリへのマウントで、イメージ内の既存の`conf.d`の内容は隠れます。手順どおり、`default.conf`を保存してから起動してください。
このファイルは`server`ブロックだけを記述する追加設定です。Nginx全体の`nginx.conf`を置き換えるものではありません。
APIの公開ポートを残している理由
今回は比較と切り分けのため、APIへの直接接続用に3000番も残しています。
NginxからAPIへの接続はCompose内のネットワークを使うため、APIのホスト側`ports`がなくても通信できます。`3000:3000`の公開が、リバースプロキシの必須条件というわけではありません。Docker Docs
また、`nginx:stable-alpine`は更新されるタグです。環境を厳密に固定する場合は、利用するバージョンやダイジェストを指定します。
7. APIを起動し、Nginxの設定を検査する
まず、Composeファイルを確認します。
docker compose config
これはCompose設定の確認です。Nginxの設定ファイルの構文検査は別に行います。
APIをビルドして起動します。依存するDBも起動します。
docker compose up --build -d api
APIへ直接アクセスします。
curl http://127.0.0.1:3000/
期待する応答は次のとおりです。
Hello, World!
起動直後に接続できない場合は、ログを確認し、起動してから再度アクセスしてください。
docker compose logs api
続いて、検査用の一時コンテナでNginxの設定を検査します。
docker compose run --rm --no-deps nginx nginx -t
| 指定 | 意味 |
|---|---|
run | サービス設定を使って一時的なコマンドを実行する |
--rm | コマンド終了後に一時コンテナを削除する |
--no-deps | このコマンドから依存サービスを起動しない |
| 最初の`nginx` | Composeのサービス名 |
| 後ろの`nginx -t` | コンテナ内で実行する構文検査コマンド |
通常の`docker compose run`では、サービスの公開ポートは割り当てられません。ここでは設定の検査だけを行います。Docker Docs
Nginxは今回の設定を読み込む際、転送先の`api`も名前解決するため、先にAPIを起動しています。
成功時の出力例です。
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
検査に成功したら、Nginxを起動します。
docker compose up -d nginx
docker compose ps
nginx・`api`・`db`が実行中か確認してください。
短い形式の`depends_on`は起動順序を指定するもので、APIがHTTP応答できる状態まで待つ設定ではありません。今回はAPIの応答を先に手動で確認しています。Docker Docs
8. Nginx経由でAPIへアクセスする
PCのターミナルで実行します。
curl -i http://127.0.0.1:8080/
HTTPステータスが200で、本文に`Hello, World!`が含まれることを確認します。
ブラウザでは、次のURLを開きます。
http://127.0.0.1:8080/
直接接続とNginx経由の接続を比べる
| URL | 通信の経路 | 期待する本文 |
|---|---|---|
http://127.0.0.1:3000/ | PCからExpressへ直接接続 | Hello, World! |
http://127.0.0.1:8080/ | PCからNginxを経由してExpressへ接続 | Hello, World! |
同じ文字列が返っても、通信の経路は異なります。
Nginxのログを表示した状態で、8080番へ再度アクセスすると、Nginxへリクエストが届いたことを確認できます。
docker compose logs -f nginx
終了するときは`Ctrl + C`を押します。コンテナ自体は停止しません。
9. 設定を変更したら検査して再読み込みする
ホスト側の`nginx/default.conf`を変更した場合、コンテナからも変更後のファイルを参照できます。ただし、ファイルを書き換えるだけでは、動作中のNginxへ設定は反映されません。
まず構文を確認します。
docker compose exec nginx nginx -t
成功したことを確認してから、再読み込みします。
docker compose exec nginx nginx -s reload
その後、curlやブラウザで実際の応答を確認してください。構文検査に成功しても、転送先のAPIが正しく応答することまでは保証されません。nginx.org
Nginxはコンテナ内で動いているため、今回はホスト側の`sudo systemctl reload nginx`は使いません。
Composeのポートやマウント設定を変更した場合は、Nginxの再読み込みだけでは反映できません。変更したサービスを`docker compose up -d`で更新します。
10. proxy_passの末尾スラッシュとパスの違い【補足】
proxy_passでは、転送先にURIを含めるかどうかで、バックエンドへ送るパスが変わります。
次のような単純なプレフィックスの`location`を例にします。
location /api/ {
proxy_pass http://api:3000;
}
この場合、`/api/`を含むパスを転送します。
一方、転送先の末尾に`/`を付けると、その`/`が置き換え先のURIになります。
location /api/ {
proxy_pass http://api:3000/;
}
location /api/内の設定 | Nginxへ送るパス | Expressへ渡すパス |
|---|---|---|
proxy_pass http://api:3000; | /api/ | /api/ |
proxy_pass http://api:3000/; | /api/ | / |
proxy_pass http://api:3000; | /api/users | /api/users |
proxy_pass http://api:3000/; | /api/users | /users |
このように、URIを付けた場合は、`location`に一致するパス部分が置き換わります。nginx.org
今回のExpressには`/`しか定義していません。`/api/`や`/users`をそのまま渡すと、API側に対応するルートがなく404になります。
この比較用の設定は説明例です。本編の`location /`は変更せずに進めて構いません。正規表現の`location`や`rewrite`を組み合わせる場合は、別途ルールの確認が必要です。
11. 接続できない場合の確認方法
まずAPIへの直接接続と比較する
curl -i http://127.0.0.1:3000/
curl -i http://127.0.0.1:8080/
| 状況 | 最初に確認すること |
|---|---|
| 3000番も8080番も接続できない | APIの起動、公開ポート、ポート競合 |
| 3000番は成功し、8080番は接続できない | Nginxの起動とポート公開 |
| 3000番は成功し、8080番は502 | NginxからAPIへの接続先・待受・ネットワーク |
| 8080番で404になる | Nginxの転送先と、Express側へ渡るパス |
HTTPエラーの番号だけで原因を断定せず、ログと合わせて調べます。
502 Bad Gatewayになる
Nginxが転送先から正常な応答を得られない場合などに発生します。
docker compose ps -a
docker compose logs nginx api
特に、次の設定を確認してください。
proxy_passが`http://api:3000`になっているかapiがComposeのサービス名と一致しているか- Expressが`0.0.0.0:3000`で待ち受けているか
- NginxとAPIが同じComposeネットワークにいるか
今回の`nginx:stable-alpine`では、コンテナ内のwgetでも接続を確認できます。
docker compose exec nginx wget -qO- http://api:3000/
Hello, World!が返れば、このコマンドからAPIへ接続できています。Nginxのプロキシ設定そのものが正しいかは、8080番への接続でも確認してください。
APIを再作成した後に接続できなくなった場合は、コンテナIPの変更も確認対象です。APIが正常に起動していることを確認したうえで、Nginxを再起動して再接続を試せます。
docker compose restart nginx
サービス名が同じでも、この実習のNginx設定が転送先IPの変更を常に自動追従するとは考えないでください。
host not found in upstreamと表示される
転送先の名前を解決できず、Nginxが起動しない場合があります。
docker compose ps -a
docker compose logs nginx
apiのスペル、APIの起動状態、作業中のComposeプロジェクトを確認してください。別ディレクトリで起動した別プロジェクトは、通常は別のネットワークになります。
原因を修正したら、一時コンテナで再度検査し、成功してから起動します。
docker compose run --rm --no-deps nginx nginx -t
docker compose up -d nginx
Nginxの初期ページが表示される
今回の設定が読み込まれず、別の設定が使われている可能性があります。
docker compose exec nginx cat /etc/nginx/conf.d/default.conf
docker compose exec nginx nginx -T
proxy_pass http://api:3000;が含まれるか確認してください。`nginx -T`はファイル上の設定を検査して表示するもので、動作中のプロセスへの反映状況は再読み込みとHTTP応答でも確認します。
また、Ubuntuへ直接導入したNginxや、別のComposeプロジェクトのURLへアクセスしていないかも確認します。
port is already allocatedなどのエラーになる
PC側の3000番や8080番が使用中の可能性があります。
docker ps
例えば8080番が使えない場合は、Nginxサービスの公開設定を次のように変更します。
ports:
- "127.0.0.1:8081:80"
更新して、`http://127.0.0.1:8081/`へアクセスします。
docker compose up -d nginx
変更したのはPC側の公開ポートです。コンテナ間の転送先`http://api:3000`は変わりません。
12. 実習を終了する・元の構成へ戻す
コンテナとネットワークを停止・削除します。
docker compose down
名前付きボリューム`pgdata`と、PC上のファイルは残ります。Docker Docs
前の記事のAPI+DB構成へ戻す場合は、停止後に退避したComposeファイルを戻します。
cp compose.before-proxy.yaml compose.yaml
docker compose up --build -d
同じディレクトリ・プロジェクト名で起動すれば、同じ名前付きボリュームを引き続き使います。今回作成した`nginx/default.conf`はPC上に残りますが、戻したComposeファイルでは使用しません。
13. まとめ
この記事では、既存のExpress APIの前にNginxを追加し、リバースプロキシの接続を確認する手順を紹介しました。
- Nginxはクライアントのリクエストを受け、バックエンドへ転送して応答を返す
proxy_passでHTTPの転送先を指定する- 同じComposeネットワークでは、`api:3000`のようにサービス名とコンテナ側ポートで接続する
- Nginxコンテナ内の`localhost`は、別コンテナのAPIを指さない
- Nginxの設定変更後は、検査に成功してから再読み込みする
proxy_passのURIの指定によって、バックエンドへ渡すパスが変わる- 接続に失敗したら、直接接続・Nginx経由・コンテナ間接続を分けて調べる
まずは、3000番への直接接続と8080番へのNginx経由の接続を比較してください。同じ応答が返るまでに、どのコンテナを通っているかを説明できれば、リバースプロキシの基本を理解できています。
今回のAPIはDBへ接続しておらず、HTTPS・認証・複数APIへの負荷分散も追加していません。これらは、基本のHTTP転送を理解してから取り組む次のテーマです。