How to Deploy a Next.js App to Production Without Vercel
Next.js runs anywhere Node.js runs. Here's how to deploy it without Vercel's per-seat pricing.
In This Guide
- Why Developers Move Away from Vercel
- Next.js Deployment Modes Explained
- Step 1: Configure Next.js for Container Deployment
- Step 2: Handle Environment Variables
- Step 3: Configure ApexWeave for Next.js
- Step 4: Deploy
- Step 5: Handle Database with Prisma (Common Next.js Setup)
- Step 6: Next.js Image Optimisation 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 RequiredPowered by WHMCompleteSolution