このチュートリアルでは、Nodeアプリケーションをローカルで作成・実行する代わりに、Docker Nodeの公式イメージがベースとするDebian Linuxオペレーティングシステムのメリットを活用する方法について紹介します。ここでは、ポータブルなNode開発環境を作成し、開発者が「自分のマシンで実行しているはずなのに?」といぶかしがる問題を解決します。これは、いずれかのプラットフォームでDockerイメージを実行すると、コンテナが先読みして作成されることで発生するものです。
このチュートリアルでは、次の2つの空間を使います。
ローカルのオペレーティングシステム:TerminalやPowerShellなどのCLIアプリケーションを使って、Dockerのローカルインストールを使用し、イメージをビルドしてコンテナとして実行します。
コンテナのオペレーティングシステム:Dockerコマンドを使って、実行中のコンテナのベースのオペレーティングシステムにアクセスします。このコンテキストの中でコンテナのシェルを使ってコマンドを実行し、Nodeアプリケーションの作成と実行を行います。
コンテナのオペレーティングシステムは、ローカルのオペレーティングシステムとは別に実行します。コンテナ内で作成したファイルは、ローカルではアクセスできません。コンテナ内で実行しているサーバーは、ローカルのウェブブラウザでのリクエストをリッスンできません。これは、ローカル開発には理想的ではありません。この制約を解決するには、次のことを行って2つのシステム間を橋渡しします。
コンテナのファイルシステムにローカルのフォルダをマウントする:このマウントポイントをコンテナの作業ディレクトリとして使うと、コンテナ内で作成したすべてのファイルをローカルで永続化することができます。また、ローカルで行われたプロジェクトファイルへの変更をコンテナに伝えることもできます。
ホストにコンテナネットワークの操作を許可する:ローカルポートをコンテナポートにマッピングすると、ローカルポートへのHTTPリクエストはすべてDockerによってコンテナポートにリダイレクトされます。
このDockerベースのNode環境戦略を実際に使ってみるには、基本的なNode Expressウェブサーバーを作成します。さあ、始めてみましょう。
Nodeをインストールする負担を取り除く
シンプルな"Hello World!"というNodeアプリケーションを実行するには、一般的なチュートリアルでは次のことをやるようにと書かれています。
- Nodeをダウンロードしてインストールする
- Yarnをダウンロードしてインストールする
- Nodeの別のバージョンを使うには、Nodeをアンインストールしてから、
nvm
をインストールする - NPMパッケージをグローバルにインストールする
-><-
どのオペレーティングシステムにも、上記のインストールが一筋縄ではいかないそれぞれの癖があります。しかし、NodeエコシステムへのアクセスはDockerイメージを使うことで標準化することができます。このチュートリアルでインストールが必要なのはDockerだけです。Dockerのインストールが必要な場合は、このDockerインストールドキュメントから使用しているオペレーティングシステムを選択し、次の手順に従ってください。
NPMと同様に、Dockerは数多くのDockerイメージが登録されているDocker Hubにアクセスできます。このDocker Hubから、Nodeのさまざまなバージョンをイメージとして実行することができます。これらのイメージは、ローカルプロセスとして他と重複・競合せずに実行できます。Node 8 with NPMやNode 11 with Yarnに依存するクロスプラットフォームプロジェクトを同時に作成することもできます。
プロジェクトの基礎を作成する
まず、システムのどこかにnode-docker
フォルダを作成します。これがプロジェクトディレクトリになります。
Node Expressサーバーを実行することを目標に、node-docker
プロジェクトディレクトリの下にserver.js
ファイルを作成し、次のように入力して保存します。
// server.js
const express = require("express");
const app = express();
const PORT = process.env.PORT || 8080;
app.get("/", (req, res) => {
res.send(`
<h1>Docker + Node</h1>
<span>A match made in the cloud</span>
`);
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}...`);
});
Nodeプロジェクトにはpackage.json
ファイルとnode_modules
フォルダが必要です。Nodeがシステムにインストールされていない場合は、Dockerを使ってこれらのファイルを構造化ワークフローに従って作成します。
コンテナのオペレーティングシステムにアクセスする
コンテナOSには、次のメソッドでアクセスします。
1つのdocker run
コマンドを使用する
次のコマンドを実行します。
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
コンテナシェルへのアクセスがどのように行われるかを理解するために、このdocker run
コマンドを分析してみましょう。
docker run --rm -it
docker run
は、新しいコンテナインスタンスを作成します。コンテナが存在するようになると、--rm
フラグがコンテナを自動的に停止・削除します。-i
と-t
を組み合わせたフラグは、シェルなどのインタラクティブプロセスを実行します。-i
フラグは、STDIN(Standard Input)を開いたままの状態にし、その間に-t
フラグがプロセスがテキストターミナルになりすまして信号をわたします。
--rm
は、ことわざの「out of sight, out of mind」のようなもので、見なければ忘れてしまいます。
-it
チームがいないと、画面には何も表示されません。
-><-
docker run --rm -it --name node-docker
--name
フラグは、ログやテーブルで見つけやすいように、コンテナにわかりやすい名前を付けます。たとえば、docker ps
を実行したときなどです。
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app
-v
フラグは、このマッピングを引数として使い、ローカルフォルダをコンテナフォルダにマウントします。
<HOST FOLDER RELATIVE PATH>:<CONTAINER FOLDER ABSOLUTE PATH>
環境変数は、MacやLinux上でコマンド$PWD
を、Windows上でコマンド$CD
を実行したときに、現在の作業ディレクトリをプリントすることができます。-w
フラグは、マウントポイントをコンテナの作業ディレクトリとして設定します。
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000
-e
フラグは、環境変数PORT
に値3000
を設定します。-p
フラグは、ローカルポート8080
をコンテナポート3000
にマッピングし、server.js
の中で消費される環境変数PORT
を照合します。
const PORT = process.env.PORT || 8080;
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
セキュリティの保護とファイル権限の問題を回避するため、-u
フラグによって、Nodeイメージ内にコンテナプロセスを実行するユーザーとしてルート以外のユーザーnode
を設定します。フラグを設定すると、実行するイメージがnode:latest
に指定されます最後の引数は、実行中にコンテナ内で実行するコマンドです。/bin/bash
によって、コンテナシェルが呼び出されます。
イメージがローカルにない場合は、Dockerがバックグラウンドで
docker pull
を発行し、Docker Hubからダウンロードします。
コマンドが実行されると、次のコンテナシェルのプロンプトが表示されます。
node@<CONTAINER ID>:/home/app$
次のメソッドに移る前に、exit
と入力して
Dockerfile
を使用する
前のセクションのdocker run
コマンドは、イメージのビルドタイムとコンテナのランタイムフラグおよび要素から構成されます。
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
イメージのビルドタイムに関連するものはすべて、次のようにDockerfile
を使ってカスタムイメージとして定義できます。
FROM
は、コンテナのベースイメージnode:latest
を指定します。WORKDIR
は、-w
を定義します。USER
は、-u
を定義します。ENV
は、-e
を定義します。ENTRYPOINT
は、コンテナが実行されると/bin/bash
の実行を指定します。
これにもとづき、node-docker
プロジェクトディレクトリの下にDockerfile
という名前のファイルを作成し、次のように入力して保存します。
FROM node:latest
WORKDIR /home/app
USER node
ENV PORT 3000
EXPOSE 3000
ENTRYPOINT /bin/bash
EXPOSE 3000
は、ランタイムに表示するポートを記述します。ただし、コンテナの名前、ポートマッピング、ボリュームマウンティングを定義するコンテナのランタイムフラグは、docker run
で指定する必要があります。
Dockerfile
内で定義するコンテナイメージは、実行する前にdocker build
を使ってビルドする必要があります。ローカルターミナルで、次のように実行します。
docker build -t node-docker .
docker build
は、-t
フラグを使ってイメージにnode-docker
というわかりやすい名前をつけます。これは、コンテナの名前とは別のものです。イメージが作成されたことを確認するには、docker images
を実行します。
イメージが作成されたら、次の短いコマンドを実行してサーバーを実行します。
docker run --rm -it --name node-docker \
-v $PWD:/home/app -p 8080:3000 \
node-docker
コンテナシェルのプロンプトは、次の形式で表示されます。
node@<CONTAINER ID>:/home/app$
もう一度、次のメソッドに移る前に、exit
と入力して
Docker Composeを使用する
Linuxの場合は、Docker Composeは個別にインストールします。
前のセクションのDockerfile
と短いdocker run
コマンドにもとづいて、Docker ComposeのYAMLファイルを作成し、Node開発環境をサービスとして定義します。
Dockerfile
:
FROM node:latest
WORKDIR /home/app
USER node
ENV PORT 3000
EXPOSE 3000
ENTRYPOINT /bin/bash
Command
docker run --rm -it --name node-docker \
-v $PWD:/home/app -p 8080:3000 \
node-docker
抽象化するdocker run
コマンドの要素は、コンテナ名、ボリュームマウンティング、ポートマッピングのみです。
node-docker
プロジェクトディレクトリの下にdocker-compose.yml
という名前のファイルを作成し、次のように入力して保存します。
version:"3"
services:
nod_dev_env:
build: .
container_name: node-docker
ports:
- 8080:3000
volumes:
- ./:/home/app
nod_dev_env
は、サービスにわかりやすい名前を付けます。build
は、Dockerfile
へのパスを指定します。container_name
は、コンテナにわかりやすい名前を付けます。ports
は、ホストからコンテナへのポートマッピングを構成します。volumes
は、ローカルフォルダからコンテナフォルダへのマウンティングポイントを定義します。
このサービスを開始して実行するには、次のコマンドを実行します。
docker-compose up
up
は、自らのイメージとコンテナを、前に使ったdocker run
コマンドとdocker build
コマンドで作成されたものから個別にビルドします。これを検証するには、次を実行します。
docker image
# Notice the image named <project-folder>_nod_dev_env
docker ps -a
# Notice the container named <project-folder>_nod_dev_env_<number>
up
は、イメージとコンテナを作成しますが、コンテナシェルのプロンプトは表示されません。これは、docker-compose.yml
で定義したフルサービスをup
が開始するためです。ただしインタラクティブな出力は表示せず、代わりに静的なサービスログのみを表示します。インタラクティブな出力を得るには、代わりにdocker-compose run
を使って、nod_dev_env
を個別に実行します。
まず、up
で作成したイメージやコンテナを消去するために、ローカルターミナルで次のコマンドを実行します。
docker-compose down
さらに、次のコマンドでサービスを実行します。
docker-compose run --rm --service-ports nod_dev_env
run
コマンドは、docker run -it
と同じように動作しますが、コンテナポートをホストにマッピングすることも表示することもありません。Docker Composeファイルの中で構成したポートマッピングを使用するには、--service-ports
フラグを使います。コンテナシェルのプロンプトが、次の形式でもう一度表示されます。
node@<CONTAINER ID>:/home/app$
何らかの理由でDocker Composeファイルに指定したポートが使用中の場合は、--publish
, (-p
)フラグを使って別のポートマッピングを手動で指定できます。たとえば、次のコマンドはホストポート4000
をコンテナポート3000
にマッピングします。
docker-compose run --rm -p 4000:3000 nod_dev_env
依存関係をインストールしてサーバーを実行する
アクティブなコンテナシェルがない場合は、上記のいずれかのメソッドを使ってアクセスします。
コンテナシェルで、Nodeプロジェクトを初期化し、次のコマンドを発行して依存関係をインストールします(
npm
を使うこともできます)。
yarn init -y
yarn add express
yarn add -D nodemon
package.json
とnode_modules
がローカルのnode-docker
プロジェクトディレクトリの下にできていることを確認します。
nodemon
は、ソースコードに変更を加えるたびにサーバーを自動的に再起動し、開発ワークフローの効率化を図ります。nodemon
を構成するには、package.json
を次のように更新します。
{
// Other properties...
"scripts": {
"start": "nodemon server.js"
}
}
コンテナシェル内で、yarn start
を実行してNodeサーバーを実行します。
サーバーをテストするには、ローカルブラウザを使ってhttp://localhost:8080/
にアクセスします。Dockerは、ホストポート8080
からコンテナポート3000
にリクエストを自動的にリダイレクトします。
ローカルファイルのコンテンツとサーバーの接続をテストするには、server.js
をローカルで開き、レスポンスを次のように更新して変更を保存します。
// server.js
// package and constant definitions...
app.get("/", (req, res) => {
res.send(`
<h1>Hello From Node Running Inside Docker</h1>
`);
});
// server listening...
ブラウザのウィンドウを閉じ、新しいレスポンスを調べます。
プロジェクトを変更・拡張する
Nodeがローカルシステムにインストールされていない場合は、ローカルターミナルを使ってプロジェクトの構造とファイルのコンテンツを修正できますが、yarn add
などのNode関連のコマンドは発行できません。コンテナなしでサーバーを実行すると、内部のコンテナポート3000
へのサーバーリクエストも行えません。
コンテナ内のサーバーを操作したり、Nodeプロジェクトに変更を加えたい場合は、docker exec
を使って、実行中のコンテナとそのIDにコマンドを実行する必要があります。docker run
コマンドは、分離した新しいコンテナを作成するため、使用しません。
実行中のコンテナのIDは、簡単に取得できます。
- すでにコンテナシェルが開いている場合は、コンテナIDがシェルプロンプトに表示されています。
node@<CONTAINER ID>:/home/app$
- コンテナIDは、プログラミング的に取得することもできます。
docker ps
を使い、一致するコンテナのCONTAINER ID
を返すように、名前でフィルタします。
docker ps -qf "name=node-docker"
-f
フラグは、name=node-docker
の条件にもとづいてコンテナをフィルタします。-q
(--quiet
)は、出力の中から一致するコンテナのIDだけを表示するように制限します。実質上、docker execコマンドの中に
node-dockerの
CONTAINER ID`が組み込まれます。
コンテナIDを取得したら、docker exec
を使って次のことができます。
- 実行中のコンテナシェルの新しいインスタンスを開きます。
docker exec -it $(docker ps -qf "name=node-docker") /bin/bash
- 内部ポート
3000
を使ってサーバーリクエストを出します。
docker exec -it $(docker ps -qf "name=node-docker") curl localhost:3000
- 依存関係をインストールまたは削除します。
docker exec -it $(docker ps -qf "name=node-docker") yarn add body-parser
もう一つのアクティブなコンテナシェルができたら、代わりにそこで
curl
、yarn add
を簡単に実行できます。
まとめ... 小さなウソをばらすと
ここでは、さまざまなレベルの複雑さで分離したNode開発環境を作成する方法を紹介しました。1つのdocker run
コマンドを使用する方法、Dockerfile
を使ってカスタムイメージをビルドして実行する方法、Docker Composeを使ってコンテナをDockerサービスとして実行する方法などです。
それぞれのレベルはより多くのファイル構成が必要ですが、コンテナを実行するコマンドはより短くなります。これは、構成をファイルにカプセル化することで環境がポータブルなものになり、維持しやすくなるので、価値あるトレードオフです。さらに、実行中のコンテナを操作してプロジェクトを拡張する方法も紹介しました。
IDEの場合は、構文アシスタントが使うにはNodeをローカルにインストールしなければなりません。もしくは、vim
などのCLIエディタをコンテナ内で使うこともできます。
-><-
それでも、分離した開発環境の恩恵は受けられます。プロジェクトのセットアップ、インストール、コンテナ内で実行するランタイムの手順に制限を課すと、チーム全員が同じバージョンのLinuxでコマンドを実行するようになるので、これらの手順を標準化できます。さらに、Nodeツールで作成したキャッシュや隠れファイルはすべて、コンテナの中に閉じ込めておけるので、ローカルシステムを汚すことはありません。しかも、yarn
が何とタダで手に入ります。
JetBrainsは、デバッグアプリケーションを実行しているときに、DockerイメージをNodeやPythonのリモートインタープリタとして使用する機能の提供を始めました。将来的には、これらのツールをシステムにダウンロードしてインストールする必要がまったくなくなるかもしれません。開発者の環境を標準化し、ポータブルなものとするために、業界が何をもたらしてくれるか、これからも注目していきましょう。
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.