Edited at

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

More than 1 year has passed since last update.


はじめに

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つの操作を一度に行ってくれます。

この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オプション例

-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を返すだけのサンプルプログラムです。

(実はこのプログラムではうまくいきません。あとで解消するのでこのままいきます。)


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があるディレクトリで以下のコマンドを実行します。


サーバ起動

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つと思われます。


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で調べることができます。


コンテナの詳細情報を表示

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