Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Docker

This guide walks you through running a Prisma ORM application in Docker. You build a small Node.js app with Express, add Prisma ORM to it with orm init, run PostgreSQL from Docker Compose to apply the first migration, and then containerize the app so the app and the database start together with one command.

Prisma ORM has no query engines, no prisma generate step, and no schema.prisma. That makes the container story short: the image needs Node.js, your dependencies, the committed contract artifacts, and the migrations directory. The container applies pending migrations at start and then runs the server.

Every command and output below was run end to end with Docker Desktop.

Before starting, make sure no PostgreSQL service is running locally and that ports 5432 (PostgreSQL), 3000 (application server), and 5555 (Prisma Studio) are free.

To stop an existing PostgreSQL service, use:

sudo systemctl stop postgresql  # Linux

brew services stop postgresql   # macOS

net stop postgresql             # Windows (Run as Administrator)

To stop all running Docker containers and free up ports:

docker ps -q | xargs docker stop

To delegate this guide to your coding agent, copy the prompt below and hand it over:

with
Containerize a new Express app on Prisma ORM with Docker Compose, following https://www.prisma.io/docs/guides/deployment/docker.md.

1. Create `docker-test`, run `npm init -y` and `npm install express`, then run `npx prisma@latest orm init --yes --target postgres --authoring psl`. Set `"type": "module"` in package.json and add the scripts `start` (`node src/index.ts`), `contract:emit` (`prisma contract emit`), and `db:migrate` (`prisma db migrate`). Run `npx prisma@latest init` so the Prisma agent skills are installed, and use them.

2. Replace `src/prisma/contract.prisma` with a single `User` model (id, createdAt, email, name), run `npx prisma contract emit`, and write `src/index.ts`: an Express server whose `GET /` counts users with `db.orm.public.User.aggregate((a) => ({ total: a.count() }))` and reports whether any exist. Read the port from `PORT` with a default of 3000.

3. Write `docker-compose.postgres.yml` with a `postgres:17` service on port 5432 (user postgres, password prisma, database postgres, with a pg_isready healthcheck). Start it with `docker compose -f docker-compose.postgres.yml up -d`, set `DATABASE_URL="postgresql://postgres:prisma@localhost:5432/postgres"` in `.env`, run `npx prisma migration plan --name init` then `npx prisma db migrate`, start `npm start` in the background, verify `curl http://localhost:3000` returns "No users have been added yet.", stop the server, and run `docker compose -f docker-compose.postgres.yml down`.

4. Regenerate the lockfile with `rm -rf node_modules package-lock.json && npm install` so `npm ci` succeeds on Linux. Write a `.dockerignore` (node_modules, .env, .env.prod), a `Dockerfile` based on `node:24-alpine` that runs `npm ci`, copies the project, and uses `CMD ["sh", "-c", "npm run db:migrate && npm start"]`, a `docker-compose.yml` with a `postgres_db` service and a `server` service that builds the Dockerfile, publishes port 3000, waits for the database healthcheck, and loads `.env.prod`, and `.env.prod` with `DATABASE_URL="postgresql://postgres:prisma@postgres_db:5432/postgres"`.

5. Run `docker compose up --build -d`, verify `curl http://localhost:3000` returns "No users have been added yet.", show me the `server` logs, then run `docker compose down -v`.

Start by creating a small Node.js application with Express and Prisma ORM.

Create a new project directory, initialize a Node.js project, and install Express:

title="bun"
mkdir docker-test

cd docker-test

bun init

bun add express
pnpm
mkdir docker-test
cd docker-test
pnpm init
pnpm add express
yarn
mkdir docker-test
cd docker-test
yarn init
yarn add express
npm
mkdir docker-test
cd docker-test
npm init
npm install express

Installing Express first also creates package-lock.json, which is how the next command detects that you use npm.

Run orm init in the project. It preselects PostgreSQL and the PSL authoring style, installs the runtime and the CLI, and emits the contract artifacts:

bunx prisma@latest orm init --yes --target postgres --authoring psl
Bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
Bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
Bash
npx prisma@latest orm init --yes --target postgres --authoring psl
no-copy
package.json declares "type": "commonjs" ... the scaffolded prisma/db.ts uses an ESM-only import attribute (`with { type: 'json' }`) and will not load under that module type.
If you want the default, set "type": "module" in package.json.
✔ npm add @prisma/orm-postgres dotenv
✔ npm add -D prisma@latest @types/node
✔ npm add -D @prisma/cli-engine@0.6.1
✔ Emit the contract
│  target:     postgres
│  authoring:  psl
│  schema:     src/prisma/contract.prisma
written
├─ src/prisma/contract.prisma
├─ prisma.config.ts
├─ src/prisma/db.ts
├─ prisma-8.md
├─ .env.example
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.json
✔ Done. Open prisma-8.md to get started.

The command creates:

  • prisma.config.ts, which tells the CLI where the contract lives and loads .env through dotenv/config.
  • src/prisma/contract.prisma, your data model.
  • src/prisma/contract.json and src/prisma/contract.d.ts, the emitted artifacts the runtime and the CLI read. Commit both.
  • src/prisma/db.ts, the client your app imports. It reads DATABASE_URL from the environment.

The warning at the top matters: npm init -y writes "type": "commonjs", and the scaffolded db.ts is an ES module. Open package.json, switch the module type, and replace the scripts with the ones this guide uses:

package.json
{  "name": "docker-test",  "version": "1.0.0",  "scripts": {    "test": "echo \"Error: no test specified\" && exit 1",    "start": "node src/index.ts",    "contract:emit": "prisma contract emit",    "db:migrate": "prisma db migrate"  },  "type": "commonjs",  "type": "module",  "dependencies": {    "@prisma/orm-postgres": "...",    "dotenv": "...",    "express": "..."  },  "devDependencies": {    "@prisma/cli-engine": "...",    "@types/node": "...",    "prisma": "..."  }}

There is no prisma generate and no postinstall hook to add. Node.js 24 runs src/index.ts directly, so the start script needs no build step either.

package.json declares "type": "commonjs" ... the scaffolded prisma/db.ts uses an ESM-only import attribute (`with { type: 'json' }`) and will not load under that module type.

If you want the default, set "type": "module" in package.json.

✔ npm add @prisma/orm-postgres dotenv

✔ npm add -D prisma@latest @types/node

✔ npm add -D @prisma/cli-engine@0.6.1

✔ Emit the contract

│  target:     postgres

│  authoring:  psl

│  schema:     src/prisma/contract.prisma

written

├─ src/prisma/contract.prisma

├─ prisma.config.ts

├─ src/prisma/db.ts

├─ prisma-8.md

├─ .env.example

├─ tsconfig.json

├─ .gitignore

├─ .gitattributes

└─ package.json

✔ Done. Open prisma-8.md to get started.

The command creates:

  • prisma.config.ts, which tells the CLI where the contract lives and loads .env through dotenv/config.
  • src/prisma/contract.prisma, your data model.
  • src/prisma/contract.json and src/prisma/contract.d.ts, the emitted artifacts the runtime and the CLI read. Commit both.
  • src/prisma/db.ts, the client your app imports. It reads DATABASE_URL from the environment.

The warning at the top matters: npm init -y writes "type": "commonjs", and the scaffolded db.ts is an ES module. Open package.json, switch the module type, and replace the scripts with the ones this guide uses:

title="package.json"
{

  "name": "docker-test",

  "version": "1.0.0",

  "scripts": {

    "test": "echo \"Error: no test specified\" && exit 1", 

    "start": "node src/index.ts", 

    "contract:emit": "prisma contract emit",

    "db:migrate": "prisma db migrate"

  },

  "type": "commonjs", 

  "type": "module", 

  "dependencies": {

    "@prisma/orm-postgres": "...",

    "dotenv": "...",

    "express": "..."

  },

  "devDependencies": {

    "@prisma/cli-engine": "...",

    "@types/node": "...",

    "prisma": "..."

  }

}

There is no prisma generate and no postinstall hook to add. Node.js 24 runs src/index.ts directly, so the start script needs no build step either.

Replace the starter contract with a single User model:

title="src/prisma/contract.prisma"
// use prisma-8

model User {

  id        Int               @id @default(autoincrement())

  createdAt TimestamptzString @default(now())

  email     String            @unique

  name      String?

}

Emit the contract again so contract.json and contract.d.ts match the new model:

bunx prisma contract emit
Bash
pnpm prisma contract emit
Bash
yarn prisma contract emit
Bash
npx prisma contract emit
no-copy
✔ Emitted contract.json and contract.d.ts
storageHash:  f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

contract emit is offline; it needs no database.

✔ Emitted contract.json and contract.d.ts

storageHash:  f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

contract emit is offline; it needs no database.

Create src/index.ts next to the src/prisma/ folder:

title="src/index.ts"
import express from "express";

import { db } from "./prisma/db.ts";

const app = express();

app.use(express.json());

// Report whether any users exist

app.get("/", async (req, res) => {

  const { total } = await db.orm.public.User.aggregate((a) => ({

    total: a.count(),

  }));

  res.json(

    total === 0

      ? "No users have been added yet."

      : "Some users have been added to the database.",

  );

});

const PORT = Number(process.env.PORT) || 3000;

app.listen(PORT, () => {

  console.log(`Server is running on http://localhost:${PORT}`);

});

Model access is namespace-qualified on PostgreSQL (db.orm.public.User), and counting goes through .aggregate(...). There is no PrismaClient, no driver adapter, and nothing to instantiate: db.ts already exports the client.

With the application in place, the next step is a PostgreSQL database to migrate against.

To create the first migration, start a standalone PostgreSQL database with Docker Compose.

Create docker-compose.postgres.yml in the project root:

title="docker-compose.postgres.yml"
services:

  postgres:

    image: postgres:17

    restart: always

    environment:

      - POSTGRES_DB=postgres

      - POSTGRES_USER=postgres

      - POSTGRES_PASSWORD=prisma

    ports:

      - "5432:5432"

    healthcheck:

      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]

      interval: 5s

      timeout: 2s

      retries: 20

    volumes:

      - postgres_data:/var/lib/postgresql/data

volumes:

  postgres_data:
docker compose -f docker-compose.postgres.yml up -d
 Container docker-test-postgres-1  Started

Point the project at the database. orm init wrote .env.example; create .env with the connection string of the container you just started:

title=".env"
DATABASE_URL="postgresql://postgres:prisma@localhost:5432/postgres"

Plan the first migration from the emitted contract. This is offline and writes a migration package under migrations/app/:

bunx prisma migration plan --name init
Bash
pnpm prisma migration plan --name init
Bash
yarn prisma migration plan --name init
Bash
npx prisma migration plan --name init
no-copy
✔ Planned 3 operation(s)

migrations/app/20260910T1532_init
├─ Create schema "public"
├─ Create table "User"
└─ Add unique constraint on "User" (email)

from:       (baseline)
to:         f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

Apply it to the running database:

✔ Planned 3 operation(s)

migrations/app/20260910T1532_init

├─ Create schema "public"

├─ Create table "User"

└─ Add unique constraint on "User" (email)

from:       (baseline)

to:         f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

Apply it to the running database:

bunx prisma db migrate
Bash
pnpm prisma db migrate
Bash
yarn prisma db migrate
Bash
npx prisma db migrate
no-copy
│  migrations:  migrations
│  database:    postgresql://****:****@localhost:5432/postgres
✔ Applied 1 migration(s) (3 operation(s)) across 1 contract space(s)
App space
├─ Create schema "public"
├─ Create table "User"
├─ Add unique constraint on "User" (email)
└─ marker f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

db migrate applies the migration packages in migrations/ and records the resulting contract hash in the database. Running it again is safe; it reports Already up to date. This is the same command the container runs at start in step 3, which replaces prisma migrate deploy from Prisma ORM 7. There is no generate step afterwards: the runtime reads contract.json, which you already emitted.

Confirm the database and the contract agree:

│  migrations:  migrations

│  database:    postgresql://****:****@localhost:5432/postgres

✔ Applied 1 migration(s) (3 operation(s)) across 1 contract space(s)

App space

├─ Create schema "public"

├─ Create table "User"

├─ Add unique constraint on "User" (email)

└─ marker f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

db migrate applies the migration packages in migrations/ and records the resulting contract hash in the database. Running it again is safe; it reports Already up to date. This is the same command the container runs at start in step 3, which replaces prisma migrate deploy from Prisma ORM 7. There is no generate step afterwards: the runtime reads contract.json, which you already emitted.

Confirm the database and the contract agree:

bunx prisma migration status
Bash
pnpm prisma migration status
Bash
yarn prisma migration status
Bash
npx prisma migration status
no-copy
○   f340195  @contract @db
│↑  20260910T1532_init         ∅ → f340195  3 ops  ✓ applied
○   ∅
✔ Up to date
○   f340195  @contract @db

│↑  20260910T1532_init         ∅ → f340195  3 ops  ✓ applied

○   ∅

✔ Up to date

Start the server:

bun start
Bash
pnpm start
Bash
yarn start
Bash
npm start
no-copy
Server is running on http://localhost:3000

Open http://localhost:3000 or query it with curl:

Bash
curl http://localhost:3000
no-copy
"No users have been added yet."

Stop the server with Ctrl+C.

Server is running on http://localhost:3000

Open http://localhost:3000 or query it with curl:

curl http://localhost:3000
"No users have been added yet."

Stop the server with Ctrl+C.

Remove the standalone PostgreSQL container:

docker compose -f docker-compose.postgres.yml down --remove-orphans

This stops and removes the container and the default network. The named postgres_data volume stays; add -v to remove it too.

With the application tested locally, the next step is to containerize it.

Containerizing the application means it runs the same way on any host that has Docker installed.

orm init installs its packages in three steps, and on macOS and Windows the resulting package-lock.json misses two Linux-only optional packages that the Prisma CLI depends on. npm ci inside the Linux image then refuses to install:

npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.

npm error Missing: @emnapi/runtime@1.11.3 from lock file

Regenerate the lockfile once from a clean install before you build the image:

rm -rf node_modules package-lock.json

bun install
Bash
rm -rf node_modules package-lock.json
pnpm install
Bash
rm -rf node_modules package-lock.json
yarn install
Bash
rm -rf node_modules package-lock.json
npm install

The new lockfile records every platform's optional packages, so the same npm ci works on your machine and in the container.

The new lockfile records every platform's optional packages, so the same npm ci works on your machine and in the container.

Prisma ORM ships no native query engine, so the choice between Alpine and Debian base images is only about image size and your other dependencies. Both node:24-alpine and node:24-slim were tested with this guide and behave the same; no openssl or libc6-compat package is needed.

First, keep the host's node_modules and local environment files out of the image:

title=".dockerignore"
node_modules

.env

.env.prod

Then create the Dockerfile:

Docker
FROM node:24-alpine

WORKDIR /usr/src/app

COPY package.json package-lock.json ./

RUN npm ci

COPY . .

CMD ["sh", "-c", "npm run db:migrate && npm start"]

What the image needs, and why:

  • npm ci installs dev dependencies too. The container runs the Prisma CLI at start to apply migrations, so prisma has to be in the image.
  • COPY . . brings in src/prisma/contract.json, src/prisma/contract.d.ts, and migrations/. Because those are committed, the build has no Prisma step. If you prefer not to commit the emitted artifacts, add RUN npx prisma contract emit after the copy; it runs offline.
  • CMD applies pending migrations with db migrate and then starts the server. On a fresh database this creates the schema; on an existing one it reports Already up to date and moves on.
  • DATABASE_URL is not baked in. The CLI reads it through prisma.config.ts and the runtime through src/prisma/db.ts, both from the container's environment, which Compose sets in the next step.

With the Dockerfile ready, use Docker Compose to manage the app and the database together.

Create docker-compose.yml in the project root:

title="docker-compose.yml"
services:

  postgres_db:

    image: postgres:17

    hostname: postgres_db

    container_name: postgres_db

    restart: always

    environment:

      POSTGRES_DB: postgres

      POSTGRES_USER: postgres

      POSTGRES_PASSWORD: prisma

    ports:

      - "5432:5432"

    healthcheck:

      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]

      interval: 5s

      timeout: 2s

      retries: 20

  server:

    build:

      context: .

      dockerfile: Dockerfile

    ports:

      - "3000:3000"

    depends_on:

      postgres_db:

        condition: service_healthy

    env_file:

      - .env.prod

The server service waits for the database healthcheck before it starts, so db migrate never runs against a database that is still booting.

Inside the Compose network the database is reachable by its service name, not localhost. Create .env.prod with that hostname:

title=".env.prod"
DATABASE_URL="postgresql://postgres:prisma@postgres_db:5432/postgres"

Compose strips the quotes when it loads the file. .env (with the localhost URL) stays on your machine for local commands; .dockerignore keeps it out of the image.

Build the image and start both services:

docker compose up --build -d
[+] up 4/4

 ✔ Image docker-test-server       Built                                     1.4s

 ✔ Network docker-test_default    Created                                   0.0s

 ✔ Container postgres_db          Healthy                                   5.7s

 ✔ Container docker-test-server-1 Started                                   5.7s

Check the app:

curl http://localhost:3000
"No users have been added yet."

The server logs show the migration running before the server starts:

docker compose logs server
server-1  | > docker-test@1.0.0 db:migrate

server-1  | > prisma db migrate

server-1  | {"kind":"result","envelope":{"ok":true,"commandId":"db.migrate","result":{"ok":true,"migrationsApplied":1,"migrationsTotal":1,...,"summary":"Applied 1 migration(s) (3 operation(s)) across 1 contract space(s)",...}}}

server-1  | > docker-test@1.0.0 start

server-1  | > node src/index.ts

server-1  | Server is running on http://localhost:3000

The CLI prints JSON lines when it has no terminal attached, which is what you see in container logs. Restart the server and the migration step becomes a no-op:

docker compose restart server

docker compose logs --since 30s server
server-1  | ...,"summary":"Already up to date",...

server-1  | Server is running on http://localhost:3000

Your Prisma ORM app and database now run together under Docker Compose. To stop everything and remove the containers, network, and volumes:

docker compose down -v

Prisma Studio lets you view and edit your data in the browser. With the Compose stack running, the database is published on port 5432, so run Studio on your machine against it. Studio ships with the Prisma ORM 7 CLI and takes the connection string directly; run it from a directory outside the project, because that CLI cannot read the Prisma ORM 8 prisma.config.ts:

cd .. && npx prisma@prev studio --url "postgresql://postgres:prisma@localhost:5432/postgres"
Prisma Studio is running at: http://localhost:5555

Open http://localhost:5555 to browse the User table. Once the database has an applied migration, Studio also shows a Migrations view with the history db migrate recorded; see Studio with Prisma ORM.

Running Studio as a Compose service, as the Prisma ORM 7 guide did, does not work from the host: the Studio server binds to 127.0.0.1 inside its container, so a published port never reaches it.

  • npm ci fails in the image with Missing: @emnapi/runtime ... from lock file. The lockfile orm init leaves behind on macOS or Windows lacks Linux-only optional packages. Regenerate it as in step 3.1.
  • db.ts will not load. If orm init warned about "type": "commonjs", set "type": "module" in package.json. The scaffolded db.ts imports contract.json with an ES module import attribute.
  • The image is large. The built image measured 2.12 GB on disk on node:24-alpine and 2.23 GB on node:24-slim (the DISK USAGE column of docker image ls), almost all of it the prisma dev dependency, which bundles the Composer and deploy toolchain. The image needs the CLI because it runs db migrate at start.
  • docker run --env-file keeps the quotes. Compose strips the quotes around DATABASE_URL in .env.prod; plain docker run --env-file does not, and the Prisma CLI then rejects the URL. Pass -e DATABASE_URL=... without quotes when you run the image by hand.
  • Port 3000 or 5432 is already in use. Change the host side of the port mapping ("3100:3000") or stop the service that holds the port; the container side stays the same.

Run npx prisma@latest init once to install the Prisma ORM skills for your coding agent and keep them matching your installed packages. Prompts that map to this guide:

  • "Using the prisma-8 skill, add a POST /users route that creates a user from the request body."
  • "Add a Post model with an author relation to the contract, plan a migration named add_posts, and apply it with db migrate."
  • "Add a healthcheck to the server service in docker-compose.yml that hits GET / once the migration has run."
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu