👌

【超簡単】Dockerを完全に理解する

に公開

はじめに

本職でDockerを用いたWebアプリの開発をすることになりました。今まで存在は知っていたものの、どういったものなのかなどが分からずに手を出していませんでした。
前任者がDockerを用いて行なっていたため、その方法を引き継いで作業をすることになり、今回勉強してみました。

今回の記事では「ざっくりと、完全に理解した!」状態になることを目的としています。そのため説明はかなり簡潔になっています。私自身まだまだ知識が浅い状態ですので、誤りがあったら訂正していただければと思います。
既にある程度知識があり具体的な内容が知りたい方は、別の記事を読まれることをオススメします。

ひとことで言うと

まず結論から、ひとことで言えば「コンテナ技術を使った仮想環境」です。
「そんなことはわかっとるわぃ」と言われる方が大半な気はしますが、これはかなり重要なことです。
では何故この技術が注目されるようになったのか、また他の仮想化技術とはどこが異なるのかなどみてみましょう。

クラウドの流行〜Dockerの登場

Dockerは、2013年に登場しました。このDockerとクラウドサーバーは切っても切れない関係です。
クラウドサーバーでアプリケーションを運用する場合、ほぼ必ず仮想環境を使用すると思います。
Dockerがなかった頃には、VMなどを使用していました。しかしそこには問題がありました。それは、、、

【本番環境と検証環境で動作が同じになる担保を取るのが非常に難しかった】

と言うことです。
これには、仮想化の根本の仕組みの違いが関係します。

VMとDockerの違い

さて、先ほど出てきたVMとDockerで何が違うのかについてです。

主な違いをまとめると、以下のようになります。

  • VM ... システム全体を仮想化する
  • Docker ... システムの構成要素ごとに仮想化する

例えばWebアプリケーションで考えると、構成要素は以下のようになります。

  1. Webサーバー(apache/nginx/node.jsなど)
  2. アプリケーション(Laravel/React/Next.jsなど)
  3. データベース(mySQL/MariaDB/PostgreSQLなど)
  4. ストレージ

VMの場合はこれら全てが構築された状態で全体のイメージを取るのに対し、
Dockerの場合はこれら1要素ごとに仮想化します。

この説明で、Dockerを使うメリットがわかっていただければ幸いです。

Dockerを動かす仕組み

さて、いよいよDockerを動かしていきます。Dockerを使うためには、基本的には2種類のファイルを用意する必要があります。

  • Dockerfile ... 1コンテナの定義
  • docker-compose.yml ... 複数コンテナの定義。どのコンテナでどのDockerfileを使うかなど。

例えばとあるNext.js製Webアプリケーションの場合のDockerfileは以下のようになります。

Dockerfile
# syntax=docker.io/docker/dockerfile:1

# ---- ベースステージ ----
# Node.jsのバージョンを定義し、共通の依存関係をインストールします。
FROM node:20-alpine AS base
# Alpineパッケージのインストール
# libc6-compat: Node.jsのネイティブアドオンで必要になる場合がある互換性ライブラリ
# openssl: HTTPS通信や暗号化処理でNext.jsやNode.jsの依存関係が要求することがあるため追加
RUN apk add --no-cache libc6-compat openssl

# ---- 依存関係ステージ ----
# Node.jsの依存関係をインストールします。このレイヤーは、
# package.jsonやロックファイルが変更されない限り効果的にキャッシュされます。
FROM base AS deps
WORKDIR /usr/src/app
# プロジェクトのルートにあるpackage.jsonとロックファイル(yarn.lock, package-lock.json, pnpm-lock.yaml)をコピー
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# 使用しているパッケージマネージャーに基づいて依存関係をインストール
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

# ---- 開発ステージ ----
# ローカル開発用にアプリケーションをセットアップします。
# ボリュームマウントと組み合わせることで、ホットリロードが可能になります。
FROM base AS development
WORKDIR /usr/src/app

# Node環境を 'development' に設定
ENV NODE_ENV=development

# 'deps' ステージからインストール済みの依存関係をコピー
COPY --from=deps /usr/src/app/node_modules ./node_modules

# アプリケーションの残りのソースコードをコピー
# ローカル開発では、通常 'docker run -v .:/app' や docker-compose を使用して
# ローカルのソースコードをコンテナにマウントします。
# このCOPY命令は、マウントしない場合でもコードがイメージ内に存在することを保証します。
#COPY . .

# ローカル開発における環境変数に関する重要事項
# -----------------------------------------------------
# このDockerfile内に、シークレットや環境固有の設定をハードコードしないでください。
# ローカル開発では、プロジェクトルートに .env ファイルを作成し、それをコンテナに渡してください。
#
# .env ファイルの記述例:
# GOOGLE_CLIENT_ID="your_local_google_client_id"
# GOOGLE_CLIENT_SECRET="your_local_google_client_secret"
# # ... 以下、元のDockerfileに記載されていた全ての環境変数を同様に記述
#
# コンテナ実行時のコマンド例:
# docker build -t my-nextjs-app-dev .
# docker run -p 3000:3000 --env-file ./.env -v .:/app my-nextjs-app-dev
#
# Docker Composeを使用する場合は、docker-compose.ymlファイル内で 'env_file' を指定します。
#
# 以下の環境変数はNext.jsのランタイム設定用であり、シークレットではありません。
ENV PORT=3000
# Docker内で実行されるNext.js開発サーバーがホストマシンからアクセスできるようにするため、0.0.0.0を指定
ENV HOSTNAME="0.0.0.0"

# Next.jsが実行されるポートを公開
EXPOSE 3000

#環境変数追加

# オプション: 本番環境と同様に、セキュリティ向上のために非ルートユーザーを作成します。
# ただし、ホストのUID/GIDとコンテナのUID/GIDが一致しない場合、
# ボリュームマウント時にパーミッションの問題が発生することがあります。
# パーミッションエラーが発生した場合は、rootユーザー(デフォルト)で実行するか、
# UID/GIDのマッチングを行う必要があるかもしれません。
# RUN addgroup --system --gid 1001 nodejs
# RUN adduser --system --uid 1001 nextjs
# USER nextjs # ローカル開発ではボリュームマウント時のパーミッション問題を避けるため、一旦コメントアウト

# Next.js開発サーバーを実行するコマンド。
# package.jsonに "dev": "next dev" のような "dev" スクリプトが定義されていることを確認してください。
# -H 0.0.0.0 や --host 0.0.0.0 オプションは、コンテナの外部からアクセスできるようにするために重要です。
CMD \
  if [ -f yarn.lock ]; then yarn dev -H 0.0.0.0; \
  elif [ -f package-lock.json ]; then npm run dev -- -H 0.0.0.0; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm dev --host 0.0.0.0; \
  else echo "Lockfile not found or dev script not configured." && exit 1; \
  fi

コメントアウトでこんな説明が書けるなんてすごい!って思った方、褒め称えてください。
(このDockerfileは、コメントアウトを含めてGemini(Google生成AI)で作成しました)

このように、かつてはDockerfileを作成するのが難しくて利用を避けていましたが、生成AIで作れてしまいます。

続いて、docker-compose.ymlをみてみましょう。

docker-compose.yml
version: '3.8'

services:
  # Next.js アプリケーションサービス (既存のDockerfileを使用)
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev # Dockerfile名が異なる場合は適宜修正
    ports:
      - "3000:3000" # ホストの3000番ポートをコンテナの3000番ポートにマッピング
    depends_on:
      - db # dbサービスが起動してからappサービスが起動する
    environment:
      # Next.jsアプリケーションからMySQLに接続するための環境変数
      MYSQL_HOST: db         # Next.jsコードが参照する名前に合わせる
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: sample
      MYSQL_PORT: 3306       # Next.jsコードが参照する名前に合わせる
      # その他、Next.jsが必要とする環境変数
      # 例: NEXT_PUBLIC_API_URLなど
    volumes:
      - .:/app # 開発時のホットリロードのため (本番では不要な場合あり)
      - /app/node_modules # 開発時のボリュームマウントでローカルのnode_modulesを上書きしないように
      - /app/.next
    networks:
      - app-network

  # MySQL 8.0 サービス
  db:
    image: mysql:8.0
    ports:
      - "3307:3306" # ホストの3307番ポートをコンテナの3306番ポートにマッピング (ホストからアクセスする場合)
    environment:
      MYSQL_ROOT_PASSWORD: root # MySQLのrootユーザーのパスワード
      MYSQL_DATABASE:sample # 作成するデータベース名
      MYSQL_USER: user # 作成するユーザー名
      MYSQL_PASSWORD: password # 作成するユーザーのパスワード
    volumes:
      - mysql-data:/var/lib/mysql # データの永続化
    networks:
      - app-network
    restart: unless-stopped

volumes:
  mysql-data: # MySQLのデータを永続化するための名前付きボリューム

networks:
  app-network:
    driver: bridge

これも生成AIで作成しました。「Dockerを使って、Next.jsとMySQLを動かしたい」といえば、このファイルが作られます。さらに配置場所も教えてもらえます。

さて、docker compose up -d のようなコマンドを叩けば、コンテナが立ち上がります。
このコンテナが立ち上がるのは、Dockerfileとdocker-compose.ymlファイルの内容をみてコンテナが作られるからです。

結論

生成AIを活用することで昔より低くなったから、みんなも1回試してみてね!

Discussion