nginx のリバースプロキシを Docker-Compose で試してみる

nginx のリバースプロキシ機能を使って、複数の Node.js サーバへの通信を、単一ドメインで受けてみたい。

目次

リバースプロキシとは

リバースプロキシとは、クライアント側に用意するプロキシと違って、サーバ側に存在してリクエストを仲介するプロキシのこと。

何がリバースなんや?というと、通常クライアント側にいるプロキシは「内々から外へ出ていく」通信を仲介するが、コチラは「外から内々に入っていく」通信を仲介するので、逆だよねってことでリバースらしい。

リバースプロキシで何ができるの?

リバースプロキシによってどういう通信経路ができるかというと、以下のような感じ。

前提条件として、

の2つのポートで、それぞれ何らかのサーバを動かしていることとする。Node.js・Express サーバでも、Python・Flask サーバでもなんでも良い。

これらのウェブアプリにアクセスするには、上のようにそれぞれポート番号を指定してアクセスしても良いが、ドメインは1つに集約したい、ポート番号をイチイチ指定したくない、という時に、リバースプロキシが前段に立ってやる。

リバースプロキシサーバ (= 今回は nginx が担う) は

と80番ポートで待ち構えている。そして、

という風にリクエストをハンドリングしてくれるようになる。この時、app-1/ とか app-2/ とかいう階層指定が転送先では削られていることに留意。

Docker-Compose で開発環境を作る

ふむふむなるほど、nginx でリバプロを立てるとこんなことが出来るのね、じゃあやってみよう。

nginx は Homebrew なんかでもインストールできたりするが、裏に用意するアプリケーションサーバ含めて開発環境・実行環境を構築していくのはちょっと面倒だ。そこで今回は、Docker-Compose を利用して、nginx および Node.js 環境をそれぞれ立ち上げることにしたい。

デモプロジェクトとコード全量

今回作ったデモプロジェクトは、以下の GitHub リポジトリで公開している。細かなコードはコチラを参考にしてほしい。

プロジェクト構成

プロジェクト構成は次のようにする。

【プロジェクトルート】
├ docker-compose.yml
├ nginx/
│ ├ Dockerfile-nginx
│ ├ default.conf
│ └ html/
│    └ index.html
├ app-1/
│ ├ Dockerfile-app-1
│ └ src/
│    ├ index.js
│    └ example.html
└ app-2/
   ├ Dockerfile-app-2
   └ src/
      ├ index.js
      └ example.html

ルートの docker-compose.yml が、nginx/app-1/app-2/ それぞれのディレクトリを3つのコンテナとして立ち上げる。

nginx/ ディレクトリはそのものズバリ、nginx サーバの設定を持っている。Dockerfile-nginx という Dockerfile で、nginx ベースのイメージを用意している。

app-1/app-2/ ディレクトリは、それぞれ Node.js サーバというテイ。今回は簡単にするため package.json を省いている。

実際は package.json があって、Dockerfile で npm install しておいて npm start でサーバを起動するような作りになるだろうか。

それぞれの Dockerfile や docker-compose.yml の設定はひとまず後にして、まずは本命である nginx の設定を作っていこう。

nginx の設定を書いていく

nginx の設定ファイル、default.conf を用意して、リバースプロキシ設定を書いていく。

server {
  listen       80;
  server_name  localhost;
  root         /var/www/html;
  
  location /app-1/ {
    proxy_set_header  Host                $host;
    proxy_set_header  X-Real-IP           $remote_addr;
    proxy_set_header  X-Forwarded-Host    $host;
    proxy_set_header  X-Forwarded-Server  $host;
    proxy_set_header  X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_pass        http://app-1:3001/;
  }
  
  location /app-2/ {
    proxy_set_header  Host                $host;
    proxy_set_header  X-Real-IP           $remote_addr;
    proxy_set_header  X-Forwarded-Host    $host;
    proxy_set_header  X-Forwarded-Server  $host;
    proxy_set_header  X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_pass        http://app-2:3002/;
  }
}

唐突だが、リバプロの設定はこんな感じ。location /app-1/ {} という一つのブロックで、一つのアプリサーバに対するリバプロ設定が書かれている。

nginx のルートパスは root プロパティを指定して、/var/www/html/ ディレクトリ内のファイルを返すようにしておく。サーバにインストールした nginx の場合、/usr/share/nginx/html/ をデフォルトで参照する場合もあったりする。パスだけ知っておくと良いかも。

あと、server_name 部分は、実行環境によっては以下のように書いたりもするかな。

server_name  $hostname '' 【グローバル IP を書いたり】;

認識してほしいサーバ名を複数書いたり、'' と空白を書いたり。

HTTPS に対応する際は、location 内に CORS 設定を書く場合もあるかな。転送先の Node.js サーバ側でヘッダを付与しても良い。どちらかで指定しておけば良いようだ。Allow-Origin がダブらないよう注意。

location /app-1/ {
  add_header  Access-Control-Allow-Origin       '*'  always;
  add_header  Access-Control-Allow-Methods      'GET, POST, PUT, DELETE, OPTIONS';
  add_header  Access-Control-Allow-Headers      '*';
  add_header  Access-Control-Allow-Credentials  true;
}
// Express サーバに書くイメージ。nginx 側とどちらかで指定すれば良い
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin'     , '*');
  res.header('Access-Control-Allow-Methods'    , 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers'    , '*');
  res.header('Access-Control-Allow-Credentials', true);
  next();
});

CORS 設定をしたとき、なぜかルートパス /app-1/ への Ajax リクエストだけ Allow-Origin が効かなくて困ったのだが、直し方が分からず諦めた。普通に遷移するとアクセスはできるので、CORS だけ上手く効かないみたい。よく分からん。

nginx の設定ファイルの書き方はイマイチ分からなくて、それでいて自由度もあって難しいな…。

今回は Docker で動かすので以下の操作は実際には行わないが、どこかのサーバ上にインストールした nginx を使っている場合は、以下のように nginx を再起動したりして、設定ファイルの変更を反映したりする。

# default.conf ではなく nginx.conf がデフォルトの設定ファイルな場合もある (バージョンによるもの?)
$ vi /etc/nginx/default.conf

# 設定を再読込したり
$ service nginx reload
# nginx を再起動したり
$ service nginx restart
# 稼動状況を確認したり
$ service nginx status

nginx コンテナを立ち上げるための設定

設定ファイルができたので、コレを nginx コンテナとして立ち上げるための設定を書いていく。

FROM nginx:1.17

COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY ./html/        /var/www/html/

Dockerfile がやることは、ベースイメージの指定と設定ファイルのコピーだけ。

html/ ディレクトリはサーバルートにアクセスした時に index.html を返せるように、コピーしている。

この Dockerfile により、予めファイルをコピーして Docker イメージをビルドして nginx サーバを起動するので、設定ファイルをココで書き換えても、起動中の Docker コンテナの設定は変わらない点に注意。変更の度に Docker イメージの再ビルド (COPY) と再起動が必要になる。まぁ頻繁にイジらないし良いか。

version: '3'
services:
  nginx:
    build:
      # Dockerfile の保存場所を指定する
      context   : ./nginx/
      # Dockerfile 名を指定する
      dockerfile: Dockerfile-nginx
    # イメージ名
    image         : practice-nginx
    # コンテナ名
    container_name: practice-nginx
    # ポートフォワード
    ports:
      - 80:80

Docker-Compose の設定ファイルはこんな感じ。build プロパティで ./nginx/Dockerfile-nginx を参照できるようにしておく。imagecontainer_name は任意に。最後に ports で80番ポートをホスト側に公開するようにしておく。

コレで $ docker-compose up --build コマンドを叩けば、nginx サーバが起動し http://localhost/ にアクセスすると index.html の内容が返ることが確認できる。まだ /app-1//app-2/ を用意していないので、リバプロの動作確認はできていない。

適当な Node.js サーバを作る

続いて、リバプロの転送先となる適当な Node.js サーバを作っておく。今回は本筋ではないので、雑にコーディングする。

require('http').createServer((_req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.end(require('fs').readFileSync(require('path').join(__dirname, './example.html'), 'utf-8'));
}).listen(3001);

最後に指定しているように、3001番ポートで公開する設定。app-2 の方はこのポート番号だけ変えていて、3002番ポートで公開するようになっている。

レスポンスに使用する example.html は、区別がつくようにサーバ名が書いてあるだけの適当なファイル。なんでも良い。

FROM node:14.3

COPY ./src/ /src/
WORKDIR /src/
CMD ["node", "/src/index.js"]

Dockerfile は、Node.js ベースのイメージに資材を配置して起動しているだけ。package.json を用意した場合は npm install などを事前に行っておく感じかな。./app-2/Dockerfile-app-2 も全く同じ内容で良い。

version: '3'
services:
  nginx:
    # 前述のとおり・省略
  
  # app-1 : ビルド済のコンテナを公開する。ファイルの変更は検知できない
  app-1:
    build:
      context   : ./app-1/
      dockerfile: Dockerfile-app-1
    image         : practice-app-1
    container_name: practice-app-1
  
  # app-2 : ボリュームマウントすることで静的ファイルの変更を反映できるようにする
  app-2:
    build:
      context   : ./app-2/
      dockerfile: Dockerfile-app-2
    image         : practice-app-2
    container_name: practice-app-2
    # ボリュームマウントして実行する
    volumes:
      - ./app-2/src:/src
    working_dir: /src/
    # node コマンドのパスを認識させるため sh -c が必要
    command: [sh, -c, node /src/index.js]
    # 直接公開する場合は以下を書く
    #ports:
    #  - 3002:3002

Docker-Compose 設定ファイルには app-1app-2 という services が追加されている。この名前が、nginx の default.conf 内に指定した http://app-1:3001/ などのサーバ名に対応することになる。

シンプルにビルドした Docker イメージを立ち上げるだけなら、app-1 の書き方で app-2 も書いてやれば良い。

app-2 の書き方の方は、./app-2/Dockerfile-app-2 で指定した COPY・WORKDIR・CMD 設定を上書きしていて、ボリュームマウントして動かしている。コレにより、./app-2/src/example.html の内容を書き換えた時に即座に反映されるようになる。index.js 側は書き換えても反映されないので、完全なライブリロード開発ではないが。

こんな感じでボリュームマウントを使ったり、起動コマンドを

などと開発用コマンドに変えてやったりすれば、もう少し開発がしやすくなると思われる。

なお、app-1 が公開している3001番ポート、app-2 が公開している3002番ポートは、ホスト側から直接アクセスはできない。直接アクセスしたい場合は ports プロパティをそれぞれに書いてやり、ポートフォワードしてやること。今回は nginx が転送するので、基本的には3001・3002番ポートをホストに公開する必要はない。

動かしてみる

それでは実際に動かしてみよう。

$ docker-compose up --build

常に上のコマンドを使用し、Dockerfile を都度ビルドすることにする。コレにより設定ファイルの変更が反映された状態でコンテナ群が起動する。

起動したら、ホスト側で次の URL にアクセスして動作確認してみよう。

  1. http://localhost/
    • ./nginx/html/index.html の内容が返ること
    • default.confroot プロパティが効いていることが確認できる
  2. http://localhost/app-1/
    • ./app-1/src/example.html の内容が返ること
    • アプリケーションサーバ app-1 が3001番ポートで起動しており、nginx がリバースプロキシしていることが確認できる
  3. http://localhost/app-2/
    • ./app-2/src/example.html の内容が返ること
    • アプリケーションサーバ app-2 が3002番ポートで起動しており、nginx がリバースプロキシしていることが確認できる

それぞれちゃんと動作すれば OK。

502 Bad Gateway が出た場合は、アプリサーバがリスンしているポートが何になっているか、default.conf で指定しているポート番号が合っているか、などを確認してみよう。

以上

こんな感じで、nginx によるリバースプロキシの設定方法を確認できた。

Docker を使えばホストマシンへのインストール作業が要らなくなるし、Docker-Compose を使うことで複数のサーバを起動したりする手間が省ける。

仲介する・ラップする要素が増えると理解するのが難しくなるところもあるが、マシンに直接インストールして設定したりしていると、何がどうなっているのか整理が付かなくなる場合もあるので、こうしてクリーンな環境で試せるのは良いことだろう。