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 pgStrapi 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=productionGenerate 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:
- 1Create a managed PostgreSQL database (Strapi 5 supports PostgreSQL 14.x and 16.x).
- 2Create a container app from your Git repo. PandaStack detects Node and runs
npm installthennpm run build. - 3Link the database to the app —
DATABASE_URLis injected automatically, which is exactly what yourconfig/database.tsreads. This is the "auto-wired DB" experience: you don't copy-paste a connection string. - 4Add the secrets from Step 3 as environment variables.
- 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
| Item | SQLite (dev) | PostgreSQL (prod) |
|---|---|---|
| Persistence | Lost on restart | Durable |
| Multiple replicas | Impossible | Supported |
| Media files | Local disk | S3-compatible |
| Secrets | Can be auto | Must 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_SECRETorAPP_KEYSchanged. 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