Back to Blog
Tutorial8 min read2026-07-12

How to Deploy RedwoodJS with PostgreSQL on PandaStack

Build RedwoodJS for production, switch Prisma to Postgres, run migrations safely, and ship the whole app — web and API — as one container via Git push.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

RedwoodJS is a full-stack framework with an unusual shape: one repo, two "sides." The web side is a React SPA, the api side is a GraphQL server backed by Prisma. That split is great for development and slightly awkward for deployment, because you have to decide whether to ship one process or two. (Worth knowing: the Redwood team pivoted to a new project, RedwoodSDK, in 2025 — the classic GraphQL framework this tutorial covers continues under community maintenance and still deploys fine.)

The simplest production setup — and the one I'd recommend unless you have a reason not to — is a single container that serves both sides. Here's the whole path: Postgres, build, migrations, Dockerfile, deploy.

Project anatomy, deployment edition

The parts of a Redwood project that matter for shipping:

├── api/
│   ├── db/schema.prisma      # Prisma schema — your database lives here
│   └── dist/                 # compiled api side (after build)
├── web/
│   └── dist/                 # compiled static assets (after build)
├── redwood.toml              # ports, apiUrl, env-var exposure
└── .env                      # local only — never committed, never deployed

Redwood is yarn-first (modern versions use Yarn via corepack) and expects Node 20+. Match that in production.

Point Prisma at PostgreSQL

New Redwood projects default to SQLite, which is fine locally and wrong in production. Open api/db/schema.prisma and change the datasource:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Locally, put a connection string in .env:

DATABASE_URL=postgresql://user:pass@localhost:5432/myapp_dev

Then create your first migration:

yarn rw prisma migrate dev --name init

migrate dev is a development command — it can reset your database when the schema drifts. In production you'll use migrate deploy instead, which only applies pending migrations and never resets anything. Keep that distinction sacred.

The production build

One command builds both sides:

yarn rw prisma generate   # explicit, so builds never depend on a stale client
yarn rw build

This produces web/dist (static assets, hashed filenames) and api/dist (compiled GraphQL functions). Then serve both from a single process:

yarn rw serve

yarn rw serve runs Redwood's API server and serves the web assets from the same process, with the web side's GraphQL requests routed to the API under the path configured as apiUrl in redwood.toml. The default web port is 8910. Check your redwood.toml:

[web]
  port = 8910
  apiUrl = "/.redwood/functions"
[api]
  port = 8911

With the single-server setup you expose only 8910; the apiUrl stays a relative path so the browser talks to the same origin — no CORS configuration, no second hostname.

Environment variables: the web-side gotcha

This one bites almost everyone once. The api side sees every environment variable via process.env, like any Node process. The web side sees nothing by default — it's a static bundle, and Redwood only inlines variables you explicitly allow:

[web]
  includeEnvironmentVariables = ["PUBLIC_ANALYTICS_ID"]

Variables prefixed REDWOOD_ENV_ are also exposed to the web side automatically. Two consequences:

  1. 1Secrets are safe by default — DATABASE_URL can't leak into the browser bundle.
  2. 2Web-side variables are baked in at build time. Changing one means rebuilding, not restarting.

A Dockerfile that works

Recent Redwood versions can generate a Dockerfile for you; if you'd rather see exactly what's happening, this minimal one works:

FROM node:20-slim

# Prisma's engines need OpenSSL, which node:*-slim doesn't include
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*

WORKDIR /app
RUN corepack enable

COPY . .
RUN yarn install --immutable
RUN yarn rw prisma generate && yarn rw build

ENV NODE_ENV=production
EXPOSE 8910
CMD ["yarn", "rw", "serve"]

Two details worth calling out:

  • The OpenSSL line is not optional. Prisma's query engine fails on Debian slim images without it, with an error message that does not obviously say "install openssl."
  • yarn install --immutable is the Yarn Berry equivalent of npm ci — it fails the build if the lockfile is out of date instead of silently drifting.

Deploying on PandaStack

Redwood's needs map cleanly onto a container platform with a managed database:

  1. 1Provision PostgreSQL first. Create a managed Postgres instance (14.x or 16.x are available) from the PandaStack dashboard.
  2. 2Connect the repo as a container app. With the Dockerfile above in the repo root, the platform builds it — images are built with rootless BuildKit in ephemeral Kubernetes job pods, and the build logs stream live, so when yarn rw build fails you watch it fail in real time instead of digging afterwards.
  3. 3Attach the database to the app. DATABASE_URL is injected into the container's environment automatically — the exact variable Prisma's env("DATABASE_URL") expects. No credentials to copy, nothing extra to configure.
  4. 4Set remaining env vars in the dashboard: SESSION_SECRET if you use Redwood's dbAuth, plus any API keys your services need.
  5. 5Push to deploy. Every subsequent git push triggers a build and rollout.

Running migrations in production

Never run migrations inside the container's startup command. Two replicas starting simultaneously will race on the same migration table. Instead, run migrations as a discrete step before the new version takes traffic:

yarn rw prisma migrate deploy

Run it as a release step or a one-off command against the app's environment (it already has DATABASE_URL). migrate deploy is forward-only and idempotent — running it twice is a no-op, which is exactly the property you want in an automated pipeline.

Gotchas checklist

  • SQLite artifacts: if you started on SQLite, delete api/db/migrations and re-create migrations against Postgres. Prisma migrations are provider-specific; SQLite-era migration files will fail on Postgres.
  • Bind address: serving inside a container, make sure the server listens on 0.0.0.0, not localhost — otherwise the platform's router can't reach it and health checks fail with a confusing "connection refused."
  • migrate dev in prod: worth repeating — it can reset the database. CI and production only ever run migrate deploy.
  • Web env changes need rebuilds: if a REDWOOD_ENV_ value looks stale in the browser, you changed the variable but didn't redeploy.
  • Connection limits: Prisma opens a connection pool per instance. If you scale replicas up, watch your database's connection ceiling and size connection_limit in the DATABASE_URL query string accordingly.

That's the whole path: Postgres via one env var, one build command, one container, migrations as a separate forward-only step. If you want to try it with the database wiring handled for you, spin it up on [pandastack.io](https://pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also