# Docker (/docs/guides/deployment/docker)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

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

Location: Guides > Deployment > Docker

## Introduction [#introduction]

This guide walks you through running a Prisma ORM application in Docker. You build a small Node.js app with [Express](https://expressjs.com/), 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.

> [!NOTE]
> Using Prisma ORM 7?
> 
> Prisma ORM 8 is the current release. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at [/guides/v7/deployment/docker](https://www.prisma.io/docs/guides/v7/deployment/docker).

## Prerequisites [#prerequisites]

* [Docker](https://docs.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed
* [Node.js](https://nodejs.org) 24 or later

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:

```bash
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:

```bash
docker ps -q | xargs docker stop
```

## Use with your agent [#use-with-your-agent]

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

```text
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@latest 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@latest migration plan --name init` then `npx prisma@latest 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 [#1-set-up-your-nodejs-and-prisma-orm-application]

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

### 1.1. Initialize your project [#11-initialize-your-project]

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

  

#### bun

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

#### pnpm

```bash
mkdir docker-test
cd docker-test
pnpm init
pnpm add express
```

#### yarn

```bash
mkdir docker-test
cd docker-test
yarn init
yarn add express
```

#### npm

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

### 1.2. Add Prisma ORM [#12-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:

  

#### bun

```bash
bunx prisma@latest orm init --yes --target postgres --authoring psl
```

#### pnpm

```bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### yarn

```bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### npm

```bash
npx prisma@latest orm init --yes --target postgres --authoring psl
```

```text 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.3.0
✔ 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:

```json title="package.json"
{
  "name": "docker-test",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1", // [!code --]
    "start": "node src/index.ts", // [!code ++]
    "contract:emit": "prisma contract emit",
    "db:migrate": "prisma db migrate" // [!code ++]
  },
  "type": "commonjs", // [!code --]
  "type": "module", // [!code ++]
  "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 [#13-define-the-data-model]

Replace the starter contract with a single `User` model:

```prisma 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:

  

#### bun

```bash
bunx prisma@latest contract emit
```

#### pnpm

```bash
pnpm dlx prisma@latest contract emit
```

#### yarn

```bash
yarn dlx prisma@latest contract emit
```

#### npm

```bash
npx prisma@latest contract emit
```

```text no-copy
✔ Emitted contract.json and contract.d.ts
storageHash:  3bb5103a83e513d042e9a53dec29c899f92b5209b3b88944fee251cbe86c0e9b
```

`contract emit` is offline; it needs no database.

### 1.4. Create an Express server [#14-create-an-express-server]

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

```typescript 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](https://www.prisma.io/docs/orm/fundamentals/reading-data#count-records) 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 [#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 [#21-create-a-docker-compose-file-for-postgresql]

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

```yml 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:
```

### 2.2. Start the PostgreSQL container [#22-start-the-postgresql-container]

```bash
docker compose -f docker-compose.postgres.yml up -d
```

```text no-copy
 Container docker-test-postgres-1  Started
```

### 2.3. Create and apply the first migration [#23-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:

```bash title=".env"
DATABASE_URL="postgresql://postgres:prisma@localhost:5432/postgres" # [!code highlight]
```

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

  

#### bun

```bash
bunx prisma@latest migration plan --name init
```

#### pnpm

```bash
pnpm dlx prisma@latest migration plan --name init
```

#### yarn

```bash
yarn dlx prisma@latest migration plan --name init
```

#### npm

```bash
npx prisma@latest migration plan --name init
```

```text 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:         3bb5103a83e513d042e9a53dec29c899f92b5209b3b88944fee251cbe86c0e9b
```

Apply it to the running database:

  

#### bun

```bash
bunx prisma@latest db migrate
```

#### pnpm

```bash
pnpm dlx prisma@latest db migrate
```

#### yarn

```bash
yarn dlx prisma@latest db migrate
```

#### npm

```bash
npx prisma@latest db migrate
```

```text 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 3bb5103a83e513d042e9a53dec29c899f92b5209b3b88944fee251cbe86c0e9b
```

`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:

  

#### bun

```bash
bunx prisma@latest migration status
```

#### pnpm

```bash
pnpm dlx prisma@latest migration status
```

#### yarn

```bash
yarn dlx prisma@latest migration status
```

#### npm

```bash
npx prisma@latest migration status
```

```text no-copy
○   3bb5103  @contract @db
│↑  20260910T1532_init         ∅ → 3bb5103  3 ops  ✓ applied
○   ∅
✔ Up to date
```

### 2.4. Test the application [#24-test-the-application]

Start the server:

  

#### bun

```bash
bun start
```

#### pnpm

```bash
pnpm start
```

#### yarn

```bash
yarn start
```

#### npm

```bash
npm start
```

```text no-copy
Server is running on http://localhost:3000
```

Open [`http://localhost:3000`](http://localhost:3000) or query it with curl:

```bash
curl http://localhost:3000
```

```json no-copy
"No users have been added yet."
```

Stop the server with `Ctrl+C`.

### 2.5. Clean up the standalone database [#25-clean-up-the-standalone-database]

Remove the standalone PostgreSQL container:

```bash
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 [#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 [#31-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:

```text no-copy
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:

  

#### bun

```bash
rm -rf node_modules package-lock.json
bun install
```

#### pnpm

```bash
rm -rf node_modules package-lock.json
pnpm install
```

#### yarn

```bash
rm -rf node_modules package-lock.json
yarn install
```

#### npm

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

### 3.2. Create the Dockerfile [#32-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:

```text title=".dockerignore"
node_modules
.env
.env.prod
```

Then create the `Dockerfile`:

```shell title="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 [#33-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:

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

### 3.4. Configure the environment variable for the container [#34-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:

```bash title=".env.prod"
DATABASE_URL="postgresql://postgres:prisma@postgres_db:5432/postgres" # [!code highlight]
```

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 [#35-build-and-run-the-application]

Build the image and start both services:

```bash
docker compose up --build -d
```

```text no-copy
 Container postgres_db  Healthy
 Container docker-test-server-1  Starting
 Container docker-test-server-1  Started
```

Check the app:

```bash
curl http://localhost:3000
```

```json no-copy
"No users have been added yet."
```

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

```bash
docker compose logs server
```

```text no-copy
docker-test-server-1  | > docker-test@1.0.0 db:migrate
docker-test-server-1  | > prisma db migrate
docker-test-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)",...}}}
docker-test-server-1  | > docker-test@1.0.0 start
docker-test-server-1  | > node src/index.ts
docker-test-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:

```bash
docker compose restart server
docker compose logs --since 30s server
```

```text no-copy
docker-test-server-1  | ...,"summary":"Already up to date",...
docker-test-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:

```bash
docker compose down -v
```

### 3.6. Bonus: browse the database with Prisma Studio [#36-bonus-browse-the-database-with-prisma-studio]

[Prisma Studio](https://www.prisma.io/docs/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`:

```bash
cd .. && npx prisma@prev studio --url "postgresql://postgres:prisma@localhost:5432/postgres"
```

```text no-copy
Prisma Studio is running at: http://localhost:5555
```

Open [`http://localhost:5555`](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](https://www.prisma.io/docs/studio/prisma-next).

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 [#common-gotchas]

> [!WARNING]
> Do not call `db.runtime().close()` in a route handler. The client's connection pool is shared across requests; close it only on process shutdown.

* **`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](#31-regenerate-the-lockfile).
* **`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 1.41 GB on `node:24-alpine` and 1.5 GB on `node:24-slim`, 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 [#prompt-your-coding-agent]

Run [`npx prisma@latest init`](https://www.prisma.io/docs/cli/init) once to install the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) 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 [#next-steps]

* Change the schema in `src/prisma/contract.prisma`, run `npm run contract:emit`, then `npx prisma@latest migration plan --name <change>`; the next `docker compose up --build` applies it. See [Generating a migration](https://www.prisma.io/docs/orm/migrations/generating-a-migration) and [Applying a migration](https://www.prisma.io/docs/orm/migrations/applying-a-migration).
* [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes.
* [Read the Prisma ORM overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries.

## Related pages

- [`Bun workspaces`](https://www.prisma.io/docs/guides/deployment/bun-workspaces): Set up Prisma ORM in a Bun workspaces monorepo through a shared database package, seed it with Bun, and query it from a Next.js app in the same workspace.
- [`Cloudflare Workers`](https://www.prisma.io/docs/guides/deployment/cloudflare-workers): Add Prisma ORM to a Cloudflare Worker, query PostgreSQL from the fetch handler with the nodejs_compat flag, and deploy it with Wrangler.
- [`pnpm workspaces`](https://www.prisma.io/docs/guides/deployment/pnpm-workspaces): Set up Prisma 8 in a shared database package inside a pnpm workspaces monorepo and query it from a Next.js app.
- [`Turborepo`](https://www.prisma.io/docs/guides/deployment/turborepo): Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.