Back to Blog
Tutorial10 min read2026-07-03

How to Deploy a Remix App with a Managed Database

Remix runs as a long-lived Node server, which makes it a natural fit for containers plus a managed Postgres. Here's a full-stack deploy with loaders, actions, and Prisma wired end to end.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Why Remix likes a container

Remix (now folded into React Router v7's framework mode, but the deployment shape is the same) is a server-first framework. Loaders and actions run on the server on every request, which means you want a long-running Node process, not ephemeral functions that cold-start per call. A container platform with a managed database next to it is the sweet spot.

Step 1: Build and serve

Remix's standard Express/Node server build serves both SSR and assets. Make sure the server binds to the injected port:

// server.js
import { createRequestHandler } from '@remix-run/express';
import express from 'express';

const app = express();
app.use(express.static('build/client', { maxAge: '1h' }));
app.all('*', createRequestHandler({ build: await import('./build/server/index.js') }));

const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => console.log(`Remix on ${port}`));

Your package.json scripts:

{
  "scripts": {
    "build": "remix vite:build",
    "start": "node server.js"
  }
}

Step 2: Add Prisma and a managed Postgres

Provision a managed PostgreSQL on PandaStack. Attaching it to the app injects DATABASE_URL, which is exactly what Prisma reads:

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Note {
  id        String   @id @default(cuid())
  title     String
  body      String
  createdAt DateTime @default(now())
}

Generate the client and run migrations as a release step:

npx prisma generate
npx prisma migrate deploy

Step 3: Use the database in loaders and actions

This is the part that makes Remix shine — data fetching lives next to the route.

// app/routes/notes._index.tsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { prisma } from '~/db.server';

export async function loader() {
  const notes = await prisma.note.findMany({ orderBy: { createdAt: 'desc' } });
  return json({ notes });
}

export async function action({ request }) {
  const form = await request.formData();
  await prisma.note.create({
    data: { title: String(form.get('title')), body: String(form.get('body')) },
  });
  return json({ ok: true });
}

Keep a single Prisma client instance to avoid exhausting connections — important since the free tier allows 50 DB connections:

// app/db.server.ts
import { PrismaClient } from '@prisma/client';
export const prisma = global.__db ?? (global.__db = new PrismaClient());

Step 4: Deploy

Connect your repo; PandaStack auto-detects Node and the build/start commands. On push it builds with rootless BuildKit in a K8s Job pod, ships the image to Artifact Registry, and Helm-deploys:

git push origin main

Live logs stream from self-hosted Elasticsearch, so you can watch the build and the running server. Custom domain plus automatic SSL come standard.

Step 5: Sessions and secrets

Remix sessions need a secret. Set it as an env var (keep the same key name across environments):

SESSION_SECRET=<random-32-bytes>
import { createCookieSessionStorage } from '@remix-run/node';
export const sessionStorage = createCookieSessionStorage({
  cookie: { secrets: [process.env.SESSION_SECRET!], sameSite: 'lax', httpOnly: true, secure: true },
});

Step 6: Migrations on every deploy

Don't run migrate deploy inside every replica's startup — run it once per release. A clean pattern is a separate release command in your build config, or a one-off cronjob/job that runs migrations before the new version takes traffic.

Performance notes

  • Cache static assets aggressively (build/client is fingerprinted).
  • Use Remix's headers export to set Cache-Control on loader responses where appropriate.
  • On the free tier, the app scales to zero on spot nodes; the first request after idle pays a cold start. Paid tiers stay warm.

References

  • [Remix — deployment docs](https://remix.run/docs/en/main/guides/deployment)
  • [Prisma — deploy migrations](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production)
  • [Remix on Express adapter](https://remix.run/docs/en/main/other-api/adapter)
  • [PostgreSQL connection limits](https://www.postgresql.org/docs/current/runtime-config-connection.html)

---

Remix plus a managed Postgres is full-stack with almost no glue code — loaders read, actions write, and DATABASE_URL is injected for you. Deploy it on PandaStack's [free tier](https://dashboard.pandastack.io) and ship a real database-backed app at $0/mo.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also