これまで開発環境でのDockerの使用について学んできました。
Docker Composeまでなんとなくわかってきたので、今回は実際のプロジェクトにおけるDockerの使い方を学ぶことにしました。
私がReactを使っているということもあり、Create React Appのプロジェクトを開発および本番環境で実行することを目標にしました。
開発環境用Dockerfileの作成
npx create-react-app [プロジェクト名]で適当なプロジェクトを作ります。
開発環境と本番環境ではサーバが異なります。そのため、Dockerfileも開発用と本番用で2つ必要になります。
ファイル名がかぶらないように、開発用はDockerfile.devという名前にします。
FROM node:alpine
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "run", "start"]
イメージをビルドするときは、docker build -f Dockerfile.dev . で実行することができます(ファイル名に.devとつくため、docker build .ではビルドできません)。
ビルドの過程でコンテナのファイルシステム上にnode_modulesがつくられます(その後にコンテナは停止してファイルシステムのスナップショットがコピーされたイメージがつくられます)。
そのため、プロジェクトフォルダ直下にあるnode_modulesは削除して問題ありません。
-pフラグをつけることで、localhostのポート(3000)とコンテナのポート(3000)を関連付けて、コンテナを起動することができます。
docker run -it -p 3000:3000 [image id]
これでブラウザからプロジェクトを開けるようになりました。
ボリューム
無事に開発環境ができたのでプロジェクト作成を進めようとすると、ここで思わぬ落とし穴にハマります。
それは、ファイルの変更内容がブラウザに反映されないということです。
何故このようなことが起こるかというと、コンテナのファイルシステムはローカルのファイルから独立しているためです。
つまり、ファイルの変更を反映させるためには、再度イメージをビルドしてコンテナを立ち上げる必要があります。...超面倒くさい。
この問題を解決するのがボリュームです。
ボリュームというのは、コンテナの外にあるデータの置き場のことです。コンテナ内部にデータ(ファイル)を保存してもコンテナを破棄すると消えてしまうため、永続化したいデータがある場合にボリュームは使われます。
ボリュームはコンテナにマウントされる(-v)ことで、コンテナからのアクセスが可能になります。
また、ボリューム(データの置き場)には以下の2つがあります。
- ホストのディレクトリ(ローカル)
- Docker管理下のストレージ領域
ホストのディレクトリをボリュームとしてコンテナにマウントすることで、/srcや/puclicフォルダ内の変更がコンテナにも反映されるようになります。

また、node_modulesについてはビルドの過程でつくられるため、ホストのディレクトリには置く必要がありません。その代わりにDocker管理化のリソースとして扱います。
コンテナの起動とボリュームのマウントを同時に行うには以下のコマンドを実行します。
docker run -p 3000:3000 -v /app/node_modules -v $(pwd):/app [image id]
-v $(pwd):/appの部分で、ローカルのカレントディレクトリをコンテナの/appにマウントします。
これによってコンテナからローカルのディレクトリを参照できるようになります。
つぎに、-v /app/node_modulesでnode_modulesをDockerのリソースとして扱います。
-v $(pwd):/appでローカルのnode_modulesはすでに削除しているため、こちらの記述がないとエラーになります。
docker-compose.ymlは以下のように書くことができます。
version: '3'
services:
web: //起動するコンテナ名
stdin_open: true
build: //ビルドするDockerfileの指定
context: .
dockerfile: Dockerfile.dev //ファイル名も指定する
ports: //ローカルとコンテナのポートの関連付け
- '3000:3000'
volumes: //マウントするボリューム
- /app/node_modules
- .:/app
テスト用コンテナの作成
Create React AppでつくられたプロジェクトにはすでにJestが含まれています。
そのため、docker run -it [image id] npm run testとすることでテストを実行することができます。
対話型になっているため、-itでターミナルとコンテナ内のJest CLIの標準入力を接続する必要があります。
Watch Usage
› Press a to run all tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
また、docker-compose.ymlにtestsコンテナの記述を加えると、upしたときにwebコンテナとtestsコンテナが同時に立ち上がります。
testsコンテナではファイルの変更を反映(watch)しながらテストを実行します。
testsもwebと同じDockerfile.devを素にしているので、実行コマンドをcommand: ['npm', 'run', 'test']として上書きしてあげる必要があります。
version: '3'
services:
web:
stdin_open: true
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '3000:3000'
volumes:
- /app/node_modules
- .:/app
tests:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- /app/node_modules
- .:/app
command: ['npm', 'run', 'test'] //コマンドを上書きする
ついでに、ターミナルとコンテナの標準入出力を接続する方法には、attachとexecの2種類あります。
例えば、docker-compose upでコンテナを起動した後、ターミナルの別タブを開いてdocker psでtestsコンテナのid(container id)を調べ、docker attach [testsのcontainer id]またはdocker exec -it [testsのcontainer id] sh接続することができます。
どちらもコンテナへ接続することに変わりはないのですが、attachでは、コンテナで起動しているPID=1のプロセスの標準入出力に接続し、execではコンテナで任意のコマンドを実行させるという違いがあります。
本番環境用Dockerfileの作成
本番環境でコンテナを使う場合、nginxのようなWEBサーバが必要となります。
そのため、Dockerfileの中身もそれに対応させる必要があります。

nginxで作成したプログラムを動かすためには、プログラムを実行できるような形に変換してあげる必要があり、これをビルドといいます。
つまり、Dockerfileにこのビルドのフェーズとビルドしたプログラムをnginxに配置する実行のフェーズを書いてあげれば、本番環境をたちあげることができます。
// ビルドフェーズ
FROM node:alpine
WORKDIR '/app'
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
//実行フェーズ
FROM nginx
COPY --from=0 /app/build /usr/share/nginx/html
ビルドフェーズで、npm run buildが実行されると、実行可能なプログラムがまとめられたbuildフォルダがコンテナの/appにつくられます。
実行フェーズにおいては、COPY --from=0 /app/build /usr/share/nginx/htmlでbuildフォルダを/usr/share/nginx/htmlにコピー(ホスティング)しています。
--from=0では別のフェーズ(ビルドフェーズ)を指定しています。
docker build .でイメージをビルドし、docker run -p 8080:80 [image id]でローカルホストのポート8080とコンテナnginxのデフォルトポート番号80をマッピングし、コンテナを起動します。
localhost:8080にブラウザからアクセスすると、nginxに配置されたプログラムを開くことができるようになりました。

おわりに
次はGitHub ActionsかCircle CIでCI/CDの構築に挑戦してみたいと思います。
参考資料