Prisma ORM 8 is here.Read the docs
Deployment

Docker

Build an Express app on Prisma ORM, run PostgreSQL from Docker Compose, then run the app and the database together in containers.

Introduction

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.

Prerequisites

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

Use with your agent

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

Use with your agent
Use with your agent
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`.

1. Set up your Node.js and Prisma ORM application

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

1.1. Initialize your project

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

mkdir docker-test
cd docker-test
bun init
bun add express

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

1.2. Add Prisma ORM

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
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.3
✔ 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.

1.3. Define the data model

Replace the starter contract with a single User model:

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
✔ Emitted contract.json and contract.d.ts
storageHash:  f340195186d0acf8e9914097e58af0375650e7251358382aa9ad3ed5b724daf7

contract emit is offline; it needs no database.

1.4. Create an Express server

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

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.

2. Set up a PostgreSQL database with Docker Compose

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

2.1. Create a Docker Compose file for PostgreSQL

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

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:

2.2. Start the PostgreSQL container

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

2.3. Create and apply the first migration

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

.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
✔ 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
│  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
○   f340195  @contract @db
│↑  20260910T1532_init         ∅ → f340195  3 ops  ✓ applied
○   ∅
✔ Up to date

2.4. Test the application

Start the server:

bun start
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.

2.5. Clean up the standalone database

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.

3. Run the app and database together with Docker Compose

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

3.1. Regenerate the lockfile

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

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

3.2. Create the Dockerfile

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:

.dockerignore
node_modules
.env
.env.prod

Then create the Dockerfile:

Dockerfile
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.

3.3. Create and configure a Docker Compose file

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

Create docker-compose.yml in the project root:

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.

3.4. Configure the environment variable for the container

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

.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.

3.5. Build and run the application

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

3.6. Bonus: browse the database with Prisma Studio

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.

Common gotchas

  • 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.

Prompt your coding agent

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."

Next steps

On this page