awesome-hacks
Docs

NestJSをDocker化する

Node公式イメージでAPIをビルドし、docker-composeで起動・SQLiteボリュームまでつなぐ最小例

最終更新:2026/05/16

NestJSテスト入門まで終えている前提。
この記事では、Prisma 7 + SQLite + pnpm のAPIをDockerで起動できるようにする。

用語や全体の流れは、Dockerとコンテナの整理(概要) でまとめている。

このシリーズでは、まず Controller / DTO / Prisma / 例外 / JWT / Swagger / テスト と、NestJSとしての中身を順に積み上げる。Dockerは「できあがったAPIを、コンテナという別の形で起動する」話であり、アプリが一通り動く前提ができてから触った方が、不具合の切り分けも学びもしやすい。そのためDockerは最後(任意のまとめ)に置いている。

コンテナ化のよくある動機は、実行環境をイメージにまとめて配ることで、他の人や別マシン・CIでも同じAPIを立ち上げやすくすることだ。一方で「実装したら必ずDocker」ではなく、PaaSに任せる・サーバにNodeを直接入れる・サーバレスに載せるなど、プロジェクトの置かれ方で選ぶことも多い。ここではそのうちのひとつの形として、最小のDockerfileとcomposeを扱う。

この記事でやること

  • Dockerfile を作る
  • .dockerignore を作る
  • docker-compose.yml でポート・環境変数・SQLite保存先を設定する
  • docker compose up --build で起動確認する

本番レベルのマルチステージビルド、非rootユーザー、ヘルスチェック、マイグレーション戦略は別途詰める。ここでは「ローカルで同じAPIをコンテナ起動できる」ことを優先する。

触るファイル一覧

api-sample/
├─ Dockerfile          # 新規作成
├─ .dockerignore       # 新規作成
└─ docker-compose.yml  # 新規作成

1. .dockerignore を作る

新規作成するファイルは api-sample/.dockerignore

node_modules
dist
.git
.env
prisma/dev.db
*.log

node_modulesdist をDocker buildのコンテキストに入れない。.env もイメージに焼き込まず、docker-compose.ymlenvironment から渡す。

2. Dockerfile を作る

新規作成するファイルは api-sample/Dockerfile

FROM node:22-alpine

WORKDIR /app

# 追加: pnpmを使うためにCorepackを有効化する
RUN corepack enable

# 追加: better-sqlite3 などネイティブアドオンのビルドに必要になることがある
RUN apk add --no-cache python3 make g++

COPY package.json pnpm-lock.yaml ./
# pnpm 10+ は依存の lifecycle(postinstall 等)を既定で「未承認なら実行しない」扱いにすることがある。
# Docker ビルドでは対話の `pnpm approve-builds` ができないため、ビルド時だけ全依存のビルドスクリプトを許可する(Prisma / better-sqlite3 / bcrypt 等に必須)。
RUN pnpm install --frozen-lockfile --config.dangerouslyAllowAllBuilds=true

COPY prisma ./prisma
COPY prisma.config.ts ./

# Prisma 7 の generate で参照。実行時も compose の DATABASE_URL と揃える(ホストの prisma/dev.db とは別パス)
ENV DATABASE_URL="file:/data/dev.db"

# 追加: schema.prisma から src/generated/prisma を生成する
RUN pnpm prisma generate

COPY tsconfig*.json nest-cli.json ./
COPY src ./src

RUN pnpm run build

ENV NODE_ENV=production
EXPOSE 3000

CMD ["pnpm", "run", "start:prod"]

ホストで pnpm start:dev などしていたときは、.envDATABASE_URLfile:./prisma/dev.db のような 相対パスになることが多く、実ファイルは api-sample/prisma/dev.db になる。一方、上の ENV と compose の DATABASE_URLコンテナの中でのパスであり、SQLite ファイルを /data/dev.db(ボリュームマウント先)に置く前提だ。ホストの prisma/dev.db をそのまま使う設定ではない(.dockerignoreprisma/dev.db をビルドコンテキストから外している)。

この記事ではpnpm前提にしている。npmで進めている場合は、pnpm-lock.yamlpnpm installpnpm prisma generatepnpm run buildpnpm run start:prod をnpm用に置き換える。

better-sqlite3 はネイティブアドオンを含むため、Alpine上でビルドツールが必要になることがある。そのため python3 make g++ を入れている。

3. docker-compose.yml を作る

新規作成するファイルは api-sample/docker-compose.yml

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: "file:/data/dev.db"
      JWT_SECRET: "dev-secret-change-me"
    volumes:
      - sqlite-data:/data
    command: sh -c "pnpm prisma migrate deploy && pnpm run start:prod"

volumes:
  sqlite-data:

DATABASE_URL: "file:/data/dev.db" は、コンテナ内の /data/dev.db にSQLiteファイルを作る指定。sqlite-data volumeを /data にマウントしているため、コンテナを作り直してもDBファイルが残りやすい。

pnpm prisma migrate deploy は、prisma/migrations にあるマイグレーションをDBへ反映するコマンド。開発中に新しいmigrationを作るのは migrate dev、コンテナ起動時に既存migrationを適用するのは migrate deploy と覚える。

4. 起動確認

ビルドして起動する。

docker compose up --build

別ターミナルで疎通確認する。

curl -s http://localhost:3000/users

ユーザー作成も確認する。

curl -s -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"email":"docker@example.com","name":"Docker Taro","password":"password123"}'

Swaggerを入れている場合は、ブラウザで次を開く。

http://localhost:3000/api

本当にコンテナ経由か確認するには

docker-compose.ymlports: "3000:3000" は、ホストの 3000 番をコンテナ内の 3000 番に転送している。手元で pnpm start:dev など ホスト直起動の Nest を止めたうえでdocker compose up だけが動いていれば、上の curl http://localhost:3000/...コンテナ内のプロセスが応答している。

補助的な確認として、次が使える。

  • コンテナが動いているかdocker compose psapirunning / Up になっているか)
  • Nest のログが出ているかdocker compose logs -f api(サービス名は compose の services: のキー。この記事では api
  • コンテナの中から叩く … 例: docker compose exec api wget -qO- http://127.0.0.1:3000/users(Alpine に wget が無い場合は apk add curl 後に curl で代用)

5. よくあるエラー

pnpm-lock.yaml がない

Dockerfileで COPY package.json pnpm-lock.yaml ./ としているため、pnpm-lockが無いとビルドに失敗する。npmで進めているならDockerfileをnpm用に変更する。

migrate deploy でmigrationが無いと言われる

先にホスト側でmigrationを作る。

pnpm exec prisma migrate dev --name init_user
pnpm exec prisma generate

その後、prisma/migrations をコミット対象にする。

Cannot find module '/app/dist/main'(コンテナ起動直後)

pnpm run start:prodnode dist/main のままなのに、nest build の成果物が dist/src/main.js など別パスになっていると起きる。TypeScriptの rootDir の取り方で、src/ 配下の階層が dist/ に残ることがある。

手早い対処として、package.jsonstart:prod を実際の出力に合わせる(多いのは次の形)。

"start:prod": "node dist/src/main"

変更後に docker compose up --build でイメージを作り直す。

どこに出ているか迷ったら、ビルド済みイメージで一覧する。

docker compose run --rm --no-deps api ls -la /app/dist
docker compose run --rm --no-deps api ls -la /app/dist/src

別の直し方として、tsconfig.build.jsoncompilerOptions.rootDir./src に固定し、成果物が dist/main.js になるよう整える方法もある(プロジェクトの既存設定に合わせて選ぶ)。

Cannot connect to the Docker daemon / docker.sock

Docker Engine(デーモン)が動いていないときに出る。macOS では Docker Desktop を起動し、メニューバーにクジラのアイコンが出て安定するまで待ってから、もう一度 docker compose up --build を実行する。未インストールなら Docker Desktop for Mac から入れる。

ERR_PNPM_IGNORED_BUILDS / pnpm approve-builds

pnpm 10以降では、セキュリティ上の理由で 依存パッケージのビルドスクリプトが既定でスキップされ、pnpm install が失敗することがある。Dockerfile では pnpm install --frozen-lockfile --config.dangerouslyAllowAllBuilds=true とし、イメージビルドのときだけビルドスクリプトを通す(上記「2」の Dockerfile を参照)。

ホスト側で pnpm approve-builds を実行して package.json に許可リストを書き込む方法もあるが、Docker 内では対話できないため、学習用の最小例では上記フラグを使うのが手軽だ。より厳密にやるなら、許可したいパッケージだけを package.jsonpnpm.onlyBuiltDependencies に列挙する。

better-sqlite3 のビルドで落ちる

Alpine上でネイティブアドオンのビルドに失敗している可能性がある。この記事のDockerfileでは python3 make g++ を入れている。もし削っていたら戻す。

実装タスク(チェックリスト)

  • .dockerignore を作る
  • pnpm前提の Dockerfile を作る
  • docker-compose.ymlDATABASE_URL / JWT_SECRET / volumeを設定
  • Docker Desktop(または Docker Engine)を起動してから docker compose up --build を試す
  • start:proddist/main で落ちる場合は dist/src/main など実パスに合わせる(上記「5」を参照)

参考