deployment

How to Deploy Your Next.js App to Production (The Complete Guide)

The gap between npm run dev and a production deploy is where most AI-built apps die. Here's the step-by-step guide to actually getting your Next.js app live and reliable.

FinishKit Team14 min read

Your app works perfectly on localhost. Beautiful UI. Smooth flows. Data pulling from Supabase. Auth working. You open a beer, connect your repo to Vercel, hit deploy, and...

Build failed. 37 TypeScript errors. Three missing environment variables. A hydration mismatch on every page. Your localhost:3000 URLs are hardcoded in two API routes. And your database migration? What migration?

Welcome to the deployment gap. The graveyard where AI-built apps go to die.

This guide is the step-by-step path out of that graveyard. By the end, your Next.js app will be live, monitored, and running on real infrastructure. Not "deploy and pray." Deploy and know.

Why Deployment Is Where AI-Built Apps Die

AI coding tools are optimized for one thing: making your app look and feel right inside npm run dev. Hot module replacement. Lenient TypeScript checking. Environment variables loaded from a local .env file. No build step. No production constraints.

Production is a completely different environment. And AI tools consistently fail to prepare you for it.

Here are the deployment killers that show up in almost every AI-built project:

  • TypeScript errors hidden by ignoreBuildErrors. Cursor, Bolt, and similar tools frequently add ignoreBuildErrors: true to your Next.js config to keep the dev server running. This means your app compiles in dev but explodes during a production build.
  • Missing environment variables. Your .env.local has twelve variables. Your Vercel project has three. The other nine silently return undefined in production, causing blank pages and broken API calls.
  • Hardcoded localhost URLs. fetch('http://localhost:3000/api/...') works great in dev. In production, it tries to call localhost on the server, which is either nothing or a completely different service.
  • Client/server boundary confusion. AI-generated code regularly imports server-only modules (like fs or database clients) into client components. Next.js dev mode is forgiving about this. The production bundler is not.

The stat bears repeating: over 90% of AI-assisted projects never make it to production. Deployment failures are the single biggest reason why.

The fix isn't complicated. It's methodical. Let's walk through it.

The Pre-Deploy Checklist

Before you touch a deployment platform, fix these three things locally. Deploying broken code faster doesn't help.

Fix Your Build

This is step one because it catches the most issues in the least time.

Open your next.config.mjs (or next.config.ts). If you see this, remove it immediately:

// next.config.mjs
 
// BEFORE: The silent killer
const nextConfig = {
  typescript: {
    ignoreBuildErrors: true, // DELETE THIS LINE
  },
  eslint: {
    ignoreDuringBuilds: true, // DELETE THIS TOO
  },
};
 
export default nextConfig;
// next.config.mjs
 
// AFTER: Let the build tell you the truth
const nextConfig = {};
 
export default nextConfig;

If your AI tool added ignoreBuildErrors: true, your app almost certainly has TypeScript errors that will cause runtime failures in production. Removing this flag is non-negotiable. Fix every error it surfaces before deploying.

Now run the production build locally:

npm run build

You'll likely see errors. Here are the ones AI-generated code leaves behind most often:

Implicit any types. AI frequently writes function parameters without types, which strict mode flags.

// Error: Parameter 'data' implicitly has an 'any' type
async function handleSubmit(data) { ... }
 
// Fix: Add the type
async function handleSubmit(data: ProjectFormData) { ... }

Unused imports and variables. AI generates code in multiple passes and doesn't clean up after itself.

// Error: 'useState' is declared but its value is never read
import { useState, useEffect, useCallback } from "react";
 
// Fix: Remove what you don't use
import { useEffect } from "react";

Missing return types on API routes. Next.js App Router expects specific return types from route handlers.

// Error: Type 'void' is not assignable to type 'Response'
export async function GET(request: Request) {
  const data = await fetchData();
  // AI forgot to return a Response
}
 
// Fix: Always return a Response
export async function GET(request: Request) {
  const data = await fetchData();
  return Response.json(data);
}

Run npm run build after each batch of fixes. Keep going until you see a clean build with zero errors.

Environment Variables

Environment variable misconfiguration is the number one cause of "works locally, blank page in production." Here's how to make it bulletproof.

Step 1: Create a definitive inventory. Search your entire codebase for every process.env reference:

grep -rn "process.env" --include="*.ts" --include="*.tsx" app/ lib/ | sort -u

Step 2: Validate at startup. Don't let your app boot with missing config. Use Zod to validate environment variables when the server starts:

// lib/env.ts
import { z } from "zod";
 
const envSchema = z.object({
  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
  SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
  OPENAI_API_KEY: z.string().startsWith("sk-"),
  NEXT_PUBLIC_APP_URL: z.string().url(),
});
 
export const env = envSchema.parse(process.env);

If any variable is missing or malformed, this throws immediately at startup instead of silently returning undefined at runtime. You'll see the exact problem in your deployment logs instead of debugging a blank page.

The NEXT_PUBLIC_ prefix is not just a naming convention. It controls whether the variable is bundled into client-side JavaScript. Any variable without this prefix is server-only and will be undefined in client components. Never put secrets in NEXT_PUBLIC_ variables, and always use NEXT_PUBLIC_ for values your client code needs (like your Supabase URL and anon key).

Step 3: Replace hardcoded URLs. Search for localhost in your codebase and replace every instance with an environment variable:

// Before: Hardcoded localhost
const res = await fetch("http://localhost:3000/api/projects");
 
// After: Dynamic base URL
const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/projects`);

Database Migrations

If you built with Supabase and created your tables through the dashboard UI, you have a problem. Your production database schema exists only as live state. There are no migration files. This means:

  • You can't reproduce the database on a new project
  • You can't set up a staging environment
  • You can't roll back schema changes
  • Any team member who joins has to manually recreate tables

Before deploying, export your schema as migration files. If you're using the Supabase CLI:

# Pull the current schema from your remote database
supabase db pull
 
# This creates a migration file in supabase/migrations/
# Commit this file to your repo

If you don't have the Supabase CLI set up yet, at minimum document your table structures in SQL so you can recreate them. But get proper migrations set up before your second deploy. You'll need them when something breaks and you need to know what changed.

Deploying to Vercel (Step by Step)

Vercel is the default deployment target for Next.js, and for good reason. It's built by the same team, handles edge cases automatically, and the free tier covers most early-stage apps. Here's the step-by-step process.

1. Connect your repository.

Push your code to GitHub (or GitLab/Bitbucket). Go to vercel.com, sign in, and click "Add New Project." Select your repo. Vercel detects the Next.js framework automatically.

2. Configure environment variables.

This is where most people rush and pay for it later. Go to the "Environment Variables" section before deploying. Add every variable from your inventory. Use the dropdown to control which environments each variable applies to (Production, Preview, Development).

# These are examples. Use your actual values.
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...
NEXT_PUBLIC_APP_URL=https://yourdomain.com
OPENAI_API_KEY=sk-...

3. Verify build settings.

Vercel should auto-detect these, but confirm:

  • Framework Preset: Next.js
  • Build Command: npm run build (or next build)
  • Output Directory: .next
  • Install Command: npm install

4. Deploy.

Click "Deploy." Watch the build logs. If it fails, the error is almost always one of the issues from the pre-deploy checklist above. Fix it locally, push, and Vercel will auto-redeploy.

5. Set up your custom domain.

In your project settings, go to "Domains." Add your domain. Vercel handles SSL certificates automatically. Update your DNS records as instructed (usually a CNAME record pointing to cname.vercel-dns.com).

6. Verify the deployment.

Don't just check the homepage. Walk through your entire critical path:

  • Sign up / sign in
  • Create the core resource (project, post, item, whatever your app does)
  • Verify data persists after a page refresh
  • Test on mobile
  • Check the browser console for errors

Common Vercel gotchas with AI-built apps:

  • Serverless function timeout. Vercel's free tier has a 10-second function timeout. If your AI tool generated API routes that do heavy processing (LLM calls, large database queries), they'll time out. Either optimize or upgrade to Pro (60-second timeout).
  • Cold starts. Serverless functions spin down after inactivity. The first request after a cold start is slower. This isn't a bug.
  • File system access. You cannot write to the file system in Vercel serverless functions. If your AI generated code that writes temp files, you'll need to switch to blob storage or an in-memory approach.
  • Edge vs. Node runtime. If your route uses Node.js-specific APIs (like crypto or Buffer), make sure it's not accidentally configured to run on the Edge runtime.

Deploying to Other Platforms

Vercel isn't always the right choice. Here's when to consider alternatives and how to deploy to each.

Railway

Best for: Apps that need persistent processes, background workers, or WebSocket connections that Vercel's serverless model doesn't support well.

Railway deploys from your repo and auto-detects Next.js. The main difference is that your app runs as a long-lived process instead of serverless functions.

# Install the Railway CLI
npm install -g @railway/cli
 
# Login and initialize
railway login
railway init
 
# Deploy
railway up

Set environment variables in the Railway dashboard or via CLI:

railway variables set NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co

Railway charges based on usage (CPU, memory, network). For small apps, it's comparable to Vercel's free tier.

Fly.io

Best for: When you need full control over your infrastructure, multi-region deployment, or persistent machines for background processing.

Fly.io runs your app in lightweight VMs (Machines) instead of containers or serverless functions. This gives you SSH access, persistent volumes, and fine-grained scaling control.

# Install flyctl
curl -L https://fly.io/install.sh | sh
 
# Launch your app (creates fly.toml)
fly launch
 
# Deploy
fly deploy

You'll need a Dockerfile for Fly.io. See the Docker section below for a production-ready Next.js Dockerfile.

Set secrets via the CLI:

fly secrets set SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...
fly secrets set OPENAI_API_KEY=sk-...

Platform comparison at a glance. Vercel is the path of least resistance for Next.js apps that fit the serverless model. Railway is simpler than Fly.io for persistent processes. Fly.io gives you the most control but requires more configuration. All three support custom domains, environment variables, and auto-deploys from Git.

Docker

Best for: Platform-agnostic deployment, self-hosting, or any platform that supports containers (AWS ECS, Google Cloud Run, DigitalOcean App Platform, Coolify, etc.).

Here's a production-ready Dockerfile for Next.js using the standalone output mode:

FROM node:20-alpine AS base
 
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
 
# Build the application
FROM base AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
 
# Set build-time env vars if needed
# ARG NEXT_PUBLIC_SUPABASE_URL
# ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
 
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 nextjs
 
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
USER nextjs
 
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
 
CMD ["node", "server.js"]

For this to work, add standalone output to your Next.js config:

// next.config.mjs
const nextConfig = {
  output: "standalone",
};
 
export default nextConfig;

Build and run locally to verify:

docker build -t my-nextjs-app .
docker run -p 3000:3000 --env-file .env.local my-nextjs-app

Post-Deploy Essentials

Your app is live. Now make sure it stays live.

Error Monitoring with Sentry

You need to know when things break before your users tell you (or before they silently leave and never come back).

Sentry's free tier gives you 5,000 errors per month, which is more than enough to start. Install it:

npx @sentry/wizard@latest -i nextjs

The wizard creates configuration files automatically. Verify it's working by adding a test error:

// app/api/debug-sentry/route.ts
// Remove this route after verifying Sentry works
export async function GET() {
  throw new Error("Sentry test error - delete this route");
}

Hit that endpoint once in production, check the Sentry dashboard, then delete the route.

For more nuanced error handling patterns in AI-built apps, see our guide to error handling.

Health Check Endpoint

Every production app needs a health check. Load balancers, uptime monitors, and deployment platforms use it to verify your app is alive.

// app/api/health/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const health = {
    status: "ok",
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
  };
 
  // Optionally check database connectivity
  try {
    const { createClient } = await import("@supabase/supabase-js");
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY!
    );
    const { error } = await supabase.from("profiles").select("id").limit(1);
    if (error) throw error;
    health.status = "ok";
  } catch {
    return NextResponse.json(
      { status: "degraded", timestamp: new Date().toISOString() },
      { status: 503 }
    );
  }
 
  return NextResponse.json(health);
}

Point an uptime monitor (UptimeRobot's free tier works) at https://yourdomain.com/api/health and get alerted when your app goes down.

CORS Configuration

If your frontend and API are on the same domain (which they are with Next.js API routes on Vercel), you probably don't need custom CORS configuration. But if you're calling your API from a different domain, a mobile app, or a browser extension, you'll need it.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  if (request.method === "OPTIONS") {
    return new NextResponse(null, {
      status: 200,
      headers: {
        "Access-Control-Allow-Origin": process.env.ALLOWED_ORIGIN || "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
        "Access-Control-Max-Age": "86400",
      },
    });
  }
 
  const response = NextResponse.next();
  response.headers.set(
    "Access-Control-Allow-Origin",
    process.env.ALLOWED_ORIGIN || "*"
  );
  return response;
}
 
export const config = {
  matcher: "/api/:path*",
};

Replace * with your actual frontend domain in production. Wildcard CORS is fine for development but a security risk in production.

Performance Basics

A few quick wins that make a real difference:

Image optimization. Use Next.js <Image> instead of raw <img> tags. AI-generated code almost always uses <img>. The Next.js component automatically handles lazy loading, responsive sizing, and modern formats like WebP.

// Before: Raw img tag (AI default)
<img src="/hero.png" alt="Hero" />
 
// After: Optimized Next.js Image
import Image from "next/image";
<Image src="/hero.png" alt="Hero" width={1200} height={630} priority />

Caching headers on API routes. For data that doesn't change every request, add cache-control headers:

export async function GET() {
  const data = await fetchPublicData();
 
  return Response.json(data, {
    headers: {
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
    },
  });
}

This tells CDN edge nodes to cache the response for 60 seconds and serve stale content for up to 5 minutes while revalidating in the background. One header, major performance improvement.

Bundle analysis. If your app feels slow, check what's in the bundle:

npm install @next/bundle-analyzer
 
# Add to next.config.mjs
# const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' })
# module.exports = withBundleAnalyzer(nextConfig)
 
ANALYZE=true npm run build

AI tools frequently import entire libraries when only a single function is needed. You'll often find you can cut your bundle size by switching to targeted imports.

How FinishKit Automates Deploy Readiness

Everything in this guide is what FinishKit does automatically. It scans your full repo, detects missing env vars, build-breaking TypeScript errors, hardcoded URLs, missing migrations, and the dozens of other deployment gaps AI tools leave behind. Before you push, you get a deploy readiness report that tells you exactly what to fix, in priority order.

Ship It Tonight

The distance between localhost and production is smaller than it feels. Here's the short version:

  1. Remove ignoreBuildErrors and fix your build
  2. Inventory and validate your environment variables
  3. Set up database migrations
  4. Connect your repo to Vercel (or Railway, or Fly.io, or Docker)
  5. Configure env vars on your platform
  6. Deploy, verify the full flow, check mobile
  7. Add error monitoring and a health check

That's it. No dark magic. No three-week slog. If your build passes locally and your env vars are set, you're 90% of the way there.

The app you built this weekend can be live by tonight. Start with the finishing checklist if you want to make sure nothing else is lurking, or jump straight to the deploy. Either way, stop perfecting and start shipping.