このチュートリアルでは、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パッケージをグローバルにインストールする

Ain't nobody got time for that!

どのオペレーティングシステムにも、上記のインストールが一筋縄ではいかないそれぞれの癖があります。しかし、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チームがいないと、画面には何も表示されません。

Hello, IT, have you tried turning it off and on again?

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.jsonnode_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-dockerCONTAINER 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

もう一つのアクティブなコンテナシェルができたら、代わりにそこでcurlyarn addを簡単に実行できます。

まとめ... 小さなウソをばらすと

ここでは、さまざまなレベルの複雑さで分離したNode開発環境を作成する方法を紹介しました。1つのdocker runコマンドを使用する方法、Dockerfileを使ってカスタムイメージをビルドして実行する方法、Docker Composeを使ってコンテナをDockerサービスとして実行する方法などです。

それぞれのレベルはより多くのファイル構成が必要ですが、コンテナを実行するコマンドはより短くなります。これは、構成をファイルにカプセル化することで環境がポータブルなものになり、維持しやすくなるので、価値あるトレードオフです。さらに、実行中のコンテナを操作してプロジェクトを拡張する方法も紹介しました。

IDEの場合は、構文アシスタントが使うにはNodeをローカルにインストールしなければなりません。もしくは、vimなどのCLIエディタをコンテナ内で使うこともできます。

「構文アシスタントが使うにはNodeをローカルにインストールしなければならないなんて!」と、ハリー・ポッターが怒っています。

それでも、分離した開発環境の恩恵は受けられます。プロジェクトのセットアップ、インストール、コンテナ内で実行するランタイムの手順に制限を課すと、チーム全員が同じバージョンのLinuxでコマンドを実行するようになるので、これらの手順を標準化できます。さらに、Nodeツールで作成したキャッシュや隠れファイルはすべて、コンテナの中に閉じ込めておけるので、ローカルシステムを汚すことはありません。しかも、yarnが何とタダで手に入ります。

JetBrainsは、デバッグアプリケーションを実行しているときに、DockerイメージをNodeやPythonのリモートインタープリタとして使用する機能の提供を始めました。将来的には、これらのツールをシステムにダウンロードしてインストールする必要がまったくなくなるかもしれません。開発者の環境を標準化し、ポータブルなものとするために、業界が何をもたらしてくれるか、これからも注目していきましょう。

About Auth0

Auth0, the identity platform for application builders, provides thousands of customers in every market sector with the only identity solution they need for their web, mobile, IoT, and internal applications. Its extensible platform seamlessly authenticates and secures more than 2.5 billion logins per month, making it loved by developers and trusted by global enterprises. The company's U.S. headquarters in Bellevue, WA, and additional offices in Buenos Aires, London, Tokyo, and Sydney, support its global customers that are located in 70+ countries.

For more information, visit https://auth0.com or follow @auth0 on Twitter.