現場でDockerという存在を知りまして、さっそく自宅PCに導入しました。
この記事では、コンテナ上でTypeScriptのフルスタック開発を始められるまでの手順をまとめていきます。
環境と事前インストール
- docker(27.3.1)
- Visual Studio Code
私はWindows11のWSL上にDockerをインストールしました。
下記の内容は、Dockerfileやdocker-compose.ymlが実行できるなら環境自体は何でもいいはずです。Dockerはそういうものなはずです。たぶん。
最終的なディレクトリ構成
バックエンド関連のコードは backend
フォルダで監理します。また、フロントエンド関連のコードは frontend
フォルダで管理し、それらを各コンテナにマウントしていく想定です。
起動後のコンテナは VSCode で接続するので、各ディレクトリ配下には devcontainer.json
を置いてます。
project-root/
├── backend/
│ ├── .devcontainer/
│ │ └── devcontainer.json # Dev Containerの設定ファイル
│ ├── src/ # バックエンドのソースコード
│ │ └── index.js # バックエンドのエントリーポイント
│ ├── Dockerfile # バックエンド用のDockerfile
│ ├── package.json # バックエンドのNode.js依存関係管理
│ └── tsconfig.json # TypeScriptの設定ファイル(バックエンド用)
├── frontend/
│ ├── .devcontainer/
│ │ └── devcontainer.json # Dev Containerの設定ファイル
│ ├── src/ # 🟡 フロントエンドのソースコード
│ ├── index.html # 🟡 フロントエンドのHTMLエントリーポイント
│ ├── package.json # 🟡 フロントエンドのNode.js依存関係管理
│ ├── tsconfig.json # 🟡 TypeScriptの設定ファイル(フロントエンド用)
│ ├── vite.config.mts # 🟡 Viteの設定ファイル
│ └── Dockerfile # フロントエンド用のDockerfile
└── docker-compose.yml # 全体のサービスを管理するDocker Compose設定
🟡の部分は、コンテナ起動後に npm create vuetify で作成する想定なので目印を付けてます。
では、まずはフロントエンドを開発するためのコンテナを作成していきます。
フロントエンド開発コンテナを作成
/frontend
配下にある Dockerfile
を作成します。
Dockerfile とはコンテナの設定ファイルです。この Dockerfile を元にイメージというものが作られて、イメージを元にコンテナが作られるのですが、イメージはレイヤー構造になっていて、下記の RUN
や COPY
命令のたびに1層ずつレイヤーが積みあがっていきます。
FROM node:20 # Node.js イメージを使用
WORKDIR /web # 作業ディレクトリを設定
COPY package*.json ./ # パッケージ定義ファイルと依存関係をコピー
RUN npm install # 依存パッケージをインストール
RUN npm install -g typescript # globalに TypeScript をインストール
CMD ["npm", "run", "dev"] # コンテナ起動時のコマンドを設定
一行目の FROM
命令で dockerhubにある誰かのイメージを拝借しているのですが、以降の命令で更にレイヤーを重ねていき、自分だけの最強のコンテナを作ろうねって感じです。
フロントエンドは Vue.js で作っていく想定ですが、コンテナ起動後にプロジェクトを作成するので、このコンテナは Node.js が使えるだけです。
docker-compose.yml を作成する
プロジェクトのルートディレクトリに、docker-compose.yml
を作成します。
Docker には多くのコマンドやオプションがありますが、それらを.ymlファイル にまとめて記述することで、毎回コマンドを実行せずに、一発でコンテナを起動することができるのです。
この .yml の名称は任意で構いませんが、今回はひとつの .yml しか使わないので、凝らずにそのまま docker-conpose.yml としています。
services:
frontend:
build:
context: ./frontend # Dcokerデーモンに送るデータの範囲の設定
container_name: front-dev # コンテナ名の設定
ports:
- "5173:5173" # ポートマッピング 5173は Vite のデフォルトポート
volumes:
- ./frontend:/web # ローカルの frontend をコンテナ内の /web にマウント
- /web/node_modules # node_modules の競合を防ぐための空ボリューム
これらは、Dockerコマンド でも実現できますが、docker compose を使ってビルドをすれば、.yml が置かれたディレクトリ上で、$ docker compose up
コマンドを実行するだけで、Dockerイメージのビルドとコンテナの起動が済むので、圧倒的に楽です。
さて、ここでポイントは context でしょうか。
ここにはコンテキストパスを設定するのですが、ここで設定したデータの範囲をビルド時に Docker が参照します。ですので、コンテキストパスに含まれていないフォルダを、volumes などで設定すると、イメージのビルド時にエラーが出てしまって「フォルダはあるのに…」と詰まるのが、最初に陥りやすいポイントかなと思います。
また、今回は Node.js イメージを使用するので、その場合の注意事項としては、node_modules
をボリュームで隔離している部分ですね。
ホストのプロジェクトディレクトリ(/frontend
)をコンテナ(/web
)にマウントする場合、ホスト側の node_modules がコンテナ内の node_modules
を上書きしてしまう問題があります。
これを防ぐため、node_modules
をコンテナ内で独立して管理し、ホスト側の node_modules
を参照しないように設定しましょう。
volume 部分の2行目には、 : がないため、これはホスト側のディレクトリをマウントするのではなく、コンテナ内部専用の空のボリュームを作成することを意味します。
つまり、/web/node_modules
は同期対象から除外され、独立した空ボリュームとして扱われるのです。
こうすることで、アプリケーションはコンテナ内の node_modules
を利用して動作します。
バックエンド開発コンテナを作成
同様に、/backend
配下にある Dockerfile
を作成します。
今回は TypeScript かつ Express.js を使って開発していく想定なので、Dockerの機能であるマルチステージビルドを使っていきます。
TypeScriptやビルドプロセスが必要なプロジェクトでは、コンパイルや成果物の生成を行うためにステージを分けるのが一般的です。
/frontend
ではビルドツール(Vite)が担当するため、TypeScript のコンパイルステージを用意していません。
バックエンドでは Vite のような補助輪がない(というか私が知らない…)ので、TypeScript をコンパイルして成果物を生成する処理が必要になります。
そのため、下記のように Dockerfile
へ開発用ステージと本番用ステージを分けて設定します。
# 開発用ステージ development
FROM node:20 as development # Node.js イメージを使用
WORKDIR /app # 作業ディレクトリを設定
COPY package*.json ./ # パッケージ定義ファイルと依存関係をコピー
RUN npm install # 依存パッケージをインストール
COPY . . # プロジェクトの全ファイルをコンテナ内の /app にコピー
RUN npm run build # TypeScript をコンパイルして dist ディレクトリを生成
# 本番用ステージ production
FROM node:20 as production # Node.js イメージを使用
# 本番環境変数の設定
ARG NODE_ENV=production # デフォルトで NODE_ENV=production を設定、
ENV NODE_ENV=${NODE_ENV} # キャッシュ無効化やデバッグ無効化などを有効にする為
WORKDIR /app # 作業ディレクトリを設定
COPY package*.json ./ # パッケージ定義ファイルと依存関係をコピー
RUN npm ci --only=production # 本番用依存パッケージのみをインストール
# 開発ステージからの成果物をコピー
COPY --from=development /app/src/app/dist ./dist
CMD ["node", "dist/index.js"] # ビルドされた成果物を起動
色々書いていますが、後述と合わせて説明します。
大まかな流れとしては、上記のようにマルチステージビルドで定義したステージを利用し、docker-compose.yml
で柔軟に使いたいステージを選択・実行していくみたいな感じです。
docker-compose.yml を修正する
ルートディレクトリのdocker-compose.yml
に、追記します。
services:
frontend:
build:
context: ./frontend
container_name: front-dev
ports:
- "5173:5173"
volumes:
- ./frontend:/web
- /web/node_modules
depends_on:
- backend #
backend:
build:
context: ./backend # backend ディレクトリをコンテキストとして指定
target: development
container_name: back-dev
volumes:
- ./backend:/app
- /app/node_modules
ports:
- "4000:4000"
command: npm run dev # コンテナ起動時に npm run dev を実行
ポイントは、target: development
ですね。
これにより、Dockerfile 内の development ステージまでを実行してコンテナを構築します。開発に必要な環境だけが整ったコンテナが起動するのです。
また、この時 command
によって、コンテナ内で npm run dev
が実行され、バックエンドアプリケーションが開発モードで起動します。
また、フロントエンドのコンテナ起動時に depends_on
を追記してますが、これで back-dev が起動してから front-dev が起動するようになります。
指定できるのはコンテナ名ではなくてサービス名です(たしかコンテナ名だとダメだった気がします)。
その他のファイルを作成する
上記の状態でビルドしようとすると、エラーが出るはずです。なぜかというと、package.json
と tsconfig.json
が /backend
配下に存在しないからです。
package.json
右クリックで作成してもいいですし、Node.js がインストールされているのであれば npm init
コマンドで初期化できると思います。
内容は下記のように編集してください。scripts
がポイントです。
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "ts-node-dev --poll src/index.ts",
"build": "rimraf ./dist && tsc",
"start": "npm run build && node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"rimraf": "^6.0.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.6.3"
}
}
依存関係の定義と、3つのスクリプトを設定しています。
- npm run dev
ts-node-dev --poll src/index.ts
-
src/index.ts
を TypeScript として実行する。また、ファイルの変更を検知するとアプリケーションを自動的に再起動する。開発環境用のスクリプト。 - npm run build
rimraf ./dist && tsc
- 既存の
./dist
を削除した後、TypeScript コードがコンパイルされて、JavaScript が./dist
に出力される。
npm run build && node dist/index.js
このように設定することで、back-devコンテナが起動するときに、index.ts
内の記述がコンパイルされ、リアルタイムでのコード変更を検知し、ホットリロードが実現します。
Docker Composeで実行する
設定ファイルの記述と、必要のファイルの用意が済んだので、docker-compose.yml
を実行しましょう。
先述した通り、 .ymlファイルが置かれたディレクトリ上で、$ docker compose up
コマンドを実行するだけです!
まとめ
現場は JavaScript+Vue.js & Express.js なので、自分ではTypeScriptで実践してみよう!と、思い付きで始めたことですが、コンパイルの概念が出てきてしまい、なかなか難しかったです。
試行錯誤をしながら進めていたので、上記の手順に漏れがあるかもしれないので、また何かの機会でもう一度おさらいしようと思います。
ではでは~!)^o^(