🖥️

【3-3】Dockerfile入門!カスタムイメージ作成とNode.js開発環境のDocker化

に公開

Dockerfile入門!カスタムイメージ作成とNode.js開発環境のDocker化

はじめに

前回の記事では、docker rundocker volumeといった基本的なコマンドを学び、コンテナの操作とデータ管理の基礎を固めました。しかし、これまではDocker Hubで公開されている既存のイメージを使うだけでした。

https://zenn.dev/koikoi_infra/articles/b49afcc6a86fc8

今回は、いよいよDockerの真髄であるDockerfileを使い、自分だけのカスタムイメージを作成する方法を学びます。シンプルなWebサーバーから始め、より実践的なNode.jsアプリケーションの開発環境をDocker化するまでを、ステップバイステップで解説します。

この記事では、Dockerfileの書き方からdocker buildコマンドによるイメージのビルドまで、アプリケーションをコンテナ化するための一連のプロセスを、実際に手を動かしながら体験します。

実践①: シンプルなWebサーバーのイメージを作成する

まずはウォーミングアップとして、自作のHTMLファイルを表示するだけの、非常にシンプルなWebサーバーイメージを作成してみましょう。

Step 1: 準備(ディレクトリとHTMLファイルの作成)

ubuntu-server-01上で作業用のディレクトリを作成し、表示したいHTMLファイルを用意します。

# 作業用のディレクトリを作成して移動
mkdir ~/docker-practice
cd ~/docker-practice

# 表示するHTMLファイルを作成
echo '<h1>My Custom Web App</h1><p>Built with Docker!</p>' > index.html
【実践トラブル】echoコマンドで「!: event not found」?

実は最初、echo "..."のようにダブルクォートで上記のコマンドを実行したところ、-bash: !: event not foundというエラーに遭遇しました。

これは、LinuxのBashシェルが、ダブルクォート内であっても感嘆符!を「コマンド履歴の呼び出し」という特別な機能として解釈しようとしたためです。

解決策
echo '...'のようにシングルクォートで文字列全体を囲むことで、!を含むすべての文字が特殊な意味を持たなくなり、ただの文字列として扱われます。これにより、無事にindex.htmlを作成できました。シェルの挙動を学ぶ良い機会となりました。

Step 2: Dockerfileの作成と解説

次に、イメージの設計図となるDockerfileを作成します。

cat > Dockerfile << 'EOF'
# ベースとなるイメージを指定
FROM nginx:alpine

# ホストのファイルをコンテナ内にコピー
COPY index.html /usr/share/nginx/html/

# このコンテナは80番ポートを外部に公開できることを宣言
EXPOSE 80

# コンテナ起動時に実行されるデフォルトのコマンド
CMD ["nginx", "-g", "daemon off;"]
EOF

各命令の意味:

  • FROM nginx:alpine: イメージの土台です。軽量なnginx:alpineイメージをベースにします。
  • COPY index.html ...: ファイルの配置です。先ほど作成したindex.htmlを、Nginxがコンテンツを公開するディレクトリにコピーします。
  • EXPOSE 80: ポートの宣言です。このイメージが80番ポートを使うことをDockerに伝えます(実際にポートを開けるのはdocker run-pオプションです)。
  • CMD ["nginx", ... ]: 起動コマンドの指定です。このイメージからコンテナを起動した際に、デフォルトで実行されるコマンドを指定します。
補足:なぜ `/usr/share/nginx/html/` にコピーするの?

これは、FROM nginx:alpine で指定した Nginx公式イメージのデフォルト設定 に関係しています。

Webサーバー(Nginx)には、「外部からアクセスがあったときに、どのディレクトリのファイルを見せるか」という設定があり、これを「ドキュメントルート」と呼びます。

nginx イメージでは、このドキュメントルートがデフォルトで /usr/share/nginx/html/ ディレクトリに設定されています。

(デフォルトでは、この場所には「Welcome to Nginx!」と表示される用の index.html が入っています。)

したがって、COPY index.html /usr/share/nginx/html/ というコマンドは、「Nginxがデフォルトで参照する場所に、私たちが用意した index.html を上書きコピーする」という意味になります。

補足:CMD ["nginx", "-g", "daemon off;"] の意味は?

このコマンドは「Nginxサーバーをフォアグラウンド(前景)で起動する」という意味です。

Dockerコンテナは、「コンテナ起動時に実行されたメインのプロセス」が動き続けている間だけ、起動状態を維持するというルールがあります。

もし daemon off; がないと?

  • Nginxは通常、デーモン(daemon)と呼ばれるバックグラウンドプロセスとして起動するように設定されています。
  • もし CMD ["nginx"] だけで起動すると、Nginxはバックグラウンド(裏側)に移動し、メインの nginx コマンド自体は「起動完了」としてすぐに終了してしまいます。
  • メインプロセスが終了したと判断したDockerは、コンテナを「役割が終わった」とみなし、即座に停止させてしまいます。

daemon off; の役割
"-g", "daemon off;" という設定は、Nginxがバックグラウンドに移るのを防ぎ、フォアグラウンド(前景)で動作し続けるようにするおまじないです。

これによりメインプロセスが終了しなくなるため、コンテナは起動状態を保ち、Webサーバーとしてリクエストを待ち受け続けることができます。

Step 3: ビルドと実行(docker build, docker run)

設計図ができたので、docker buildコマンドでイメージを構築します。

# カレントディレクトリ(.)のDockerfileを元にイメージをビルド
docker build -t my-web-app:v1.0 .

# ビルドしたイメージからコンテナを起動
docker run -d --name custom-web -p 8082:80 my-web-app:v1.0
  • -t my-web-app:v1.0: ビルドするイメージに名前(my-web-app)とタグ(v1.0)を付けます。
  • .: Dockerfileがある場所(ビルドコンテキスト)を指定します。ここではカレントディレクトリを指します。

ブラウザでhttp://192.168.3.101:8082にアクセスし、"My Custom Web App"と表示されれば成功です!

補足:ビルド時の Dockerfile とタグの指定

Q1. カレントディレクトリにDockerfileが複数あったら?

A1. docker build . のように -f オプションを省略した場合、Dockerは Dockerfile という名前のファイル(大文字のDから始まる) だけを探しにいきます。

もし Dockerfile.devDockerfile.prod といった別の名前のファイルを使いたい場合は、-f (または --file) オプションで明示的に指定する必要があります。

# Dockerfile.dev を使ってビルドする例
docker build -t my-app-dev -f Dockerfile.dev .

Q2. イメージ名だけでタグを省略したら?

A2. タグ(:v1.0 など)を省略すると、自動的に :latest というタグが割り当てられます。

例えば、docker build -t my-web-app . と実行した場合、docker build -t my-web-app:latest . と実行したのと同じ意味になります。

補足:`daemon off` (Dockerfile内) と `-d` (docker run) は矛盾しない?

これはDockerの仕組みを理解する上でとても良い質問です。結論から言うと、この2つは制御している階層が違うため、矛盾していません。

  • CMD ["nginx", "-g", "daemon off;"]

    • 階層: コンテナ内部 の話
    • 役割: Nginxをコンテナの**フォアグラウンド(前景)**で実行します。
    • 理由: Dockerコンテナは、メインのプロセスが終了すると停止してしまいます。daemon off を指定してNginxをフォアグラウンドで動かし続けることで、「コンテナを生き続けさせる」ことができます。
  • docker run -d

    • 階層: ホストマシン(あなたのPC) から見た話
    • 役割: コンテナ全体を、ホストマシンの**バックグラウンド(裏側)**で実行します。
    • 理由: これを指定しないと、ターミナルがコンテナ(Nginx)のログ出力に占有されてしまいます。-d を使うことで、ターミナルをすぐに解放し、他のコマンドを打てるようにします。

まとめ

CMD ... "daemon off;" は「コンテナの中身(Nginx)を表で動かし続けさせる」ための設定で、
docker run -d は「コンテナ全体をホストの裏で動かす」ための設定です。

この2つは、それぞれ異なるレベルで動作しており、両方を指定することで「ターミナルを占有せず、かつ、Nginxサーバーが動き続けるコンテナ」を実現できます。

実践②: Node.js開発環境をDocker化する

次に、より実践的な例として、Node.js(Express)で書かれたWebアプリケーションの開発環境をDockerイメージにしてみましょう。

Step 1: アプリケーションの準備

新しいディレクトリを作成し、Node.jsアプリのソースコードと設定ファイルを用意します。

mkdir ~/nodejs-app
cd ~/nodejs-app
# package.jsonを作成 (アプリの依存ライブラリなどを定義)
cat > package.json << 'EOF'
{
  "name": "docker-node-app",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": { "start": "node app.js" },
  "dependencies": { "express": "^4.18.0" }
}
EOF

# app.jsを作成 (Webサーバーの処理本体)
cat > app.js << 'EOF'
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('<h1>Hello Docker!</h1><p>Node.js Express App</p>');
});

app.listen(port, () => {
  console.log(`App running at http://localhost:${port}`);
});
EOF

Step 2: より高度なDockerfileの作成と解説

Node.jsアプリのDockerfileは、ライブラリのインストール工程が加わるため、少し複雑になります。

cat > Dockerfile << 'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF

各命令の意味:

  • WORKDIR /app: コンテナ内での作業ディレクトリを指定します。これ以降の命令(COPYやRUN)は、この/appディレクトリを基準に実行されます。
  • COPY package*.json ./: package.json(とpackage-lock.json)だけを先にコピーします。
  • RUN npm install: イメージをビルドする過程で実行されるコマンドです。package.jsonを元に、expressなどの必要なライブラリをインストールします。
  • COPY . .: app.jsなど、残りのソースコードを全てコピーします。
【Tips】なぜCOPYを2回に分けるのか?

このCOPYを2段階に分けるのには、ビルド時間の短縮という大きな目的があります。

DockerはDockerfileの各行を「レイヤー」としてキャッシュします。package.jsonに変更がない限り、時間のかかるRUN npm installのレイヤーはキャッシュが再利用されます。これにより、app.jsのちょっとした修正のたびにnpm installが走り直すのを防ぎ、ビルドを劇的に高速化できるのです。

Step 3: ビルドと実行

先ほどと同じように、ビルドして実行します。

# Node.jsアプリのイメージをビルド
docker build -t nodejs-app:v1.0 .

# ビルドしたイメージからコンテナを起動
docker run -d --name my-node-app -p 3000:3000 nodejs-app:v1.0

ブラウザでhttp://192.168.3.101:3000にアクセスし、"Hello Docker!"と表示されれば成功です。docker logs my-node-appでコンテナのログも確認してみましょう。

まとめ

これで、既存のイメージを使うだけでなく、自分たちのアプリケーションに最適化されたイメージを自由に作成できるようになりました。

次回予告

毎回docker runで長いオプションを指定し、複数のコンテナを一つずつ起動するのは大変です。

次回は、複数のコンテナ構成を一つのファイルでスマートに管理・起動できるオーケストレーションツール、Docker Composeについて学んでいきたいと思います!

Discussion