なぜこの記事を書こうと思ったのか
私はフロントエンド(React / Vue)を中心に、SESエンジニアとして実務経験が丸2年になりました。現在は3案件目に参画中です。
少し前でしょうか、Xで「GitとDockerが扱えないのはエンジニアとして致命的だ」という投稿が話題になっていました。もちろん、プロジェクトの規模や役割によって必要なスキルは変わるため一概には言えません。ただ、Gitについては現場ごとのブランチ運用やルールに慣れてきて、最近は困ることも割と少なくなりました。
一方でDockerはというと──。
1案件目は小規模、2案件目は中規模、そして現在の3案件目は大規模案件とステップアップしてきたものの、実はDockerを自分でインストールしてコマンドを叩くような機会はほとんどありませんでした。
それでも「Docker」という言葉は常に頭の片隅にあり、「そろそろちゃんと触っておかないと」と感じていました。スキルアップやキャリアアップの観点からも、知識を整理してアウトプットしておくことは大事だと思い、今回この記事を書くことにしました。
なお、私はバックエンド(Java / Python)をほんの少し触った程度で、メインはあくまでフロントエンドです。そのため、本記事は「バックエンドに詳しくないフロントエンドエンジニアがDockerを学び始めるときの視点」を意識して書いていきます。
※この記事は私自身も前提知識が無いので調べながら書いています。
Dockerを触るうえでの重要なキーワード
Dockerを学ぶ方法はいろいろあります。適当に解説記事を読む、Udemyなどで講座を視聴する──いずれの方法でも必ず出てくるのが、以下のキーワードです。
DockerContainer
役割: アプリケーションが実際に動いている箱。
例:Reactの開発サーバが動いてるコンテナ,MySQLが動いてるコンテナ
実際に動く実態 = コンテナ
DockerImage
役割: コンテナを作るための「設計図・ひな型」
イメージ: ゲームのセーブデータやアプリのインストーラ
例: Node.jsの環境をまとめたDockerImage, MySQLのImage
コンテナを生み出す種 = イメージ
Dockerfile
役割: DockerImageを作るためのレシピ
どのベースのImageを使うか(例: FROM node:20)
どんなパッケージを入れるか(例: RUN npm install)
どのコマンドでアプリを起動するか(例: CMD ["npm","start"])
例: フロント用のDockerfile, バック用のDockerfile
イメージを作るための設計書 = DockerFile
DockerCompose
役割: 複数のコンテナをまとめて管理する仕組み
使い方: docker-componse.yamlに設定を書く
例: Reactのフロント、SpringBootのバック、MySQLのDBを一発で立ち上げる
例: コンテナ同士のネットワークや環境変数をまとめて設定
複数コンテナをオーケストレーションする道具 = DockerCompose
この4つをセットでイメージすると、
「DockerFile -> DockerImage -> DockerContainer」が基本の流れとなっていて
「DockerCompose」がそれらをまとめて管理する、という位置づけとなる。
これらはDockerの学習で避けて通れない基本用語なのですが、解説記事を見ていると「コンテナ」「イメージ」とカタカナで書かれていたり、英語表記が混ざっていたりと、表記がバラバラなことが多いと思います。特に「イメージ」という言葉は、日常的な意味と技術用語が重なっていて混乱しやすいポイントです。
本記事では、すべて上記の英語表記(アルファベット)で統一します。イメージが湧きづらくなると思うので。
また、極力公式ドキュメントの文言をそのままペタも極力避けます。個人的にイメージが湧きづらかったので。
Dockerってどこで動かすものなのか?
- ローカルのPC内(開発用サーバー構築に使える)
- CI/CD(GitHub Actions等でテスト・ビルド・デプロイに使える)
- 本番環境(クラウドやオンプレで実際のサービス稼働環境に使える)
になっているようです。
当初は「ローカルで仮想DBなどを立ち上げるためのツール」だと思っていましたが、それだけではありません。
ローカルサーバーの構築に使えるのはもちろん、CI/CDや本番環境でもDockerが使われるケースが多いようです。
👉 「ローカルだけの技術」という認識を一歩進めて、どの環境でも同じ環境を再現できるのがDockerの強み と理解すると、全体像がつかみやすくなると思います。
Docker使用時のディレクトリの構成
私はこれまで3案件の開発を経験しましたが、その中で モノレポ・マルチレポの両方に触れてきました。
結論としては「どちらの方法も実務では普通に存在する」し、プロジェクトの規模や運用方針によって適切な形が変わります。
片方しか経験していないと「自分のやり方が標準」と思いがちですが、実際には どちらの管理方法もあり得る というのが現場を通して得た実感です。
👉 つまり、フロントエンドエンジニアであっても 両方を理解しておくことが武器になる と思います。
モノレポ
まず考えられるのが、いわゆる モノレポ という管理方法です。
これは複数のプロジェクトのソースコードを単一のリポジトリで管理する開発手法で、フロントエンドとバックエンドをまとめて扱います。
ルートディレクトリ直下にフロントとバックのディレクトリを配置し、リポジトリ内で一括して管理するイメージです。
project-root/
├── frontend/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── …(ソースコード)
├── backend/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── …(ソースコード)
├── docker-compose.yaml
├── .env
└── README.md
- docker-compose.yaml を使うことで、フロント・バック・DBをまとめて一発起動できる
- 小規模開発や個人開発で特に便利
- 変更を横断的に管理しやすい反面、大規模になるとリポジトリが肥大化しやすい
- 「非エンジニアや検証担当が触る頻度が多い」場合は、モノレポのほうがシンプルに説明できることが多い
マルチレポ
もう一つのパターンは、サービスごとにリポジトリを分ける方法です。
フロントエンドとバックエンドをそれぞれ独立したリポジトリで管理し、必要に応じてインフラ用のリポジトリを作成します。
frontend-project/
├── Dockerfile
├── .dockerignore
└── …(ソースコード)
backend-project/
├── Dockerfile
└── …(ソースコード)
infra-project/ ← まとめて起動したい場合のみ用意
├── docker-compose.yaml
├── .env
└── README.md
- READMEがサービスごとに分かれる → 「まずfrontendリポをcloneして動かして、次はbackendリポ…」と手順が複雑化
- 非エンジニアにとってはハードルが上がりやすい
- ただし 開発チーム単位で独立して回したい ときはマルチレポの方が便利
上記を踏まえて
個人的な考えとしては、まず モノレポ管理をベースに検討するのが良い と感じています。
理由はシンプルで、PMやクライアントなど非エンジニアが動作確認に参加するケースが多い場合、モノレポの方が環境構築やREADMEの記載がシンプルになりやすい からです。
実際、これまでの現場経験でも、マルチレポ管理だと
「フロントはこのリポジトリ、バックエンドはこのリポジトリをcloneしてください」
といった具合に手順が煩雑になり、いわゆる偉い人に触ってもらう際に「面倒だな」という印象を与えてしまう場面がありました。
また定例会などで画面共有をしながらUIの動作確認をお願いしていたのですが、もしあの当時Dockerという選択肢を導入できていたら、もっと良い体験を提供できたのではないか と今では感じています。
モノレポであれば、フロント・バック・DBをまとめて立ち上げられるため、非エンジニアにも触ってもらいやすく、フィードバックを得る機会を増やすことができます。
したがって本記事では、「まず触ってもらうこと」を前提とした構成 として、ディレクトリ例もモノレポをベースに紹介していきたいと思います。
Dockerでのホットリロードについて
通常、フロントエンド開発では npm run dev などのコマンドを使って、ローカル環境でコードを書きながら画面を構築していきます。
このとき コードを変更すると即座にブラウザに反映される仕組み を「ホットリロード」と呼びます。
フロントエンド開発においてはごく当たり前の体験なので、Dockerを初めて触ったときに「あれ?ホットリロードが扱いづらいような気がするぞ?」と自分はなっていました。
理由としてはシンプルで、Dockerは基本的に「静的なビルド環境」を前提にしているからです。
ホットリロードをコンテナ内で常時有効にすると、ファイルの監視やメモリ負担が大きくなり、動作が重くなることがあります。そのため、Docker環境ではホットリロードはあまり推奨されないケースが多いようです。
実務でよくあるパターン
実際の開発では、フルスタックでフロントとバックを同時にガリガリ書き換えているケースばかりではありません。
そのため次のような構成がよく取られます。
バックエンド … Docker上で動かす
フロントエンド … ローカルで npm run dev を実行し、ホットリロードを効かせながら開発
こうすることで、フロントは軽快に開発しつつ、バックは本番に近いDocker環境で確認できる、というバランスが取れます。
ホットリロードが無いことのメリット
逆に「ホットリロードが常時効かない環境」というのもメリットがあります。
たとえば以下のようなシーンです。
CI/CD パイプラインでのビルド・テスト
PMや顧客にUIを触ってもらい、フィードバックをもらう検証環境
つまり、常に自動でリロードされるのではなく、意図的にビルドして確認するスタイルが、レビューやフィードバックの場面と相性が良いのです。
👉 まとめると、
個人開発やフロント寄りの作業 → ローカルでホットリロード(npm run dev)
チーム開発や本番想定の動作確認 → Dockerでビルド(ホットリロード無し)
というように 使い分けるのが現実的 という理解がしっくりくると思います。
※後述するvolumeの選択肢があり、ホットリロードも可能ではありつつも、実務ではあまり推奨されないという点を補足とさせていただきます。
どんなことが書いてあるのか
先に述べましたモノレポ管理方法で、各ファイルにはどんなことが書いてあるのかということを深堀格納場所などから深堀してみます。
project-root/
├── frontend/
│ ├── Dockerfile 👈これと
│ ├── .dockerignore 👈これと
│ └── …(ソースコード)
├── backend (省略)
├── docker-compose.yaml 👈これですね
├── .env
└── README.md
project-rootに存在する「docker-compose.yaml」
それぞれのプロパティ名には解説を後述します。
version: "3.9"
services:
frontend:
build:
context: ./frontend # frontendディレクトリのDockerfileを使用
ports:
- "5173:5173" # ホスト:コンテナ
volumes:
- ./frontend:/app # ホットリロード用にローカルをマウント(任意)
depends_on:
- backend
networks:
- app-network
backend:
build:
context: ./backend
ports:
- "8080:8080"
volumes:
- ./backend:/app
depends_on:
- db
networks:
- app-network
db:
image: mysql:8
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: sample
MYSQL_USER: user
MYSQL_PASSWORD: password
volumes:
- db-data:/var/lib/mysql
networks:
- app-network
volumes:
db-data: # DB永続化用のボリューム
networks:
app-network: # 各サービスをつなぐネットワーク
services
起動させたいコンテナ(サービス)を定義するブロック
例: frontend, backend, dbがそれぞれ1つのコンテナ(サービス)
services.*.build
ローカルの Dockerfile を元に、アプリ専用の Docker イメージを新しく作る設定。
例えばフロントやバックエンドのコードは、自分のリポジトリ内にある Dockerfile からビルドする必要があるため build: を使う。
一方で、MySQL など既に公開されているミドルウェアは image: を指定して使うことが多い。
services.*.build.context
Dockerfile が置かれているディレクトリを指定する。
例: ./frontend を指定すると、その配下の Dockerfile を使ってイメージを作成する。
services.*.ports
- ホストPCのポート番号と、コンテナ内のポート番号を紐づける設定。
- "5173:5173" の場合 → http://localhost:5173 でアクセスすると、コンテナ内の 5173 番に届く。
services.*.volumes
- ホストとコンテナでディレクトリやファイルを共有する設定。
- 例: ./frontend:/app → ローカルの frontend ディレクトリがコンテナの /app にマウントされる。
- DB の場合は永続化用のディレクトリを指定する。
services.*.depends_on
- コンテナの起動順序を制御する設定。
- 例: frontend → backend → db の順で依存関係を定義できる。
- ただし「完全に待機する」わけではなく、最低限の依存解決。
services.*.networks
- サービスが参加するネットワークを指定する。
- 同じネットワークにいるサービス同士は「サービス名」で通信できる。
- 例: backend:8080 というURLでフロントからバックにアクセス可能。
volumes
- データ永続化のための名前付きボリュームを定義する場所。
- 例: db-data を定義すると、コンテナ削除後も MySQL のデータが保持される。
networks
- サービス同士をつなぐ仮想ネットワークを定義する場所。
- 例: app-network を定義して、全サービスを同じネットワークに所属させている。
モノレポ管理でDockerを起動させるコマンド
全部まとめて起動
docker compose up
- docker-compose.yamlに定義されている全サービスが起動
- ログも全部一緒に流れる
※カレントディレクトリはルートディレクトリであること
フロントだけエンドだけ起動
※ディレクトリ名がfrontendである場合
docker compose up frontend
- compose ファイルの services: のキー名(例: frontend, backend, db)を指定。
- この場合は frontend サービスだけ立ち上がる。
バックエンドとDBを起動する
※ディレクトリ名がbackend, dbである場合
docker compose up backend db
フロントエンドをガリガリ更新しつつもバックエンドとDBを起動しながらなどのケースで扱う場合はこちらを使用します。
ユースケースとしてはこれが一番使うのかも?
停止するとき
docker compose down
- 全サービスを停止してコンテナを削除。
- ボリュームやネットワークもまとめて片付く。
DockerFileにはどんなことが書いてあるのか
以下はshadcn(UIライブラリ)を使用したReactプロジェクトの一例です。
ルートディレクトリにDockerファイルが格納されるようにしています。
ファイル名称は拡張子なしで「Dockerfile」です。1ルートディレクトリで1つが基本のため、固定名称に近い認識で問題ありません。
※Dockerfileと.dockerignoreはgit管理下に置かれます。
frontend/
├── ⭐Dockerfile⭐ !NEW!
├── ⭐.dockerignore⭐ !NEW!
├── .storybook/
├── .vscode/
├── node_modules/
├── public/
├── src/
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── components.json
├── cspell.json
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── README.md
├── tailwind.config.ts
...
フロント側のDockerfileにはどんなことが記載されているか
以下を見ていただけると、100%の理解とまではいかなくても
「Dockerfileって、自分がリポジトリをcloneしたあとに手動で叩いていた環境構築コマンドを順番にまとめたものなんだな」
というイメージが掴めるのではないかと思います。1つずつ深堀してみたいと思います。
# A Node.js の軽量公式イメージをベースにする
FROM node:20-alpine
# B 作業ディレクトリを作成
WORKDIR /app
# C package.json と lockファイルをコピー
COPY package*.json ./
# D 依存関係をインストール
RUN npm install
# E プロジェクト全体をコピー
COPY . .
# F 開発サーバを起動
CMD ["npm", "run", "dev"]
# G ホスト側からアクセスできるようにポートを開ける
EXPOSE 5173
このように、Dockerfileは上から順に実行されていきます。
A Node.js の軽量公式イメージをベースにする
Dockerには Docker Hub という公式のレジストリ(イメージの公開場所)が存在します。
ここでは、さまざまな言語やフレームワークの「公式イメージ」というものが配布されています。
- フロントエンド開発でよく使う Node.js
- Javaアプリケーションを動かす OpenJDK
- Python実行環境の Python公式イメージ
など、メジャーな言語・環境はほとんど用意されています。
Docker Hub には Official Images(公式イメージ) というカテゴリがあり、
「node:20-alpine」 のようなイメージは Node.js公式チーム(OpenJS Foundation)と Docker Hub のメンテナンスチームが共同で管理しています。
つまり、Docker社が勝手に作っているわけではなく、
言語やフレームワークの公式チームが責任を持って管理している信頼性の高いイメージ ということです。
また、alpine とは Alpine Linux という超軽量なLinuxディストリビューションのことを指します。
DockerHubの公式イメージには
- Debianベース(一般的で安定しており、ビルドでハマりにくいがサイズが数百MBと大きい)
- Alpineベース(ビルド時に必要なライブラリが足りず、追加インストールが必要だったりする代わりに10数MBと非常に軽い)
など、複数の選択肢が提供されており、用途に応じて選べるようになっています。
そのため、node:20-alpine も公式イメージの1つで、軽量かつ最低限のパッケージ構成になっているのが特徴です。
ただし、入門用途や個人開発でまずDockerを触ってみたい場合は、
「多少サイズが大きくても安定して動く Debianベース」を選んでおくのが無難だと思います。Debianベースを利用する場合は、先ほどのサンプルにある
FROM node:20-alpine
を
FROM node:20
に書き換えるだけでOKです。(デフォルトがDebianのため)
B 作業ディレクトリを作成, C package.json と lockファイルをコピー,D 依存関係をインストール
WORKDIR /app
と書くと「このコンテナの中では /app というフォルダを作って、そこを作業場所にします」という指定になります。
WORKDIR /app
COPY package*.json ./
RUN npm install
① /appというディレクトリがコンテナ内に作成され
② package.jsonとpackage-lock.jsonが作成されたappにコピーされ
③ そのディレクトリでnpm installが実行(RUN)される
なぜ最初に package.json だけコピーするのか?
Dockerfile は上から順に命令を実行し、その結果を レイヤーごとにキャッシュ として保存します。
このキャッシュは基本的に ローカルのDocker環境(またはCI/CD環境)に残り、同じ命令・同じ入力なら再利用されます。
こうすると、依存関係のインストール(npm install)までをキャッシュ化できるため、
package.json に変更がなければ再実行されずに済みます。
一方で、もし最初から全て(src/ 以下も含めて)をコピーしてしまうと、
ソースコードのちょっとした変更でもキャッシュが無効化され、
依存関係が変わっていないのに毎回 npm install が走ってしまう ことになります。
これではテストやビルドに無駄な時間がかかるため、
「まず依存関係ファイルだけコピーしてインストール → その後にソースをコピー」
という手順が Dockerfile の定番の書き方になっています。
結論としては『お作法』として覚えておけば十分ですね。
F 開発サーバを起動
CMD ["npm", "run", "dev"]
配列形式で文字列が書かれていますが、
- シェルを経由せずにコマンドを直接実行
- JSON配列の形で "コマンド", "引数", "引数" と明示する
- CMDやENTRYPOINTで推奨される書き方(安全・予測可能)
- CMD は「コンテナ起動時に動くメインプロセス」
とまぁ特徴らしきを書いてみましたがこれも実質お作法的なもののようです。
G ホスト側からアクセスできるようにポートを開ける
EXPOSE 5173
Dockerコンテナというのは、ホスト(自分のPC)の中で動いているが、完全に独立した小さな仮想環境。
コンテナの中で動いてるアプリというのは、そのままだとホストPCやブラウザから見ることができません。
EXPOSEというのは、そのコンテナがどのポートでアプリを待ち受けているか宣言する命令であり。
「このコンテナの中では 5173 番ポートを使って通信を受け付けていますよ」とDockerに伝える“お知らせ”みたいなもの。
実際に外(あなたのブラウザ)からアクセスできるようにするには、ポートを紐づける(ポートフォワード)設定が必要です。
これは先述したdocker-compose.yamlの中に書いた以下の部分で行われています。
ports:
- "5173:5173"
- 左側の 5173 が「ホスト側(あなたのPC)のポート」
- 右側の 5173 が「コンテナ内(アプリが動いてる)のポート」
になります
実際の流れとしては
-
コンテナ内のReactサーバが :5173 で待機
-
EXPOSE 5173 で「このポートを使います」と宣言
-
ports: "5173:5173" で
「ホストの5173 → コンテナの5173へ通信を転送」設定
結果として、ブラウザで「http://localhost:5173」
にアクセスすると。コンテナ内のアプリが表示されるようになります。
DockerIgonreとは?
.dockerignore は、Docker イメージを作る際にコピーしたくないファイルやフォルダを除外するための設定ファイルです。
COPY . . のようにプロジェクト全体をコピーする際、node_modules や .git などビルドに不要なものを除外して、イメージの軽量化とビルド時間の短縮を目的としています。
最後に
今回は、Docker の基本構成やモノレポでの使い方、初学時に「ここがわかりづらい」と感じたポイントを中心に整理してみました。
これから Docker を学び始める方の「最初のとっかかり」になれば嬉しいです。
閲覧ありがとうございました!