Back to Blog
Tutorial10 min read2026-07-02

How to Deploy a KeystoneJS App with a Database

KeystoneJS gives you a typed schema, a GraphQL API, and an admin UI from a single config file. Here's how to deploy it to production with PostgreSQL, the right build commands, and clean migrations.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

KeystoneJS is a headless CMS and application framework where you describe your data once — lists, fields, access control — and Keystone generates a Prisma-backed database layer, a GraphQL API, and an Admin UI. Deploying it well comes down to understanding its build steps and Prisma migration model.

The shape of a Keystone app

Everything starts in keystone.ts:

import { config, list } from '@keystone-6/core';
import { text, timestamp } from '@keystone-6/core/fields';

export default config({
  db: {
    provider: 'postgresql',
    url: process.env.DATABASE_URL,
    // Do NOT use this in production:
    // prismaMigrationManagement is handled via the CLI
  },
  lists: {
    Post: list({
      access: { /* ... */ },
      fields: {
        title: text({ validation: { isRequired: true } }),
        content: text(),
        publishedAt: timestamp()
      }
    })
  }
});

Keystone uses Prisma under the hood. The key production decision is how migrations are applied.

Dev vs production migrations

In development, keystone dev can push schema changes automatically. In production you want explicit, versioned Prisma migrations:

# During development, generate a migration when the schema changes
keystone prisma migrate dev --name add_published_at

# In production, apply existing migrations (no schema diffing)
keystone prisma migrate deploy

migrate deploy only applies migrations that already exist in your repo — it never generates new ones or prompts. That determinism is exactly what you want on a server.

Set db.useMigrations thinking carefully: for production deploys, commit your migrations/ folder and run migrate deploy as a deploy step.

Build steps

Keystone has a distinct build phase that generates the Prisma client, the GraphQL schema, and the Admin UI:

# Generates artifacts and builds the Admin UI
keystone build

# Starts the production server (defaults to PORT 3000)
keystone start

A common mistake is running keystone start without a prior keystone build — the Admin UI will be missing. Order matters:

FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx keystone build

FROM node:20-slim
WORKDIR /app
COPY --from=build /app ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npx", "keystone", "start"]

Sessions and secrets

If you use Keystone's session support, set a stable session secret (minimum 32 characters):

import { statelessSessions } from '@keystone-6/core/session';

const session = statelessSessions({
  secret: process.env.SESSION_SECRET,
  maxAge: 60 * 60 * 24 * 30
});

Changing SESSION_SECRET invalidates all sessions, so keep it constant per environment.

Deploying on PandaStack

  1. 1Provision a managed PostgreSQL instance. PandaStack injects DATABASE_URL automatically — Keystone reads it directly.
  2. 2Connect the Git repo as a container app. The Dockerfile is auto-detected; otherwise the Node buildpack runs keystone build then keystone start (set the start command if needed).
  3. 3Set SESSION_SECRET and NODE_ENV=production.
  4. 4Run npx keystone prisma migrate deploy once before the app serves traffic.
  5. 5Add a custom domain; SSL provisions automatically.

The GraphQL API is at /api/graphql and the Admin UI at /. The first time you load the Admin UI, Keystone walks you through creating an initial user if your schema is set up for it.

Production checklist

  • [ ] Committed migrations/ folder, applied via migrate deploy.
  • [ ] keystone build runs before keystone start.
  • [ ] Stable SESSION_SECRET (>=32 chars).
  • [ ] Access control configured on every list (default-deny mindset).
  • [ ] Connection pool sized to your DB instance.
  • [ ] NODE_ENV=production.

A note on access control

Keystone's access control is code, which is powerful but easy to leave too open. Audit every list's access rules before going live — an unset rule can mean "allow." Start restrictive and open up deliberately.

Verifying

# GraphQL endpoint should respond to introspection
curl -s -X POST https://app.example.com/api/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"{ __typename }"}'

# Admin UI loads
curl -s -o /dev/null -w '%{http_code}' https://app.example.com/

A valid GraphQL response and a 200 on the Admin UI confirm the build and database wiring.

References

  • [KeystoneJS production guide](https://keystonejs.com/docs/guides/deployment)
  • [Keystone command line (build/start/migrate)](https://keystonejs.com/docs/guides/cli)
  • [Keystone database configuration](https://keystonejs.com/docs/config/config#db)
  • [Prisma migrate deploy](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production)

---

Keystone's build-then-start flow and Prisma migrations run cleanly on PandaStack, with a managed PostgreSQL and DATABASE_URL injected for you. Try it free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also