Skip to content

Deploying a Remix App

Remix is a modern React framework that emphasizes fast, dynamic, and resilient web applications built on web standards. It features nested routing, powerful data loading APIs, progressive enhancement, form handling, error boundaries, and a strong focus on performance. Remix seamlessly handles both client and server rendering, making it ideal for building production-grade applications with a delightful developer experience. Whether you’re building APIs, content sites, or interactive applications, Remix provides the flexibility and tooling to succeed.

This comprehensive guide walks you through deploying a Remix application to Klutch.sh, covering both automatic Nixpacks-based deployments and Docker-based deployments. You’ll learn installation steps, explore sample code, configure environment variables, set up data handling, and discover best practices for production deployments.

Table of Contents

  • Prerequisites
  • Getting Started: Create a Remix App
  • Sample Code Examples
  • Project Structure
  • Deploying Without a Dockerfile (Nixpacks)
  • Deploying With a Dockerfile
  • Environment Variables & Configuration
  • Routing & Data Loading
  • Form Handling & Actions
  • Troubleshooting
  • Resources

Prerequisites

To deploy a Remix application on Klutch.sh, ensure you have:

  • Node.js 18 or higher - Remix requires a modern Node.js version
  • npm or yarn - For managing dependencies
  • Git - For version control
  • GitHub account - Klutch.sh integrates with GitHub for continuous deployments
  • Klutch.sh account - Sign up for free

Getting Started: Create a Remix App

Follow these steps to create and set up a new Remix application:

  1. Create a new Remix app using the official setup command:
    Terminal window
    npx create-remix@latest my-remix-app
    cd my-remix-app
    npm install

    The CLI will prompt you to choose your package manager, TypeScript or JavaScript, and deployment platform. For Klutch.sh, you can select Node.js as the deployment target.

  2. Start the development server:
    Terminal window
    npm run dev

    Your Remix app will be available at http://localhost:3000. The development server includes hot module reloading for instant feedback.

  3. Explore the generated project structure. Remix creates a `routes` directory with example routes and a basic layout structure. You can immediately start building pages by creating files in the `routes` directory.
  4. Build your application for production:
    Terminal window
    npm run build

    This creates an optimized build in the build/ directory ready for deployment.

  5. To stop the development server, press `Ctrl+C`. Verify your app builds successfully before deploying to Klutch.sh.

Sample Code Examples

Basic Route with Data Loading

Here’s an example of a Remix route that loads data from an API:

app/routes/posts.tsx
import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
export async function loader() {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
throw new Error('Failed to load posts')
}
const posts = await response.json()
return json({ posts })
}
export default function PostsRoute() {
const { posts } = useLoaderData<typeof loader>()
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</li>
))}
</ul>
</div>
)
}

Form Handling with Action

app/routes/contact.tsx
import { Form, useActionData } from '@remix-run/react'
import { json, redirect } from '@remix-run/node'
import type { ActionFunction } from '@remix-run/node'
export const action: ActionFunction = async ({ request }) => {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 })
}
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
const message = formData.get('message')
// Validate input
if (!name || !email || !message) {
return json(
{ error: 'All fields are required' },
{ status: 400 }
)
}
// Process form (send email, save to database, etc.)
try {
await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message })
})
return redirect('/contact/success')
} catch (error) {
return json(
{ error: 'Failed to send message' },
{ status: 500 }
)
}
}
export default function ContactRoute() {
const actionData = useActionData<typeof action>()
return (
<div>
<h1>Contact Us</h1>
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<Form method="post">
<input
type="text"
name="name"
placeholder="Your Name"
required
/>
<input
type="email"
name="email"
placeholder="Your Email"
required
/>
<textarea
name="message"
placeholder="Your Message"
required
/>
<button type="submit">Send Message</button>
</Form>
</div>
)
}

Nested Routes and Layouts

app/routes/blog/_layout.tsx
import { Outlet } from '@remix-run/react'
export default function BlogLayout() {
return (
<div className="blog-layout">
<nav className="blog-nav">
<a href="/blog">All Posts</a>
<a href="/blog/recent">Recent</a>
<a href="/blog/popular">Popular</a>
</nav>
<main className="blog-content">
<Outlet />
</main>
</div>
)
}
// app/routes/blog/index.tsx
export default function BlogIndex() {
return <h1>Welcome to the Blog</h1>
}
// app/routes/blog/$slug.tsx
import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
export async function loader({ params }: LoaderArgs) {
const { slug } = params
const post = await fetchPost(slug)
if (!post) {
throw new Response('Post not found', { status: 404 })
}
return json({ post })
}
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>()
return (
<>
<h1>{post.title}</h1>
<p>{post.content}</p>
</>
)
}

Project Structure

A typical Remix project has this structure:

my-remix-app/
├── app/
│ ├── routes/
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ ├── about.tsx
│ │ ├── posts.tsx
│ │ ├── posts/
│ │ │ └── $id.tsx
│ │ └── api/
│ │ └── data.ts
│ ├── styles/
│ │ └── globals.css
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ └── root.tsx
├── build/
│ ├── index.js
│ └── assets/
├── public/
│ └── favicon.ico
├── .env
├── .env.example
├── remix.config.js
├── package.json
└── tsconfig.json

Deploying Without a Dockerfile

Klutch.sh uses Nixpacks to automatically detect and build your Remix application. This is the simplest deployment option that requires no additional configuration files.

  1. Test your Remix app locally to ensure it builds and runs correctly:
    Terminal window
    npm run build
    npm run start
  2. Push your Remix application to a GitHub repository with all source code, configuration files, and lock files included.
  3. Log in to your Klutch.sh dashboard.
  4. Create a new project and give it a name (e.g., "My Remix App").
  5. Create a new app with the following configuration:
    • Repository - Select your Remix GitHub repository and the branch to deploy
    • Traffic Type - Select HTTP (for web applications serving HTTP traffic)
    • Internal Port - Set to 3000 (the default port for Remix applications)
    • Region - Choose your preferred region for deployment
    • Compute - Select appropriate compute resources for your application
    • Instances - Start with 1 instance for testing, scale as needed
    • Environment Variables - Add any required environment variables (API keys, database URLs, etc.)

    If you need to customize the build or start commands, set these Nixpacks environment variables:

    • BUILD_COMMAND: Override the default build command (e.g., npm run build)
    • START_COMMAND: Override the default start command (e.g., npm run start)
  6. Click "Create" to deploy. Klutch.sh will automatically detect your Node.js/Remix project, install dependencies, build your application, and start it.
  7. Once deployed, your app will be available at a URL like `example-app.klutch.sh`. Test it by visiting the URL in your browser.

Deploying With a Dockerfile

If you prefer more control over the build and runtime environment, you can use a Dockerfile. Klutch.sh will automatically detect and use any Dockerfile in your repository’s root directory.

  1. Create a `Dockerfile` in your project root:
    # Multi-stage build for optimized Remix deployment
    FROM node:18-bullseye-slim AS builder
    WORKDIR /app
    # Copy package files
    COPY package*.json ./
    COPY package-lock.json* ./
    # Install dependencies
    RUN npm ci
    # Copy source code
    COPY . .
    # Build Remix application
    RUN npm run build
    # Production stage
    FROM node:18-bullseye-slim
    WORKDIR /app
    # Copy package files
    COPY package*.json ./
    COPY package-lock.json* ./
    # Install production dependencies only
    RUN npm ci --production
    # Copy built application from builder
    COPY --from=builder /app/build ./build
    COPY --from=builder /app/public ./public
    # Set environment variables
    ENV NODE_ENV=production
    # Expose port
    EXPOSE 3000
    # Health check
    HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD node -e "require('http').get('http://localhost:3000', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
    # Start the application
    CMD ["npm", "start"]
  2. Create a `.dockerignore` file to exclude unnecessary files from the Docker build:
    node_modules
    npm-debug.log
    .git
    .gitignore
    README.md
    .env
    .env.local
    .vscode
    .idea
    .DS_Store
    build
    dist
  3. Push your code (with Dockerfile and .dockerignore) to GitHub.
  4. Follow the same deployment steps as the Nixpacks method:
    • Log in to Klutch.sh
    • Create a new project
    • Create a new app pointing to your GitHub repository
    • Set the traffic type to HTTP and internal port to 3000
    • Add any required environment variables
    • Click “Create”

    Klutch.sh will automatically detect your Dockerfile and use it to build and deploy your application.

  5. Your deployed app will be available at `example-app.klutch.sh` once the build and deployment complete.

Environment Variables & Configuration

Remix applications use environment variables for configuration. Set these in the Klutch.sh dashboard during app creation or update them afterward.

Common Environment Variables

# Server configuration
PORT=3000
NODE_ENV=production
# API configuration
API_BASE_URL=https://api.example.com
PUBLIC_API_URL=https://api.example.com
# Database (if using a backend database)
DATABASE_URL=postgresql://user:password@host:5432/remix_prod
# Authentication
SESSION_SECRET=your_secret_session_key_here
AUTH_TOKEN=your_auth_token
# Third-party services
STRIPE_PUBLIC_KEY=pk_live_xxxxx
STRIPE_SECRET_KEY=sk_live_xxxxx
SENDGRID_API_KEY=SG.xxxxx
# Environment-specific settings
LOG_LEVEL=info
ENABLE_DEBUG=false

Using Environment Variables in Remix

app/utils/env.server.ts
export const getApiUrl = () => {
return process.env.API_BASE_URL || 'https://api.example.com'
}
export const getSessionSecret = () => {
const secret = process.env.SESSION_SECRET
if (!secret) {
throw new Error('SESSION_SECRET environment variable is not set')
}
return secret
}
export const isDevelopment = () => {
return process.env.NODE_ENV === 'development'
}
export const isProduction = () => {
return process.env.NODE_ENV === 'production'
}
app/routes/api/data.ts
import { json } from '@remix-run/node'
export async function loader() {
const apiUrl = process.env.API_BASE_URL || 'https://api.example.com'
const response = await fetch(`${apiUrl}/data`)
const data = await response.json()
return json({ data })
}

Routing & Data Loading

Remix’s file-based routing system makes it easy to organize your application:

Creating Routes

// app/routes/index.tsx - Home page route
export default function Home() {
return <h1>Welcome Home</h1>
}
// app/routes/about.tsx - About page route
export default function About() {
return <h1>About Us</h1>
}
// app/routes/users/$id.tsx - Dynamic route with parameter
import { useLoaderData } from '@remix-run/react'
import type { LoaderArgs } from '@remix-run/node'
export async function loader({ params }: LoaderArgs) {
const { id } = params
const user = await fetchUser(id)
return { user }
}
export default function UserDetail() {
const { user } = useLoaderData<typeof loader>()
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
)
}

Data Loading Patterns

// Fetch data at build time and cache
export async function loader({ params }: LoaderArgs) {
const post = await db.posts.findUnique(params.id)
return json(
{ post },
{
headers: {
'Cache-Control': 'public, max-age=3600'
}
}
)
}
// Handle errors gracefully
export function ErrorBoundary() {
const error = useRouteError()
return (
<div>
<h1>Oops!</h1>
<p>{error.message}</p>
</div>
)
}

Form Handling & Actions

Remix makes form handling simple and progressive:

app/routes/posts/new.tsx
import { Form, useActionData } from '@remix-run/react'
import { redirect, json } from '@remix-run/node'
import type { ActionFunction } from '@remix-run/node'
export const action: ActionFunction = async ({ request }) => {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 })
}
const formData = await request.formData()
const title = formData.get('title')
const content = formData.get('content')
if (!title || !content) {
return json(
{ error: 'Title and content are required' },
{ status: 400 }
)
}
// Create post in database
const newPost = await db.posts.create({
data: { title, content }
})
return redirect(`/posts/${newPost.id}`)
}
export default function NewPost() {
const actionData = useActionData<typeof action>()
return (
<div>
<h1>Create New Post</h1>
{actionData?.error && (
<div className="error">{actionData.error}</div>
)}
<Form method="post">
<input
type="text"
name="title"
placeholder="Post Title"
required
/>
<textarea
name="content"
placeholder="Post Content"
required
/>
<button type="submit">Create Post</button>
</Form>
</div>
)
}

Troubleshooting

Build Failures

Problem - Deployment fails during the build phase

Solution:

  • Verify your Remix app builds locally: npm run build
  • Ensure all dependencies are properly listed in package.json
  • Check for TypeScript errors: npm run typecheck (if configured)
  • Verify that all routes are correctly exported as default components
  • Check build logs in the Klutch.sh dashboard for specific errors
  • Ensure the remix.config.js file is correctly configured

Application Won’t Start

Problem - App shows as unhealthy after deployment

Solution:

  • Verify the app starts locally: npm run start
  • Check that port 3000 is configured correctly in Klutch.sh
  • Verify all required environment variables are set
  • Check application logs in the Klutch.sh dashboard
  • Ensure the built application includes both build/ and public/ directories
  • Verify that package.json has a valid start script

Memory Issues

Problem - App crashes with out of memory errors

Solution:

  • Upgrade to a larger compute tier in Klutch.sh
  • Optimize data loading (use pagination for large datasets)
  • Implement caching strategies for expensive operations
  • Monitor memory usage during development
  • Consider splitting large routes into separate loader functions
  • Use streaming for large responses

Slow Builds

Problem - Build takes longer than expected

Solution:

  • Use the multi-stage Dockerfile to reduce final image size
  • Cache dependencies properly in Docker
  • Optimize imports and tree-shake unused code
  • Check for large dependencies that can be replaced
  • Use npm ci instead of npm install for faster, reproducible builds
  • Consider using a faster Node.js base image variant

Production Performance

Problem - App runs slowly in production

Solution:

  • Enable compression for HTTP responses
  • Use CDN for static assets
  • Implement proper caching headers
  • Optimize database queries
  • Profile performance with browser DevTools
  • Use Remix’s built-in optimization features
  • Consider enabling HTTP/2 server push for critical resources

Resources


Summary

Deploying a Remix application on Klutch.sh is straightforward whether you choose Nixpacks or Docker. Remix’s modern architecture, data loading APIs, progressive enhancement, and form handling make it ideal for building dynamic, performant web applications. Both deployment methods provide reliable, scalable hosting for your Remix apps. Start with Nixpacks for simplicity, or use Docker for complete control over your build environment. With Remix’s powerful development experience and Klutch.sh’s scalable infrastructure, you can deploy production-ready applications that serve users reliably at scale.