Remix (now part of the React Router ecosystem) is built around web standards: loaders fetch data on the server, actions handle mutations, and the framework runs anywhere there's a request/response cycle. For production you typically want a long-running Node server, a managed database, and a migration strategy. Let's build that.
How Remix runs in production
A Remix app compiles into a server build and a client build. The server build is an HTTP handler you mount onto a Node server (commonly Express or the built-in remix-serve). The client build is static assets your server (or a CDN) serves.
The simplest production server is remix-serve:
remix-serve ./build/server/index.jsIt reads PORT from the environment and serves both the SSR handler and static assets. For most apps that's all you need; reach for a custom Express server only when you need extra middleware.
Containerizing Remix
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/build ./build
COPY --from=build /app/public ./public
COPY prisma ./prisma
EXPOSE 3000
CMD ["npm", "run", "start"]Keep the prisma/ directory in the image so migrations can run at deploy time.
A managed database with Prisma
Prisma pairs nicely with Remix because loaders and actions are plain server functions. Define your client once and reuse it (a frequent gotcha — creating a new client per request exhausts connections):
// app/db.server.ts
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
declare global { var __db__: PrismaClient | undefined; }
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.__db__) global.__db__ = new PrismaClient();
prisma = global.__db__;
}
export { prisma };The .server.ts suffix guarantees Remix never bundles this into the client. Use it in a loader:
export async function loader() {
const posts = await prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
return json({ posts });
}Migrations
Run migrations as a deploy step, not on boot:
npx prisma migrate deploymigrate deploy applies committed migrations without prompting — exactly what you want in CI/CD. Avoid migrate dev in production; it can generate new migrations interactively.
Connection pooling caveat
Prisma opens a pool per process. If you run multiple replicas, your total connection count is replicas * pool size. Managed databases cap connections (PandaStack's free tier allows 50). For serverless or high-replica deployments, put PgBouncer in front or use Prisma's connection pool settings via the connection_limit query param on DATABASE_URL.
Deploying on PandaStack
- 1Create a PostgreSQL database. PandaStack injects
DATABASE_URLinto your app automatically. - 2Connect your Git repo as a container app. Node and the Dockerfile are auto-detected; if you skip the Dockerfile, buildpacks handle
npm run buildandnpm run start. - 3Add
prisma migrate deployas a pre-start or release command. - 4Push — live build logs stream as it deploys, and you get automatic SSL on your custom domain.
Builds run in rootless BuildKit inside ephemeral Kubernetes Jobs and deploy via Helm. Rollbacks and deploy history are one click away.
| Step | Command |
|---|---|
| Install | npm ci |
| Build | npm run build |
| Migrate | npx prisma migrate deploy |
| Start | npm run start (remix-serve) |
Production checklist
- Reuse a single Prisma client — don't instantiate per request.
- Use
.server.tsfor anything touching secrets or the DB. - Run
migrate deploy, nevermigrate dev, in production. - Watch connection counts across replicas.
- Serve static assets with long cache headers — Remix fingerprints them, so they're safe to cache aggressively.
References
- Remix deployment docs: https://remix.run/docs/en/main/guides/deployment
- Prisma with Remix guide: https://www.prisma.io/docs/guides/remix
- Prisma migrate deploy: https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production
- Prisma connection management: https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections
- React Router (Remix) framework docs: https://reactrouter.com/start/framework/installation
---
PandaStack's free tier gives you a container app plus a managed PostgreSQL database with DATABASE_URL auto-injected — perfect for Remix loaders and actions. Push your repo and it runs: https://dashboard.pandastack.io