Next.js Deploy · 2025

How to Deploy a Next.js App to Production Without Vercel

Updated April 2025 · 10 min read

Next.js runs anywhere Node.js runs. Here's how to deploy it without Vercel's per-seat pricing.

HomeBlog › How to Deploy a Next.js App to Production Without Vercel

How to Deploy a Next.js App to Production Without Vercel

Vercel is the company that built Next.js, so it's the default deployment target everyone reaches for. But Vercel's pricing can get expensive fast for production applications — the Pro plan starts at $20/month per team member, and enterprise pricing for high-traffic apps can reach hundreds per month.

More importantly: Next.js is open source and runs anywhere Node.js runs. You don't need Vercel to deploy it. This guide covers deploying a Next.js app to production without Vercel — on a platform where you own the runtime, control the environment, and pay per app rather than per team member or serverless invocation.

Why Developers Move Away from Vercel

Vercel is excellent engineering. Their developer experience is polished. But:

Pricing at scale:
- Hobby (personal): $0 — but no commercial use allowed
- Pro: $20/user/month
- Serverless function execution is charged beyond the included limit (100GB-hours)
- Bandwidth charges above 1TB/month
- Image optimisation charged per 1,000 source images above the free tier

For a startup running 5 developers on a commercial product, that's $100/month just for team seats — before any usage charges.

Vendor lock-in:
Vercel has Vercel-specific features — Edge Functions, Vercel KV, Vercel Blob, ISR (Incremental Static Regeneration) with their CDN — that don't work outside their platform. If you've built your app to depend on these, migrating becomes harder.

Lack of control:
Serverless deployment means your code runs in ephemeral functions, not a persistent process. You can't access your server's filesystem, maintain in-memory state, use WebSockets natively, or run background processes in the traditional sense.

The alternative: Deploy Next.js as a standard Node.js application on a container-based platform. You get a persistent process, WebSocket support, full filesystem access, and none of the serverless constraints.

Next.js Deployment Modes Explained

Before deploying, understand which mode you're using:

Mode 1: Node.js Server (Recommended for Dynamic Apps)

Next.js runs as a Node.js HTTP server using next start. This is the most flexible mode:
- Server-side rendering (SSR) works
- API routes work
- App Router (Next.js 13+) works fully
- Supports WebSockets
- Persistent server process (not serverless)
- Works on any Node.js hosting

Start command: next start or node server.js

Mode 2: Static Export

Next.js generates plain HTML/CSS/JS files with next export. No Node.js server required — serve the out/ directory with any web server.

Limitations:
- No SSR (only SSG)
- No API routes
- No dynamic routes without getStaticPaths
- No image optimisation

Build command: npm run build && next export

For this guide, we'll focus on Mode 1 (Node.js server) — it's what most production apps need.

Step 1: Configure Next.js for Container Deployment

1.1 Update next.config.js for standalone output

// next.config.js
const nextConfig = {
  output: 'standalone',  // Bundles required dependencies for container deployment

  // If you have custom domains, configure CORS/headers here
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: process.env.ALLOWED_ORIGIN || '*' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
        ],
      },
    ];
  },

  // Image domains for next/image
  images: {
    domains: ['yourdomain.com', 'your-s3-bucket.s3.amazonaws.com'],
  },

  // Environment variables available to browser (public)
  // Never put secrets here
  env: {
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
};

module.exports = nextConfig;

The output: 'standalone' mode creates a .next/standalone directory with all required Node modules bundled — smaller deployment size and faster starts.

1.2 Configure package.json scripts

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p $PORT",
    "lint": "next lint"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}

The -p $PORT flag makes Next.js listen on the platform-assigned port. Without this, Next.js defaults to port 3000, which may conflict with the container's port assignment.

1.3 Create .gitignore

node_modules/
.next/
out/
.env
.env.local
.env.*.local
*.log
.DS_Store

Step 2: Handle Environment Variables

Next.js has two types of environment variables:

Server-side only (secrets): Available in getServerSideProps, API routes, server components. Never exposed to the browser.

DATABASE_URL
API_SECRET_KEY
STRIPE_SECRET_KEY

Public (browser-accessible): Must be prefixed with NEXT_PUBLIC_. Embedded in client-side bundle at build time.

NEXT_PUBLIC_APP_URL=https://yourdomain.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_ANALYTICS_ID=UA-XXXXXXXX

Critical: NEXT_PUBLIC_ variables are baked into the build output. They must be set before the build runs. Server-side variables can be changed without rebuilding.

Step 3: Configure ApexWeave for Next.js

Set Python/Node version

apexweave env:set your-app.apexweaveapp.com APEXWEAVE_STACK=node:22

Set environment variables

# Database
apexweave env:set your-app.apexweaveapp.com DATABASE_URL=postgres://username:password@dns.apexweaveapp.com:5432/mydb

# NextAuth (if using NextAuth.js)
apexweave env:set your-app.apexweaveapp.com NEXTAUTH_URL=https://yourdomain.com
apexweave env:set your-app.apexweaveapp.com NEXTAUTH_SECRET=$(openssl rand -hex 32)

# OAuth providers (if using social login)
apexweave env:set your-app.apexweaveapp.com GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
apexweave env:set your-app.apexweaveapp.com GOOGLE_CLIENT_SECRET=GOCSPX-xxx

# Stripe
apexweave env:set your-app.apexweaveapp.com STRIPE_SECRET_KEY=sk_live_...

# Public variables (baked into build)
apexweave env:set your-app.apexweaveapp.com NEXT_PUBLIC_APP_URL=https://yourdomain.com
apexweave env:set your-app.apexweaveapp.com NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...

# App config
apexweave env:set your-app.apexweaveapp.com NODE_ENV=production

Configure build commands

In your ApexWeave dashboard → Settings → Build Configuration:

Install Command:

npm ci

Build Command:

npm run build

Start Command:

npm run start
# Or directly: next start -p $PORT

Step 4: Deploy

# Add remote
git remote add apexweave https://git.apexweaveapp.com/your-username/your-nextjs-app.git

# Push
git push apexweave main

# Watch deployment
apexweave deploy your-app.apexweaveapp.com --follow

Expected build output:

Running: npm ci
...packages installed

Running: npm run build
> next build

info  - Linting and checking validity of types...
info  - Creating an optimized production build...
info  - Compiled successfully

Route (app)                              Size     First Load JS
┌ ○ /                                   5.3 kB    89 kB
├ ○ /about                              2.1 kB    85 kB
├ ƒ /blog/[slug]                        3.2 kB    87 kB
├ ○ /blog                              4.7 kB    89 kB
└ ƒ /api/contact                       0 B       79 kB
+ First Load JS shared by all           79 kB
  └ chunks/main-app.js                  78 kB

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

Build complete.

Running: npm run start
> next start -p 8080

ready - started server on 0.0.0.0:8080

Step 5: Handle Database with Prisma (Common Next.js Setup)

If using Prisma ORM with Next.js:

# Install
npm install prisma @prisma/client

# Initialize
npx prisma init

prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}

Add database migration to post-deployment hook:
In Settings → Build Configuration → Post-Deployment Hook:

npx prisma migrate deploy

This runs only new migrations — existing ones are skipped.

Generate Prisma client during build:
Update build command:

npx prisma generate && next build

Prisma Client must be generated for your target platform's Node.js version and OS. Generating it during the build (on the server) ensures compatibility.

Step 6: Next.js Image Optimisation Without Vercel

Vercel's Image Optimisation API resizes and converts images to WebP automatically. Without Vercel, you have options:

Option 1: Use Cloudflare Images
Cloudflare's image transformation URL:

https://imagedelivery.net/account-hash/image-id/width=800

Configure in next.config.js:

images: {
  loader: 'custom',
  loaderFile: './imageLoader.js',
}

Option 2: Use Next.js built-in optimisation (default)
Next.js's built-in image optimiser works without Vercel — it runs server-side when images are requested. It caches optimised images in .next/cache/images/.

In your container, this works automatically. The optimisation happens on your Node.js server process.

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority  // LCP image — load immediately, don't lazy load
      quality={85}
    />
  );
}

Option 3: Pre-optimise images
Convert to WebP and resize before committing. Tools: sharp CLI, Squoosh, ImageOptim.

Step 7: Handle ISR (Incremental Static Regeneration) Without Vercel

Vercel's ISR works by revalidating pages on their CDN edge. Without Vercel, ISR still works — it just revalidates pages on your Node.js server.

// pages/products/[id].js
export async function getStaticProps({ params }) {
  const product = await fetchProduct(params.id);
  return {
    props: { product },
    revalidate: 60,  // Revalidate every 60 seconds
  };
}

With output: 'standalone', revalidation happens on your container server. Revalidated pages are cached in memory (.next/server/pages/). This is slightly different from Vercel's edge caching but works for most use cases.

For high-traffic apps that need edge-level ISR performance, add Cloudflare in front of your container to cache revalidated pages at the CDN layer.

Common Next.js Deployment Issues

Error: NEXT_PUBLIC_* variable not found after deployment

Cause: Public environment variables are baked into the build. If you set them after the build ran, they won't be in the bundle.

Fix: Set all NEXT_PUBLIC_* variables before deploying. Then redeploy (the build will pick them up).

next start not found

Cause: next package not found, or build failed.

Fix:

apexweave logs your-app.apexweaveapp.com
# Check for build errors

Ensure next is in dependencies (not devDependencies):

{
  "dependencies": {
    "next": "15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Images not loading in production

Cause: Domain not in images.domains config, or NEXT_PUBLIC_APP_URL not set correctly.

Fix: Add your domain to next.config.js:

images: {
  domains: ['yourdomain.com', 'another-image-source.com'],
}

getServerSideProps returning stale data

Cause: Your server-rendered pages are being cached at the CDN layer (if using Cloudflare) without being distinguished from static pages.

Fix: Set proper Cache-Control headers in getServerSideProps:

export async function getServerSideProps({ res }) {
  res.setHeader('Cache-Control', 'no-store');
  // ...
}

NextAuth.js NEXTAUTH_URL error

Cause: NEXTAUTH_URL not set or set to wrong domain.

Fix:

apexweave env:set your-app.apexweaveapp.com NEXTAUTH_URL=https://yourdomain.com

Must match the actual domain the app is running on, including protocol.

The Vercel Features You Don't Need

Many Next.js developers feel locked in to Vercel because of specific features. Most have alternatives:

Vercel Feature Alternative Without Vercel
Edge Functions API Routes with middleware
Vercel KV Redis (ApexWeave managed)
Vercel Blob S3 or Cloudflare R2
Preview Deployments Git branch + manual deploy
ISR edge caching Cloudflare CDN in front of Node.js server
Image Optimisation Next.js built-in (server-side) or Cloudflare Images
Analytics Plausible, PostHog, or Vercel's own analytics (works without Vercel hosting)

The core features of Next.js — App Router, server components, SSR, SSG, API routes — all work identically on a Node.js container. Vercel-specific features (Edge Functions, KV, Blob) are the ones that don't transfer.

If you haven't built your app to depend on Vercel-specific primitives, migration to a container host is seamless.

Deploy your Next.js app at apexweave.com/git-deployment.php — Node.js 22, automatic builds, environment variables, and custom domain with SSL.

Deploy Your App with Git Push

Automatic builds, environment variables, live logs, rollback, and custom domains. No server management required.

Deploy Free — No Card Required

Powered by WHMCompleteSolution