Node.js
docker
webpack

Dockerでnginx+node.jsのSPA構成を試す

More than 1 year has passed since last update.

なにをしたのか

Docker Composeという、複数のDockerコンテナを管理する機能を使ったサンプルです。目指しているのは、「実用的かつ最小のサンプル(と思っているもの)」です。

nginxコンテナでstaticなファイル(html/js)を配信し、APIのアクセスは後ろにいるnode.jsのアプリケーションコンテナにフォワードして、SPAのWebサービスを構築しています。

docker.png

基本的に、以下のページを参考に書いています。
DockerでのNodeアプリ構築で学んだこと | POSTD

リポジトリはこちら。
docker-compose-test

環境

  • Docker 1.12.1
  • docker-compose 1.8.0
  • node.js 4.6.0
  • nginx 1.11.4

動かす

Docker for macがインストールされていることを前提として。

$ git clone https://github.com/KeitaMoromizato/docker-compose-test
$ cd docker-compose-test
$ sudo docker-compose build
$ sudo docker-compose up

内容

Dockerとは

Dockerは、コンテナ型の仮想環境です。

簡単に言うと、Infrastructure as Code(インフラのコード化)の一種です。DockerFileという設定ファイルに、どういうサーバーを構築したいかを書いておき、docker runコマンドを実行するだけで、その環境のコンテナが即起動できます。

また、そのDockerFileから構成されるコンテナが、Docker Imageという形で世の中(DockerHubというサイト)に公開されています。なので、例えば公式のWordpressイメージをダウンロードして、docker runするだけで、簡単にWordpressの環境が入ったコンテナが作れるようになっています。

Docker Composeとは

Dockerは、その特徴から1コンテナ1アプリケーションという思想が基本です。1つのコンテナにすべての機能を詰め込まず、複数のコンテナを起動して、協調してシステムを構成しましょうという考えです。

例えば、リバースプロキシのnginx、バックエンドのアプリケーション、データベースを、それぞれ別のコンテナで起動します。それを簡単に実現するのが、Docker Composeです。

docker-compose.ymlという設定ファイルに、どのコンテナを立ち上げるか、どのコンテナ同士が通信するのかといったことを記述し、これまたdocker-compose upというコマンド1つで、必要なサービスがすべて立ち上がります。

目的

プロダクションで使うメリットは、まだ使っていないのでパス。

今回使った目的は、手元の開発環境の管理のため。(プライベートも含め)いろいろな開発をしていると、やれnode.jsのバージョンが違うだとか、やれGo langのパスが通ってないとか、なんだか色々と面倒なことが多い。

Dockerを使えば、バックエンドアプリケーションを動かす環境をDockerFileという形で明示でき、かつnginxやDBなどをローカルで個別に立ち上げるのではなく、Docker Composeを使うことでより本番環境に近いものが、簡単に再現できます。

という訳で、普段ちょっとしたものを作るときによく使うスタック、node.js+SPA(Webpackビルド)の環境を構築してみました。

ディレクトリ構成

主要なものだけで、こんな感じ。

|-- Dockerfile
|-- docker-compose.yml
|-- client
|  |-- index.html
|  |-- index.js
|
|-- server
|  |-- index.js
|
|-- nginx
|  |-- default.conf 
名前 役割
Dockerfile node.jsアプリのコンテナの設定。コンテナを最初に構築するときに、何をするのかなど。
docker-compose.yml 各コンテナの詳細設定。今回は、nodeコンテナとnginxコンテナの設定が書いてある
client 静的配信するファイルのコンパイル前データ。これをWebpackでビルドして、outフォルダに格納される
server nodeアプリ側のコード
nginx nginxの設定ファイル。静的配信と、nodeアプリへフォワードする設定が書いてある

詳細

Dockerfile

Docke Composeを使う場合でも、Singleコンテナの場合でも、基本となるのはDockerfileです。ベースとなるコンテナ(今回で言うと、nodeアプリケーション)の設定を書いていきます。

ここでは、主にnpm installとフロントのビルド(Webpack)のための設定を記述しています。

まずは必要なファイルをコピーして

Dockerfile
COPY package.json webpack.config.js $HOME/nodeapp/
COPY client $HOME/nodeapp/client

npm installnpm run buildを実行します。

Dockerfile
USER app
WORKDIR $HOME/nodeapp
RUN npm install
RUN npm run build

package.jsonwebpack.config.jsを見るとわかるように、ビルドしたファイルはnodeコンテナ上outディレクトリに出力されます。

package.json
{
  "scripts": {
    "build": "webpack --config webpack.config.js"
  }
}
webpack.config.js
{
  output: {
    path: __dirname + "/out",
    filename: 'bundle.js',
  }
}

docker-compose.yml

docker-compose.ymlには、起動するすべてのコンテナの設定を書きます。

docker-compose.yml
nodeapp:
  build: .
  container_name: "nodeapp"
  command: node index.js
  ports:
    - '3000:3000'
  environment:
    - PORT=3000
  volumes:
    - ./server:/home/app/nodeapp
    - /home/app/nodeapp/node_modules
    - www:/home/app/nodeapp/out
nginx-proxy:
  image: nginx
  container_name: "nginx"
  ports:
    - '8080:8080'
  volumes:
    - ./nginx:/etc/nginx/conf.d:ro
    - www:/www/app:ro
  links:
    - 'nodeapp'

image

DockerHubにおいてあるイメージを選択します。nodeのイメージはDockerfileの方に書いてあるので、ここではnginxのイメージのみ。

DockerHubからイメージを探すときは、できるだけOFFICIALなものを選びましょう。

environment

コンテナに設定する環境変数を指定します。

volumes

volumesの記述には、いくつか目的があります。例えば、以下の記述は「ホスト上の.serverディレクトリを、コンテナ上の/home/app/nodeappにマウントする`設定です。これにより、コンテナ上で任意のアプリケーションを動かせるようになります。

  volumes:
    - ./server:/home/app/nodeapp

また、以下の記述は「node_modulesディレクトリをボリュームとして作成する」設定です。ボリュームは、コンテナとは別のライフサイクルを持っています。ここでは、コンテナを落とす度にnpm installしたモジュールが消えてしまうと困るので、別途ボリュームとして定義しています。

  volumes:
    - /home/app/nodeapp/node_modules

また、ボリュームはコンテナ間で共有することもできます。以下の記述は「コンテナ上のディレクトリを、名前付きボリューム(www)として他のコンテナと共有できるようにする」設定です。ここでは、nodeコンテナでビルドしたファイルの出力ディレクトリ(out)にwwwwという名前を付けています。

  volumes:
    - www:/home/app/nodeapp/out

そして、そのボリュームを、nginxコンテナから参照し、静的配信しています。後ろにroを付けると、そのコンテナからは読み取り専用(ReadOnly)になります。

  volumes:
    - www:/www/app:ro

links

コンテナが通信できる、別のコンテナの名前を記述します。ここでは、nginx-proxyがリクエストをフォワードするため、nodeappコンテナを見えるようにしています。

nginxの設定

nginxのコンテナでは、静的ファイルの配信と、APIへのリクエストをnodeコンテナにフォワードする設定をします。

静的ファイルは、先のdocker-compose.ymlの設定で、outディレクトリを/www/appにマウントしているため、そこをrootとして設定します。

APIへのリクエストは、proxy_passでnodeコンテナにフォワードします。ここでは、linksで設定した名前(nodeapp)で転送できるようになります。

nginx/default.conf
server {
  listen 8080;
  server_name localhost;

  location /api/ {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://nodeapp:3000/;
  }

  location / {
    root /www/app;
  }
}

nodeアプリ

普通のexpressアプリ。上記のnginxの設定だと、URLの/api部分が省かれて転送されるので、こちらでは/itemsでLISTENする。

server/index.js
const app = require('express')();

const PORT = 3000;

app.use((req, res, next) => {
  console.log(req.url);
  next();
});

app.get('/items', (req, res) => {
  res.send([
    'hoge', 'huga'
  ]);
});

app.listen(PORT, () => console.log(`LISTEN:${PORT} on docker`));

フロント

実装はReactだけど何でも良いので省略。Webpackでビルドしたものと、HTMLファイルをoutディレクトリに出力するように設定する。

webpack.config.js
module.exports = {
  entry: {
    js: "./client/index.js",
    html: "./client/index.html",
  },  
  output: {
    path: __dirname + "/out",
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react'],
        },
      },
      {
        test: /\.html$/,
        loader: 'file?name=[name].[ext]'
      },
    ],
  },
}

まとめ

一度慣れてしまうと、開発環境は基本的にこれで構築したほうが良いと思うくらい、楽。

あとは、Mongo DBMySQLElasticsearchなども公式のイメージがあるので、自分のサービスに合わせてdocker-compose.ymlに追加してあげれば良い。公式のREADMEは結構充実しているので、それを見ればなんとかなる。

次やること

プロダクションと開発用の設定をわけたい
* フロントをプロダクションビルドしたい
* 開発中はwebpack --watchしたい