nginx のリバースプロキシを Docker-Compose で試してみる
nginx のリバースプロキシ機能を使って、複数の Node.js サーバへの通信を、単一ドメインで受けてみたい。
目次
- リバースプロキシとは
- リバースプロキシで何ができるの?
- Docker-Compose で開発環境を作る
- デモプロジェクトとコード全量
- プロジェクト構成
- nginx の設定を書いていく
- nginx コンテナを立ち上げるための設定
- 適当な Node.js サーバを作る
- 動かしてみる
- 以上
リバースプロキシとは
リバースプロキシとは、クライアント側に用意するプロキシと違って、サーバ側に存在してリクエストを仲介するプロキシのこと。
何がリバースなんや?というと、通常クライアント側にいるプロキシは「内々から外へ出ていく」通信を仲介するが、コチラは「外から内々に入っていく」通信を仲介するので、逆だよねってことでリバースらしい。
リバースプロキシで何ができるの?
リバースプロキシによってどういう通信経路ができるかというと、以下のような感じ。
前提条件として、
http://localhost:3001/
http://localhost:3002/
の2つのポートで、それぞれ何らかのサーバを動かしていることとする。Node.js・Express サーバでも、Python・Flask サーバでもなんでも良い。
これらのウェブアプリにアクセスするには、上のようにそれぞれポート番号を指定してアクセスしても良いが、ドメインは1つに集約したい、ポート番号をイチイチ指定したくない、という時に、リバースプロキシが前段に立ってやる。
リバースプロキシサーバ (= 今回は nginx が担う) は
http://localhost/
と80番ポートで待ち構えている。そして、
http://localhost/app-1/
へのアクセス →http://localhost:3001/
に転送http://localhost/app-2/
へのアクセス →http://localhost:3002/
に転送
という風にリクエストをハンドリングしてくれるようになる。この時、app-1/
とか app-2/
とかいう階層指定が転送先では削られていることに留意。
http://localhost/app-1/examples/test.html
へのアクセス →http://localhost:3001/examples/test.html
に転送- (転送先の3001番ポートの方は
app-1/
という階層が削られている) - (パスを削らずに転送する設定も可能ではある)
- (転送先の3001番ポートの方は
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
を省いている。
src/index.js
は、http
モジュールを使って簡易サーバを立てている- リクエストすると
src/example.html
の内容をレスポンスする作り
実際は 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/ {}
という一つのブロックで、一つのアプリサーバに対するリバプロ設定が書かれている。
location
のパスはスラッシュ/
を末尾に書くこと。でないと//hoge
といったようにスラッシュが2重に付いて転送されてしまう- 一連の
proxy_set_header
はほぼお決まりなのでそのまま書く proxy_pass
が転送先 URL を指定する場所。URL の末尾にスラッシュを書くかどうかで転送のされ方が変わるので注意- URL の末尾に
/
を付けると、/app-1/hoge
へのリクエストは:3001/hoge
に転送される - URL の末尾に
/
を付けないと、/app-1/hoge
へのリクエストが:3001/app-1/hoge
に転送される
- URL の末尾に
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 コンテナとして立ち上げるための設定を書いていく。
./nginx/Dockerfile-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) と再起動が必要になる。まぁ頻繁にイジらないし良いか。
./docker-compose.yml
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
を参照できるようにしておく。image
と container_name
は任意に。最後に ports
で80番ポートをホスト側に公開するようにしておく。
コレで $ docker-compose up --build
コマンドを叩けば、nginx サーバが起動し http://localhost/
にアクセスすると index.html
の内容が返ることが確認できる。まだ /app-1/
・/app-2/
を用意していないので、リバプロの動作確認はできていない。
適当な Node.js サーバを作る
続いて、リバプロの転送先となる適当な Node.js サーバを作っておく。今回は本筋ではないので、雑にコーディングする。
./app-1/src/index.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
は、区別がつくようにサーバ名が書いてあるだけの適当なファイル。なんでも良い。
./app-1/Dockerfile-app-1
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
も全く同じ内容で良い。
docker-compose.yml
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-1
と app-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
側は書き換えても反映されないので、完全なライブリロード開発ではないが。
こんな感じでボリュームマウントを使ったり、起動コマンドを
command: [sh, -c, npm run dev]
などと開発用コマンドに変えてやったりすれば、もう少し開発がしやすくなると思われる。
なお、app-1
が公開している3001番ポート、app-2
が公開している3002番ポートは、ホスト側から直接アクセスはできない。直接アクセスしたい場合は ports
プロパティをそれぞれに書いてやり、ポートフォワードしてやること。今回は nginx が転送するので、基本的には3001・3002番ポートをホストに公開する必要はない。
動かしてみる
それでは実際に動かしてみよう。
$ docker-compose up --build
常に上のコマンドを使用し、Dockerfile を都度ビルドすることにする。コレにより設定ファイルの変更が反映された状態でコンテナ群が起動する。
起動したら、ホスト側で次の URL にアクセスして動作確認してみよう。
- http://localhost/
./nginx/html/index.html
の内容が返ることdefault.conf
のroot
プロパティが効いていることが確認できる
- http://localhost/app-1/
./app-1/src/example.html
の内容が返ること- アプリケーションサーバ
app-1
が3001番ポートで起動しており、nginx がリバースプロキシしていることが確認できる
- http://localhost/app-2/
./app-2/src/example.html
の内容が返ること- アプリケーションサーバ
app-2
が3002番ポートで起動しており、nginx がリバースプロキシしていることが確認できる
それぞれちゃんと動作すれば OK。
502 Bad Gateway が出た場合は、アプリサーバがリスンしているポートが何になっているか、default.conf
で指定しているポート番号が合っているか、などを確認してみよう。
以上
こんな感じで、nginx によるリバースプロキシの設定方法を確認できた。
Docker を使えばホストマシンへのインストール作業が要らなくなるし、Docker-Compose を使うことで複数のサーバを起動したりする手間が省ける。
仲介する・ラップする要素が増えると理解するのが難しくなるところもあるが、マシンに直接インストールして設定したりしていると、何がどうなっているのか整理が付かなくなる場合もあるので、こうしてクリーンな環境で試せるのは良いことだろう。