NestJSをDocker化する
Node公式イメージでAPIをビルドし、docker-composeで起動・SQLiteボリュームまでつなぐ最小例
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
*.lognode_modules や dist をDocker buildのコンテキストに入れない。.env もイメージに焼き込まず、docker-compose.yml の environment から渡す。
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 などしていたときは、.env の DATABASE_URL が file:./prisma/dev.db のような 相対パスになることが多く、実ファイルは api-sample/prisma/dev.db になる。一方、上の ENV と compose の DATABASE_URL は コンテナの中でのパスであり、SQLite ファイルを /data/dev.db(ボリュームマウント先)に置く前提だ。ホストの prisma/dev.db をそのまま使う設定ではない(.dockerignore で prisma/dev.db をビルドコンテキストから外している)。
この記事ではpnpm前提にしている。npmで進めている場合は、pnpm-lock.yaml、pnpm install、pnpm prisma generate、pnpm run build、pnpm 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.yml の ports: "3000:3000" は、ホストの 3000 番をコンテナ内の 3000 番に転送している。手元で pnpm start:dev など ホスト直起動の Nest を止めたうえで、docker compose up だけが動いていれば、上の curl http://localhost:3000/... は コンテナ内のプロセスが応答している。
補助的な確認として、次が使える。
- コンテナが動いているか …
docker compose ps(apiがrunning/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:prod が node dist/main のままなのに、nest build の成果物が dist/src/main.js など別パスになっていると起きる。TypeScriptの rootDir の取り方で、src/ 配下の階層が dist/ に残ることがある。
手早い対処として、package.json の start: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.json の compilerOptions.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.json の pnpm.onlyBuiltDependencies に列挙する。
better-sqlite3 のビルドで落ちる
Alpine上でネイティブアドオンのビルドに失敗している可能性がある。この記事のDockerfileでは python3 make g++ を入れている。もし削っていたら戻す。
実装タスク(チェックリスト)
-
.dockerignoreを作る - pnpm前提の
Dockerfileを作る -
docker-compose.ymlでDATABASE_URL/JWT_SECRET/ volumeを設定 - Docker Desktop(または Docker Engine)を起動してから
docker compose up --buildを試す -
start:prodがdist/mainで落ちる場合はdist/src/mainなど実パスに合わせる(上記「5」を参照)