8. Deployment
You have built a full-featured bookstore with routing, database queries, forms, authentication, and file uploads. Now it is time to put it on the internet. In this chapter, you will learn how to prepare your application for production and deploy it to a server.
Building for Production
During development, tsx compiles TypeScript on the fly every time a file changes. This is convenient but adds overhead. For production, you compile your TypeScript once ahead of time.
Add a build script to package.json:
{
"scripts": {
"dev": "tsx watch server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}Update tsconfig.json to emit compiled JavaScript:
{
"compilerOptions": {
"strict": true,
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ES2022",
"moduleResolution": "Bundler",
"target": "ES2022",
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"sourceMap": true
},
"include": ["app/**/*", "server.ts"]
}Now run the build:
npm run buildThis compiles all your .ts files into .js files in the dist/ directory. The production server runs these pre-compiled files directly with Node.js -- no TypeScript compilation at runtime.
npm run startWhy compile ahead of time?
Running TypeScript directly (with tsx) adds startup time and memory overhead. Pre-compiled JavaScript starts faster and uses less memory. For a production server handling thousands of requests, this difference matters.
Environment Variables
Your application has secrets that should never appear in source code -- database passwords, session secrets, API keys, and more. Environment variables are the standard way to provide these values at runtime without hardcoding them.
Create a .env file in your project root for development:
SESSION_SECRET=a-very-long-random-string-change-this-in-production
DATABASE_URL=./bookstore.db
NODE_ENV=developmentNever commit .env files
Add .env to your .gitignore file. Each environment (development, staging, production) has its own values. Committing secrets to version control is one of the most common security mistakes.
Reference these variables in your code using process.env:
import { createCookie } from 'remix/cookie'
let sessionCookie = createCookie('__session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
secrets: [process.env.SESSION_SECRET!],
maxAge: 60 * 60 * 24 * 7,
})Notice that secure is now conditional -- it is true in production (where you have HTTPS) and false in development (where you typically use plain HTTP on localhost).
On your production server, set environment variables using your hosting provider's dashboard, a .env file on the server, or your deployment tool's configuration.
Database Considerations
SQLite for Simple Deployments
SQLite stores your entire database in a single file. This is perfect for small to medium applications running on a single server:
DATABASE_URL=./data/bookstore.dbMake sure the database file is stored outside the dist/ directory so it persists across deployments.
PostgreSQL for Production
For applications that need to handle more traffic or run on multiple servers, use PostgreSQL:
import { createPostgresConnection } from 'remix/data-table-postgres'
let db = createPostgresConnection({
connectionString: process.env.DATABASE_URL,
})PostgreSQL runs as a separate server process, so multiple application instances can connect to the same database. Most cloud hosting providers offer managed PostgreSQL databases.
Database Migrations
Before your application can start, the database schema must be up to date. Run migrations as part of your deployment process:
# Run pending migrations
node dist/migrate.jsCreate a separate migrate.ts script that applies your schema changes. This should run before starting the application server.
Session Storage for Production
The filesystem session storage from the tutorial (createFsSessionStorage) stores sessions as files on disk. This has two problems in production:
- Multiple servers -- If you run more than one instance, each has its own session files. A user might log in on server A, then their next request goes to server B, which does not have their session.
- Ephemeral filesystems -- Some hosting platforms (like containers or serverless) wipe the filesystem between deployments.
Use Redis or Memcache for production session storage:
import { createRedisSessionStorage } from 'remix/session-storage-redis'
let sessionStorage = createRedisSessionStorage({
url: process.env.REDIS_URL!,
})Redis is an in-memory data store that is fast, supports expiration (sessions automatically clean up), and is accessible from multiple application instances.
Deploying to a VPS with Node.js
A VPS (Virtual Private Server) is a virtual machine in the cloud where you have full control. Providers like DigitalOcean, Linode, Hetzner, and Vultr offer VPS instances starting at a few dollars per month.
Here is the basic deployment process:
- Set up the server -- Install Node.js 24+ on the VPS.
- Copy your code -- Use
git cloneorrsyncto get your project on the server. - Install dependencies -- Run
npm install --production(skips dev dependencies). - Build -- Run
npm run build. - Set environment variables -- Configure your secrets.
- Run migrations -- Update the database schema.
- Start the server -- Run
npm run start.
Using a Process Manager
In production, use a process manager like PM2 to keep your application running, restart it if it crashes, and manage logs:
# Install PM2 globally
npm install -g pm2
# Start your application
pm2 start dist/server.js --name bookstore
# View logs
pm2 logs bookstore
# Restart after a deployment
pm2 restart bookstorePM2 can also run multiple instances of your application to take advantage of multi-core CPUs:
pm2 start dist/server.js --name bookstore -i maxRuntime Portability
Because Remix is built on web-standard APIs (Fetch API, Web Streams, Web Crypto), your application can run on runtimes other than Node.js:
- Bun -- A faster alternative to Node.js. Just replace
nodewithbun:bashbun run dist/server.js - Deno -- A secure runtime with built-in TypeScript support:bash
deno run --allow-net --allow-read --allow-env dist/server.js - Cloudflare Workers -- Run your application at the edge, close to your users. This requires using the
fetchhandler format instead ofnode:http, and some Node.js-specific features (like filesystem storage) will not be available.
Your core application logic -- routes, handlers, middleware, validation -- works across all of these runtimes without changes. Only the server entry point and platform-specific storage backends need to be adapted.
Dockerfile
If you deploy with containers (Docker, Kubernetes, Fly.io, Railway), here is a Dockerfile for your bookstore:
FROM node:24-slim AS build
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Production image
FROM node:24-slim
WORKDIR /app
# Install production dependencies only
COPY package.json package-lock.json ./
RUN npm ci --production
# Copy compiled application
COPY --from=build /app/dist ./dist
# Create directories for data
RUN mkdir -p data sessions uploads
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/server.js"]This uses a multi-stage build:
- The
buildstage installs all dependencies (including dev dependencies) and compiles TypeScript. - The production stage only includes production dependencies and the compiled output, resulting in a smaller image.
Use .dockerignore
Create a .dockerignore file to exclude node_modules, .env, and other files that should not be in the container image:
node_modules
.env
.git
sessions
uploadsProduction Checklist
Before going live, walk through this checklist:
Security
- [ ] Set
NODE_ENV=production-- This enables production optimizations and disables development-only features. - [ ] Use strong session secrets -- Generate a random string of at least 32 characters. Use
node -e "console.log(crypto.randomUUID() + crypto.randomUUID())"to generate one. - [ ] Set
secure: trueon cookies -- Ensures cookies are only sent over HTTPS. - [ ] Set up HTTPS -- Use a reverse proxy like Nginx or Caddy in front of your Node.js server to handle TLS/SSL. Let's Encrypt provides free certificates.
- [ ] Never commit secrets -- Ensure
.envfiles and credentials are in.gitignore.
Performance
- [ ] Enable compression -- Add the compression middleware to reduce response sizes:ts
import { compression } from 'remix/compression-middleware' let router = createRouter({ middleware: [compression(), /* ... other middleware */], }) - [ ] Configure static file caching -- Set long
Cache-Controlheaders for uploaded files and static assets. - [ ] Use a reverse proxy -- Nginx or Caddy can serve static files, handle SSL termination, and load-balance across multiple application instances.
Reliability
- [ ] Use a process manager -- PM2 or systemd to restart your application if it crashes.
- [ ] Set up logging -- Make sure errors are logged somewhere you can find them.
- [ ] Run database migrations -- Include migration runs in your deployment script.
- [ ] Back up your database -- Set up automated backups, especially for SQLite (which is a single file).
Monitoring
- [ ] Health check endpoint -- Add a simple route that returns 200 OK so load balancers and uptime monitors can check if your application is running:ts
router.get('/health', () => new Response('OK'))
Congratulations!
You have completed the Remix V3 tutorial. Starting from an empty directory, you built a complete web application with:
- Routing that maps URLs to handler functions
- Pages rendered with HTML templates and JSX components
- A database for storing and querying books
- Forms for creating and editing data with server-side validation
- Authentication with sessions, login, registration, and protected routes
- File uploads for book cover images with storage and serving
- A production deployment strategy with environment variables, process management, and containers
This is a real, working web application built on web standards -- the same foundation that powers the web itself.
Where to Go Next
The tutorial covered the essentials, but Remix V3 has much more to offer. Here are some paths to explore:
Concepts
Deepen your understanding of how Remix works:
- Routing -- Advanced patterns like nested routes, catch-all parameters, and route specificity.
- Middleware -- How to write your own middleware for cross-cutting concerns.
- Streaming -- Send HTML progressively for faster perceived load times.
- Error Handling -- Graceful error boundaries and custom error pages.
Guides
Learn how to build specific features:
- Authentication -- OAuth with Google, GitHub, and other providers.
- Database -- Advanced queries, joins, transactions, and migrations.
- Testing -- Write tests for your routes and middleware.
- Styling -- CSS strategies for Remix applications.
- Security -- CSRF protection, Content Security Policy, and more.
Packages
Explore the full set of Remix packages:
- All Packages -- Browse the complete list of 40 composable packages.
Each package is designed to work independently, so you can use Remix's router without its database, its session management without its auth, or any other combination that fits your needs.
Happy building!