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 deployStep 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 mainLive 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/clientis fingerprinted). - Use Remix's
headersexport to setCache-Controlon 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.