Remix is a full-stack React framework focused on web standards and progressive enhancement. It leverages server-side rendering, nested routing, and native browser APIs to deliver fast, resilient web applications. Deploying a Remix app to production typically involves configuring servers, build pipelines, and SSL certificates.
With Out Plane, you can deploy your Remix application in under a minute — directly from your GitHub repository. No infrastructure management, no complex configuration. This guide shows you exactly how.
What You'll Need
Before starting, make sure you have:
- Node.js 20+ installed on your machine
- npm (included with Node.js)
- A GitHub account
- A Remix application in a GitHub repository
Don't have Node.js installed? Download it from nodejs.org:
- Windows: Download the LTS installer from nodejs.org
- macOS: Use
brew install nodeor download from nodejs.org - Linux: Run
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt install -y nodejs(Ubuntu/Debian)
Once Node.js is installed, create a new Remix project:
npx create-remix@latest my-remix-app
cd my-remix-appIf you don't have a Remix app yet, use our example below.
Quick Start: Sample Remix Application
Here's a minimal Remix application you can use. Create a new project:
npx create-remix@latest my-remix-app
cd my-remix-appReplace app/routes/_index.tsx with a page that uses a loader:
import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const meta: MetaFunction = () => {
return [
{ title: "My Remix App" },
{ name: "description", content: "Deployed on Out Plane" },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
return json({
message: "Hello from Remix!",
timestamp: new Date().toISOString(),
});
}
export default function Index() {
const data = useLoaderData<typeof loader>();
return (
<main style={{ padding: "2rem", fontFamily: "system-ui" }}>
<h1>{data.message}</h1>
<p>Deployed on Out Plane</p>
<p>Server time: {data.timestamp}</p>
</main>
);
}Add a health check resource route at app/routes/api.health.tsx:
import { json } from "@remix-run/node";
export async function loader() {
return json({ status: "healthy" });
}Create a Dockerfile in the project root:
FROM node:20-alpine AS base
# Install dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 remixuser
COPY --from=builder --chown=remixuser:nodejs /app/build ./build
COPY --from=builder --chown=remixuser:nodejs /app/public ./public
COPY --from=builder --chown=remixuser:nodejs /app/package.json ./package.json
COPY --from=builder --chown=remixuser:nodejs /app/node_modules ./node_modules
USER remixuser
EXPOSE 3000
ENV PORT=3000
CMD ["npm", "run", "start"]Push this code to a GitHub repository, and you're ready to deploy.
Deploy in 3 Steps
Step 1: Connect Your Repository
- Go to console.outplane.com
- Sign in with your GitHub account
- Select your Remix repository from the list
Out Plane automatically detects your Remix application and configures the build process.
Step 2: Configure Your Application
Configure the following settings in the create application form:
Build Method
Select how Out Plane should build your application:
- Dockerfile (Recommended for Remix): Uses the multi-stage Dockerfile above for optimal image size and full SSR support.
- Buildpacks: Automatically detects Node.js and runs
npm run build. Works for simpler Remix applications.
For production Remix applications with server-side rendering, Dockerfile is the recommended approach.
Basic Settings
- Port: Set to
3000 - Branch: Select
mainor your preferred branch - Region: Choose the region closest to your users
Environment Variables (Optional)
If your application uses environment-specific configuration, add them here. Remix loads environment variables on the server by default. To expose variables to the browser, you must explicitly pass them through a loader:
DATABASE_URL=postgres://user:password@host/database
SESSION_SECRET=your-session-secret
API_KEY=your-api-keyAdd variables using the Add button or Raw Edit for bulk entry.
Step 3: Deploy
Click Deploy Application and watch the build process:
- Queued → Waiting for resources
- Building → Installing dependencies, running
remix build - Deploying → Starting your Remix application
- Ready → Your app is live
Once the status shows Ready, your application is live. You can find your application URL at the top of the deployment page. Click the URL to open your Remix app in a new tab. SSL is automatically configured.
Production Best Practices
Loader and Action Patterns
Remix uses loaders for data fetching and actions for mutations. Keep your loaders focused and return only the data the component needs:
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }: LoaderFunctionArgs) {
const product = await getProduct(params.id);
if (!product) {
throw new Response("Not Found", { status: 404 });
}
return json({ product });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name");
await updateProduct({ name });
return json({ success: true });
}Error Boundaries
Remix provides built-in error handling with ErrorBoundary exports. Add them to catch errors at any route level:
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>Something went wrong</h1>
<p>An unexpected error occurred.</p>
</div>
);
}Headers and Caching
Control HTTP headers per route using the headers export for caching and performance:
import type { HeadersFunction } from "@remix-run/node";
export const headers: HeadersFunction = () => ({
"Cache-Control": "public, max-age=300, s-maxage=3600",
});Session Management
Remix provides built-in session management using cookie-based or file-based storage. For production deployments, use cookie sessions with a secret from environment variables:
import { createCookieSessionStorage } from "@remix-run/node";
const { getSession, commitSession, destroySession } =
createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET!],
secure: true,
},
});
export { getSession, commitSession, destroySession };Environment Variables
Remix does not expose server environment variables to the browser automatically. To use environment variables in client-side code, pass them through a loader in your root route:
// app/root.tsx
export async function loader() {
return json({
ENV: {
PUBLIC_API_URL: process.env.PUBLIC_API_URL,
},
});
}This keeps sensitive variables like DATABASE_URL and SESSION_SECRET server-only.
Connecting a Database
Most Remix applications need a database. Out Plane provides managed PostgreSQL:
- Go to Databases in the sidebar
- Click Create Database
- Select PostgreSQL version and region
- Copy the connection URL
Add the connection URL as an environment variable:
DATABASE_URL=postgres://user:password@host/databaseUse it in your Remix application with Prisma:
npm install prisma @prisma/client
npx prisma init// app/lib/db.server.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Query the database in a loader:
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { prisma } from "~/lib/db.server";
export async function loader() {
const users = await prisma.user.findMany({ take: 50 });
return json({ users });
}
export default function Users() {
const { users } = useLoaderData<typeof loader>();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}The .server.ts suffix ensures this module is never included in the client bundle.
Custom Domain Setup
Replace the default .outplane.app URL with your own domain:
- Navigate to Domains
- Click Map Domain
- Enter your domain (e.g.,
app.yourdomain.com) - Add the DNS records shown to your domain registrar
SSL certificates are automatically provisioned once DNS propagates.
Monitoring Your Application
After deployment, monitor your Remix application:
- Logs: View real-time application logs including server-side rendering logs and loader errors
- Metrics: Track CPU, memory, and network usage
- HTTP Logs: Analyze incoming requests, response times, and status codes
Access these from the sidebar in your application dashboard.
Troubleshooting
Build Failures
Check your dependencies and build script. Verify that package.json includes all required packages and that npm run build completes successfully locally before deploying:
npm run buildIf the build fails with missing dependencies, ensure they are listed under dependencies rather than devDependencies for any modules needed at runtime.
Hydration Mismatches
Avoid rendering server-only data directly in components. Hydration errors occur when the server-rendered HTML does not match the client-side render. Common causes include using Date.now() or Math.random() in components. Use loaders to fetch dynamic data on the server and pass it to the component:
// Bad - causes hydration mismatch
export default function Page() {
return <p>{new Date().toISOString()}</p>;
}
// Good - data comes from loader
export async function loader() {
return json({ time: new Date().toISOString() });
}
export default function Page() {
const { time } = useLoaderData<typeof loader>();
return <p>{time}</p>;
}Loader Errors
Check your application logs. Navigate to Logs in the sidebar to see error messages and stack traces from loaders and actions. Ensure your loaders handle missing data with proper HTTP responses:
export async function loader({ params }: LoaderFunctionArgs) {
const item = await getItem(params.id);
if (!item) {
throw new Response("Not Found", { status: 404 });
}
return json({ item });
}Session Storage Issues
Verify your SESSION_SECRET environment variable. If sessions are not persisting, ensure SESSION_SECRET is set in your environment variables. Without it, cookie sessions cannot be signed and will fail silently.
Route Conflicts
Check your route file naming. Remix uses file-based routing with specific conventions. Conflicting route names cause unexpected behavior. Common issues:
routes/_index.tsxis the index route for the parent layoutroutes/about.tsxandroutes/about._index.tsxcan conflict- Dynamic segments use
$paramsyntax (e.g.,routes/users.$id.tsx)
Review the Remix routing documentation for naming conventions.
Next Steps
Your Remix application is now deployed and running in production. Here's what to explore next:
- Scale your application: Adjust instance types and auto-scaling settings for higher traffic
- Set up CI/CD: Enable automatic deployments on every git push
- Add monitoring: Integrate with external monitoring tools via Out Plane's metrics export
- Configure caching: Add Redis for session storage and API response caching
Summary
Deploying a Remix application to Out Plane takes three steps:
- Connect your GitHub repository
- Configure port (3000), environment variables, and build method
- Deploy and get your live URL with automatic HTTPS
No server configuration, no manual SSL setup, no infrastructure management. Full support for server-side rendering, loaders, actions, and nested routing.
Ready to deploy your Remix application? Get started with Out Plane and receive $20 in free credit.