Back to Blog
Tutorial9 min read2026-07-01

How to Deploy Strapi with PostgreSQL

Strapi defaults to SQLite, which falls apart in a containerized world. Here's how to connect Strapi 5 to a managed PostgreSQL database and deploy it cleanly to production.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Strapi is the most popular open-source Node.js headless CMS, and for good reason — the admin UI is polished and the content modeling is fast. But its default SQLite database is a trap for production: containers are ephemeral, so a SQLite file on disk vanishes on every redeploy. The fix is to connect Strapi to PostgreSQL. This guide covers that switch and a clean production deploy.

Why SQLite fails in containers

SQLite stores your entire database in a single file. In a stateless container, that file lives on the container's local filesystem, which is wiped when the container restarts, redeploys, or scales. You'll lose content silently. PostgreSQL solves this by living in a separate, durable, managed service. Plus, you can't run multiple Strapi replicas against one SQLite file — Postgres makes horizontal scaling possible.

Step 1: Install the Postgres driver

npm install pg

Strapi uses Knex internally; pg is the only client you need.

Step 2: Configure the database connection

Strapi reads database config from config/database.ts (or .js). Make it environment-driven so the same code works locally and in production:

export default ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      connectionString: env('DATABASE_URL'),
      ssl: env.bool('DATABASE_SSL', false) && {
        rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
      },
    },
    pool: {
      min: env.int('DATABASE_POOL_MIN', 2),
      max: env.int('DATABASE_POOL_MAX', 10),
    },
  },
});

Using DATABASE_URL is the cleanest approach because most managed platforms inject exactly that variable.

Step 3: Set the production secrets

Strapi requires several secrets that *must* be stable across restarts, or admin sessions and encrypted data break:

APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=<random>
ADMIN_JWT_SECRET=<random>
TRANSFER_TOKEN_SALT=<random>
JWT_SECRET=<random>
ENCRYPTION_KEY=<random>
NODE_ENV=production

Generate each with openssl rand -base64 32. Never let these auto-generate per boot.

Step 4: Configure the build

Strapi 5 needs to build the admin panel before serving:

{
  "scripts": {
    "build": "strapi build",
    "start": "strapi start"
  }
}

The admin build is memory-hungry. If it OOMs on a tiny instance, bump to a larger compute tier for the build.

Step 5: Deploy on PandaStack

The end-to-end flow is short because the platform auto-wires the database:

  1. 1Create a managed PostgreSQL database (Strapi 5 supports PostgreSQL 14.x and 16.x).
  2. 2Create a container app from your Git repo. PandaStack detects Node and runs npm install then npm run build.
  3. 3Link the database to the app — DATABASE_URL is injected automatically, which is exactly what your config/database.ts reads. This is the "auto-wired DB" experience: you don't copy-paste a connection string.
  4. 4Add the secrets from Step 3 as environment variables.
  5. 5Set the start command to npm start.

Push, and Strapi builds the admin panel, connects to Postgres, and goes live with automatic SSL on your domain.

Step 6: Handle media uploads

Like SQLite, Strapi's default local file upload provider writes to disk and loses files on redeploy. Switch to an S3-compatible provider:

npm install @strapi/provider-upload-aws-s3
// config/plugins.ts
export default ({ env }) => ({
  upload: {
    config: {
      provider: 'aws-s3',
      providerOptions: {
        s3Options: {
          endpoint: env('S3_ENDPOINT'),
          region: env('S3_REGION'),
          forcePathStyle: true,
          credentials: {
            accessKeyId: env('S3_ACCESS_KEY'),
            secretAccessKey: env('S3_SECRET_KEY'),
          },
          params: { Bucket: env('S3_BUCKET') },
        },
      },
    },
  },
});

Point it at S3, Cloudflare R2, or a self-hosted MinIO bucket.

Migration checklist

ItemSQLite (dev)PostgreSQL (prod)
PersistenceLost on restartDurable
Multiple replicasImpossibleSupported
Media filesLocal diskS3-compatible
SecretsCan be autoMust be fixed

Common pitfalls

  • "relation does not exist" on first boot — Strapi creates tables automatically on start, but the database user needs CREATE privileges. Managed databases grant this by default.
  • Logged out of admin after deploy — your ADMIN_JWT_SECRET or APP_KEYS changed. Pin them.
  • SSL connection errors — managed Postgres often requires SSL. Set DATABASE_SSL=true.

Wrapping up

Moving Strapi off SQLite is the single most important production change you can make. Add a managed PostgreSQL database, pin your secrets, and offload media to S3 — then Strapi behaves like the stateless, scalable container it should be.

With PandaStack, the Postgres connection is auto-wired into your app, so you skip the connection-string juggling entirely. The free tier includes a database and container apps — try it at https://dashboard.pandastack.io.

References

  • Strapi database configuration: https://docs.strapi.io/dev-docs/configurations/database
  • Strapi deployment docs: https://docs.strapi.io/dev-docs/deployment
  • Strapi upload providers: https://docs.strapi.io/dev-docs/plugins/upload
  • node-postgres (pg): https://node-postgres.com/
  • PostgreSQL connection strings: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also