developers

Dockerを使ってNode開発環境を作る

Dockerのイメージとコンテナを活用して、サーバーを実行する分離したNode開発環境を作る

Feb 26, 20194 min read

このチュートリアルでは、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.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エディタをコンテナ内で使うこともできます。

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

それでも、分離した開発環境の恩恵は受けられます。プロジェクトのセットアップ、インストール、コンテナ内で実行するランタイムの手順に制限を課すと、チーム全員が同じバージョンの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.