0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DockerコンテナでTypeScriptフルスタック開発環境を構築する(Vue.js+Express.js)

Last updated at Posted at 2024-11-22

現場で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 を元にイメージというものが作られて、イメージを元にコンテナが作られるのですが、イメージはレイヤー構造になっていて、下記の RUNCOPY 命令のたびに1層ずつレイヤーが積みあがっていきます。

backend/Dockerfile
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 としています。

docker-compose.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 へ開発用ステージと本番用ステージを分けて設定します。

backend/Docokerfile
# 開発用ステージ 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 に、追記します。

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.jsontsconfig.json/backend 配下に存在しないからです。

package.json

右クリックで作成してもいいですし、Node.js がインストールされているのであれば npm init コマンドで初期化できると思います。
内容は下記のように編集してください。scripts がポイントです。

backend/package.json
{
  "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 start 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^(

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?