dockerコンテナ上でnode.jsのサーバを起動し、ホスト端末からアクセスする

# はじめに
dockerコンテナ上にnode.jsでserverを起動し、ホストであるMacから接続するまでの手順を記載します。
本手順は私が引っかかった罠を再現しつつ記載します。
そのため「そんなことはいいからdocker上でのnodeの起動方法だけ知りたい」という方は別のサイトでご確認ください。
後、私自身があまりdockerに慣れていないためコマンドの説明を入れながら記載します。

# 作業環境
* ホスト側
  * macOS Sierra 10.12.1
  * docker for Mac ~~v1.12.5~~ v17.03.0-ce(※1)
* dockerコンテナ内
  * dockerイメージ node
  * npm -v -> 3.10.9
  * node -v -> v7.1.0

> ※1  2017年3月16日時点でのdocker最新バージョンがv17.03.0-ceになっていました。おいおいバージョン上がりすぎだろ、思って調べたところどうやらバージョン番号の発番体系が変更されたようで、年2桁.月2桁.リビジョンになったようです。最後のceはCommunity Editionの頭文字のようです。
参考記事: http://www.publickey1.jp/blog/17/docker_v1703.html

# 前提条件
docker for Macがインストールされているものとして話を進めます。

# 1.dockerコンテナの起動
`docker run`コマンドを実行し、コンテナを起動します。イメージ取得もコンテナ生成もすっ飛ばしていきなり起動と言われると
「そもそも起動するものがないのでは？」
という疑問がでてくると思います。
実はこの`docker run`コマンドは **dockerイメージの取得**、**コンテナの生成**、**コンテナの起動** という３つの操作を一度に行ってくれます。
この3つの操作の概要について簡単に説明しようと思います。

### 追記
`docker run`コマンドの新コマンドは`docker container run`となりました。コマンドでできることは同じのようです。
基本的に`docker container XX`というコマンドになっているものが多いため以降の説明ではそうなっていないコマンドだけ追記を入れました。
なお、最新verでこの記事の手順が通用することも確認しました。

### dockerイメージの取得
ローカルマシンにイメージが存在しない場合は`docker hub`という外部サービスからイメージを取得します。
既にローカルマシン上にイメージが存在する場合は、ローカルのイメージを使用します。
正確には`docker run`コマンドでは、どのイメージをベースにコンテナを生成するのか指示する必要があるためイメージ名の指定が必須です。指定したイメージがローカルに存在する場合はそれを、存在しない場合は`docker hub`からイメージ名で検索し存在すれば取得します。
このような動きをするため、初回実行時はイメージをダウンロードしてくる時間が結構かかり`docker run`コマンドの実行自体も比較的長くなります。
イメージを削除しない限りローカルのイメージを使うので、２回目以降は高速になります。

### コンテナの生成
`docker run`コマンドではコンテナ生成も一緒に行ってくれます。コンテナ生成を単独で行う場合、`docker create`コマンドを使用します。
そのためコンテナを１つも作成していない状態で`docker run`を実行してもエラーになりません。
また、`docker run`を実行するたびに新しいコンテナが生成されます。
生成されたコンテナは`docker ps -a`コマンドで確認できます。（`-a`オプションをつけると停止中のコンテナも一覧に表示されます。）
`docker run`のたびにコンテナが生成されますので、頻繁に実行する場合は`--rm`オプションをつけてください。
`--rm`をつけた場合、当該コンテナを停止したタイミングでコンテナ自体が削除されます。

### 追記
`docker ps`コマンドの新コマンドは`docker container ls`となりました。こちらの方が分かりやすくていい感じだと思います。

### コンテナの起動
`docker run`コマンドではコンテナ起動も一緒に行ってくれます。コンテナ起動を単独で行う場合、`docker start`コマンドを使用します。

# 1.dockerコンテナの起動 続き
前置きが長くなりましたが、以下の通り`docker run`コマンドを実行します。
今回はMac側でファイル作成などを行なうためMac側のあるディレクトリをコンテナ側のディレクトリへマウントします。
また、`node.js`でサーバ起動するためポートマッピングを行います。

```bash:コマンド例
docker run -v [ホスト側共有ディレクトリ]:[コンテナ側共有ディレクトリ] -p 8080:3000 -it node /bin/bash
```

以下、オプションの簡単な説明です。

### -vオプションについて
Mac側のディレクトリをコンテナ内へマウントすることができます。例えばMac側の/User/XXX/dockershareディレクトリとコンテナ内の/var/nodeappを共有したい場合、以下のようにします。

```bash:-vオプション例
-v /User/XXX/dockershare:/var/nodeapp
```

ホスト側共有ディレクトリは予め適当な作業ディレクトリを決めてそこを指定してください。
コンテナ側共有ディレクトリはそのディレクトリが存在している必要はありません。コンテナ生成時に勝手に作成されマウントされます。

### -pオプションについて
外部からコンテナにアクセスするためのポートフォワード指定を行うオプションです。上記コマンド例ではホスト側から8080ポートにアクセスがあった場合、コンテナ側の3000ポートへフォワードします。
手順ではコンテナ内で`node.js`のサーバを起動し3000番ポートでlistenしますのでこの指定としました。

### -itオプションについて
iは標準入力(STDIN)を開くオプションで、tはtty(端末デバイス)を割り当てるオプションです。
`node`という指定は`docker`イメージ名で、生成するコンテナの元となるイメージとなります。最後の`/bin/bash`は実行コマンドを指定しています。
この指定をするとbashでコンテナ内の操作ができるようになります。
なお、`exit`コマンドでコンテナから抜けることができますが、このときコンテナも同時に停止します。
ホスト側でコンソールを２つ起動し、片方で上記コマンドを実行しもう片方で`docker ps`を実行すると状態がよくわかるかと思います。

# 2.server.jsの作成
`docker run`コマンド実行時に指定したホスト側の共有ディレクトリに`server.js`を作成します。
ネットでよく出てくるHello worldを返すだけのサンプルプログラムです。
（実はこのプログラムではうまくいきません。あとで解消するのでこのままいきます。）

```js:server.js
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(3000, 'localhost');
console.log('Server running at http://localhost:3000/');
```

コンテナ内の共有ディレクトリに作成した`server.js`が存在するはずです。

# 3.コンテナ内でサーバ起動
`server.js`があるディレクトリで以下のコマンドを実行します。

```bash:サーバ起動
node server.js &
```

## 4.コンテナ内でのサーバ起動確認
以下のコマンドで起動確認をします。

```bash:コンテナ内からの起動確認
curl localhost:3000
[結果]
Hello World
```
問題なく起動していることを確認しました。

## 5.ホスト側での起動確認
Mac側からサーバに接続できるか確認します。
`docker run`コマンド実行時に指定した通り、ホスト側ポート8080はコンテナ側ポートの3000にフォワードするはずなので、以下のようにすればいけるはずです。

```bash:ホスト側からの起動確認
curl localhost:8080
[結果]
curl: (52) Empty reply from server
```

エラーになりました。原因を追ってみます。
まずコンテナを起動した状態で別のコンソールを立ち上げて`docker ps`を実行します。PORTSの部分が以下のようになっているはずです。
このPORTSはポートフォワーディングの設定です。

```bash:ポートフォワーディングの設定
PORTS
0.0.0.0:8080->3000/tcp
```

0.0.0.0は（ここでは）全てのアドレスを指しており、アドレス関係なく8080ポートのアクセスはコンテナ側の3000にフォワードするという指定になるようです。
このアドレスはdocker固有の話ではなくネットワークの話になりますので、詳細は説明しません。
従って、ポートフォワーディングの設定は問題なさそうです。次にコンテナ側を確認します。

# 6.サーバのlisten設定を変更
簡単に考えられる原因はSELinuxやFirewallですが今回使用しているdockerイメージはどちらも使用していません。
色々調べた結果、接続できない原因は`server.js`のlistenにlocalhostを指定しているためであることが分かりました。
dockerのissueに同じ問題が載っていました。
https://github.com/docker/docker/issues/3269  
どうやらdockerコンテナにはlocalhostがバインドされていないためアクセス不可で、listenするホストはパブリックインタフェースにする必要があるようです。

色々試した結果、ホスト側から通るパターンは以下の３つと思われます。

```js:server.jsのlisten指定
// パターン1: ポートだけ書いてホスト省略。これはどんなIPv4アドレスの接続も受け付ける指定のようです。
}).listen(3000);

// パターン2: デフォルトルートを指定。
}).listen(3000, '0.0.0.0');

// パターン3: dockerホストが割り当てたIPアドレスを指定
}).listen(3000, '172.17.0.2');
```

パターン3を試した理由は、そもそもlocalhostがダメな理由は「dockerコンテナにバインドされていないから」ということはバインドされていればOKということなのでdockerホストが割り当てたIPを指定すればいけるのかなと考えて試してみました。
ここで指定するIPアドレスは`docker inspect`コマンドで確認できます。コンテナIDは`docker ps`で調べることができます。

```bash:コンテナの詳細情報を表示
docker inspect [コンテナID]
[結果（途中省略）]
・・・
     "Networks": {
          "bridge": {
              "IPAMConfig": null,
              "Links": null,
              "Aliases": null,
              "NetworkID": ・・・,
              "EndpointID": ・・・,
              "Gateway": "172.17.0.1",
              "IPAddress": "172.17.0.2",  ← これ
              "IPPrefixLen": 16,
・・・
```

上記3パターンならいずれも成功することを確認しました。セキュリティ云々という話を抜きにして、とりあえず接続確認するのであればどのパターンでも良いので`server.js`を書き換え、手順5でバックグラウンドで起動したプロセスを落とし、再度`node server.js`で起動します。

## 7.ホスト側で再度起動確認
curlで再び確認します。

```bash:ホスト側からの起動確認
curl localhost:8080
[結果]
Hello World
```

ホスト側であるMacから接続できるようになりました。

## 最後に
今回、私は比較的簡単なところでつまずいてしまいました。少し考えれば分かりそうだったのですが沼にハマって四苦八苦していました。
冒頭にも記載した通り、今回の手順は検証と勉強を兼ねて書いています。
本来は、`docker run`して-itでコンテナ内に入って`node start.js`を実行、という手順ではなく例えば以下のようにサーバ起動コマンドを組み込むか
dockerfileを作成する方が一般的なのかなと思います。

```
docker run -d -v /Users/XXX/dockerShare:/var/nodeapp -p 8080:3000 -w /var/nodeapp/sample node server.js
```