はじめに
dockerコンテナ上にnode.jsでserverを起動し、ホストであるMacから接続するまでの手順を記載します。
本手順は私が引っかかった罠を再現しつつ記載します。
そのため「そんなことはいいからdocker上でのnodeの起動方法だけ知りたい」という方は別のサイトでご確認ください。
後、私自身があまりdockerに慣れていないためコマンドの説明を入れながら記載します。
作業環境
- ホスト側
- macOS Sierra 10.12.1
- docker for Mac
v1.12.5v17.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つの操作を一度に行ってくれます。
この3つの操作の概要について簡単に説明しようと思います。
追記
docker run
コマンドの新コマンドはdocker container run
となりました。コマンドでできることは同じのようです。
基本的にdocker container XX
というコマンドになっているものが多いため以降の説明ではそうなっていないコマンドだけ追記を入れました。
なお、最新verでこの記事の手順が通用することも確認しました。
dockerイメージの取得
ローカルマシンにイメージが存在しない場合はdocker hub
という外部サービスからイメージを取得します。
既にローカルマシン上にイメージが存在する場合は、ローカルのイメージを使用します。
正確にはdocker run
コマンドでは、どのイメージをベースにコンテナを生成するのか指示する必要があるためイメージ名の指定が必須です。指定したイメージがローカルに存在する場合はそれを、存在しない場合はdocker hub
からイメージ名で検索し存在すれば取得します。
このような動きをするため、初回実行時はイメージをダウンロードしてくる時間が結構かかりdocker run
コマンドの実行自体も比較的長くなります。
イメージを削除しない限りローカルのイメージを使うので、2回目以降は高速になります。
コンテナの生成
docker run
コマンドではコンテナ生成も一緒に行ってくれます。コンテナ生成を単独で行う場合、docker create
コマンドを使用します。
そのためコンテナを1つも作成していない状態で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
でサーバ起動するためポートマッピングを行います。
docker run -v [ホスト側共有ディレクトリ]:[コンテナ側共有ディレクトリ] -p 8080:3000 -it node /bin/bash
以下、オプションの簡単な説明です。
-vオプションについて
Mac側のディレクトリをコンテナ内へマウントすることができます。例えばMac側の/User/XXX/dockershareディレクトリとコンテナ内の/var/nodeappを共有したい場合、以下のようにします。
-v /User/XXX/dockershare:/var/nodeapp
ホスト側共有ディレクトリは予め適当な作業ディレクトリを決めてそこを指定してください。
コンテナ側共有ディレクトリはそのディレクトリが存在している必要はありません。コンテナ生成時に勝手に作成されマウントされます。
-pオプションについて
外部からコンテナにアクセスするためのポートフォワード指定を行うオプションです。上記コマンド例ではホスト側から8080ポートにアクセスがあった場合、コンテナ側の3000ポートへフォワードします。
手順ではコンテナ内でnode.js
のサーバを起動し3000番ポートでlistenしますのでこの指定としました。
-itオプションについて
iは標準入力(STDIN)を開くオプションで、tはtty(端末デバイス)を割り当てるオプションです。
node
という指定はdocker
イメージ名で、生成するコンテナの元となるイメージとなります。最後の/bin/bash
は実行コマンドを指定しています。
この指定をするとbashでコンテナ内の操作ができるようになります。
なお、exit
コマンドでコンテナから抜けることができますが、このときコンテナも同時に停止します。
ホスト側でコンソールを2つ起動し、片方で上記コマンドを実行しもう片方でdocker ps
を実行すると状態がよくわかるかと思います。
2.server.jsの作成
docker run
コマンド実行時に指定したホスト側の共有ディレクトリにserver.js
を作成します。
ネットでよく出てくるHello worldを返すだけのサンプルプログラムです。
(実はこのプログラムではうまくいきません。あとで解消するのでこのままいきます。)
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
があるディレクトリで以下のコマンドを実行します。
node server.js &
4.コンテナ内でのサーバ起動確認
以下のコマンドで起動確認をします。
curl localhost:3000
[結果]
Hello World
問題なく起動していることを確認しました。
5.ホスト側での起動確認
Mac側からサーバに接続できるか確認します。
docker run
コマンド実行時に指定した通り、ホスト側ポート8080はコンテナ側ポートの3000にフォワードするはずなので、以下のようにすればいけるはずです。
curl localhost:8080
[結果]
curl: (52) Empty reply from server
エラーになりました。原因を追ってみます。
まずコンテナを起動した状態で別のコンソールを立ち上げてdocker ps
を実行します。PORTSの部分が以下のようになっているはずです。
このPORTSはポートフォワーディングの設定です。
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するホストはパブリックインタフェースにする必要があるようです。
色々試した結果、ホスト側から通るパターンは以下の3つと思われます。
// パターン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
で調べることができます。
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で再び確認します。
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