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 deploymigrate 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 startA 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
- 1Provision a managed PostgreSQL instance. PandaStack injects
DATABASE_URLautomatically — Keystone reads it directly. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected; otherwise the Node buildpack runs
keystone buildthenkeystone start(set the start command if needed). - 3Set
SESSION_SECRETandNODE_ENV=production. - 4Run
npx keystone prisma migrate deployonce before the app serves traffic. - 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 viamigrate deploy. - [ ]
keystone buildruns beforekeystone 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).