Nginxのリバースプロキシ入門|proxy_passでExpress APIに接続する


はじめに

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へリクエストを送る
2NginxがExpressへリクエストを転送する
3Expressが`Hello, World!`を返す
4Nginxがその応答をブラウザへ返す

ブラウザがアクセスする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.yamlNginxを含む構成へ変更する
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番は502Nginxから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転送を理解してから取り組む次のテーマです。

関連記事・参考資料


コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です