Back to Blog
Tutorial10 min read2026-07-03

How to Deploy KeystoneJS to the Cloud

A practical guide to deploying KeystoneJS 6 to production with a managed PostgreSQL database, persistent media storage, and automatic SSL — no Dockerfile gymnastics required.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

KeystoneJS is one of the most flexible headless CMS frameworks in the Node.js world. Unlike turnkey systems, Keystone is *code-first*: you define your schema in TypeScript, and it generates an Admin UI plus a GraphQL API. That power comes with a deployment catch — Keystone needs a real database, runs a build step that bakes the Admin UI, and expects a long-running Node process. This guide walks through getting a production Keystone 6 instance live.

What Keystone needs in production

Before touching any platform, it helps to enumerate Keystone's runtime requirements:

  • A SQL database. Keystone uses Prisma under the hood and supports PostgreSQL, MySQL, and SQLite. SQLite is fine for prototypes but unsuitable for a stateless container — use PostgreSQL in production.
  • A build step. keystone build compiles the Admin UI (a Next.js app) and generates the Prisma client. This must run before the app starts.
  • Migrations. In production you should run prisma migrate deploy rather than letting Keystone push schema changes automatically.
  • A persistent secret. SESSION_SECRET must be stable across restarts or every user gets logged out on redeploy.

Step 1: Configure Keystone for PostgreSQL

In your keystone.ts, point the database config at an environment variable and enable migrations:

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

export default config({
  db: {
    provider: 'postgresql',
    url: process.env.DATABASE_URL!,
    // run prisma migrate deploy yourself in production
    prismaClientPath: 'node_modules/.prisma/client',
  },
  server: {
    port: Number(process.env.PORT) || 3000,
  },
  // ...lists, session, etc.
});

The key detail: bind to process.env.PORT. Most managed platforms inject a port and expect your app to listen on it.

Step 2: Make the build reproducible

Keystone projects work cleanly with auto-detected Node buildpacks, but being explicit avoids surprises. Add these scripts to package.json:

{
  "scripts": {
    "build": "keystone build",
    "start": "keystone start --with-migrations",
    "postinstall": "keystone postinstall --fix"
  }
}

keystone start --with-migrations runs prisma migrate deploy automatically at boot, which is exactly what you want in an ephemeral container — the container starts, applies any pending migrations, then serves traffic.

Step 3: Deploy on PandaStack

PandaStack auto-detects Node projects, so the flow is genuinely "push code, it runs."

  1. 1Create a PostgreSQL database from the dashboard (Keystone 6 works well with PostgreSQL 16.x). PandaStack injects a DATABASE_URL into your service automatically when you link the database — which is the exact variable Keystone reads.
  2. 2Create a container app and connect your Git repo. The buildpack detects Node, runs npm install, then your build script.
  3. 3Set environment variables:
SESSION_SECRET=<a long random string, 32+ chars>
NODE_ENV=production
# DATABASE_URL is auto-wired from the linked database
  1. 1Set the start command to npm start (which maps to keystone start --with-migrations).

That's it. On push, PandaStack runs the build in a rootless BuildKit job, pushes the image to its registry, and Helm-deploys it. You get live build logs while it compiles the Admin UI.

Handling file uploads

Keystone's local file/image storage writes to disk, which does not survive container restarts. For production, route uploads to S3-compatible object storage. Keystone supports this natively:

storage: {
  s3_images: {
    kind: 's3',
    type: 'image',
    bucketName: process.env.S3_BUCKET!,
    region: process.env.S3_REGION!,
    accessKeyId: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!,
    endpoint: process.env.S3_ENDPOINT, // for MinIO/R2
    forcePathStyle: true,
  },
},

If you want to keep everything self-hosted, pair Keystone with a MinIO instance (see our MinIO deployment guide). Point S3_ENDPOINT at your MinIO service URL.

Production checklist

ConcernRecommendation
DatabaseManaged PostgreSQL, not SQLite
Migrationsprisma migrate deploy via --with-migrations
SessionsStable SESSION_SECRET env var
UploadsS3-compatible storage, never local disk
Admin UIRestrict via Keystone access control, not just obscurity
SecretsNever commit .env; use platform env vars

Common pitfalls

  • "Prisma client not generated" — usually means postinstall didn't run. Ensure the script is present and the build didn't skip dev dependencies.
  • Admin UI 404s after deploy — the keystone build step didn't complete. Check build logs; Keystone needs enough memory to compile the Next.js Admin UI. Bump to a larger compute tier if the build OOMs.
  • Logged out on every deploySESSION_SECRET is being auto-generated per boot. Set it explicitly.

Wrapping up

Keystone is a fantastic choice when you want a CMS you can shape with code rather than a config UI. The deployment story comes down to three things: a managed Postgres database, a build step that compiles the Admin UI, and stable secrets. Get those right and Keystone runs happily as a stateless container.

PandaStack's free tier includes container apps and a managed database, which is enough to run a small Keystone instance end to end. Connect your repo and the DATABASE_URL wiring happens for you — spin one up at https://dashboard.pandastack.io.

References

  • KeystoneJS documentation: https://keystonejs.com/docs
  • Keystone production deployment guide: https://keystonejs.com/docs/guides/production
  • Keystone file storage / S3 config: https://keystonejs.com/docs/config/config#storage
  • Prisma migrate deploy: https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production
  • PostgreSQL 16 release notes: https://www.postgresql.org/docs/16/release-16.html

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also