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

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

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

![docker.png](https://qiita-image-store.s3.amazonaws.com/0/52054/52587236-da00-99ef-b6b0-df9787df09a0.png)

基本的に、以下のページを参考に書いています。
[DockerでのNodeアプリ構築で学んだこと | POSTD](http://postd.cc/lessons-building-node-app-docker/)

リポジトリはこちら。
[docker-compose-test](https://github.com/KeitaMoromizato/docker-compose-test)

# 環境
* Docker 1.12.1
* docker-compose 1.8.0
* node.js 4.6.0
* nginx 1.11.4

# 動かす

[Docker for mac](https://docs.docker.com/engine/installation/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:Dockerfile
COPY package.json webpack.config.js $HOME/nodeapp/
COPY client $HOME/nodeapp/client
```

`npm install`と`npm run build`を実行します。

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

`package.json`、`webpack.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`には、起動するすべてのコンテナの設定を書きます。

```yaml: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](https://hub.docker.com/_/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: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する。

```js: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`ディレクトリに出力するように設定する。

```js: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 DB](https://hub.docker.com/_/mongo/)や[MySQL](https://hub.docker.com/_/mysql/)、[Elasticsearch](https://hub.docker.com/_/elasticsearch/)なども公式のイメージがあるので、自分のサービスに合わせて`docker-compose.yml`に追加してあげれば良い。公式のREADMEは結構充実しているので、それを見ればなんとかなる。

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