Deploying Next.js: Shared Hosting vs VPS (Static Export vs SSR)
Next.js gives you three rendering modes — static, server-side, and incremental. Each needs different hosting. This guide helps you pick between shared cPanel, DirectAdmin, and VPS, with concrete deployment recipes for each mode.
Three Next.js rendering modes
Static Site Generation (SSG) — pages are pre-rendered at build time into plain HTML. No server-side code runs in production. Deployable on any static host, including cPanel.
Server-Side Rendering (SSR) — every request hits the Node.js server, which renders the page fresh. Needs a long-running Node process. Requires a VPS.
Incremental Static Regeneration (ISR) — static generation with periodic regeneration. Needs a Node server; same hosting requirements as SSR.
Decision table
| Mode | Hosting needed | Features lost on shared |
|---|---|---|
| Pure static (SSG only) | cPanel / DirectAdmin / any static host | None |
| API routes | VPS | Server-side code won't run on static |
| SSR / ISR | VPS | Dynamic rendering won't work |
Image optimisation via <Image /> | VPS or a separate image service | Built-in optimiser needs Node |
| Middleware | VPS | Same reason |
getServerSideProps | VPS | Same reason |
Rule: if your app only uses getStaticProps (or no data fetching / static data only), cPanel works. Any SSR or API routes → VPS.
Path A: Static export to cPanel
Configure static export
In next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: {
unoptimized: true, // built-in optimiser disabled for static export
},
trailingSlash: true, // optional — generates /about/index.html instead of /about.html
};
module.exports = nextConfig;Build
npm run build
# Creates out/ directory with static HTML/CSS/JSUpload to cPanel
Upload the out/ folder contents to public_html/ via File Manager, FTP, or the CI pattern from GitHub Actions → cPanel.
.htaccess for clean URLs
If you set trailingSlash: true, create public_html/.htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1/index.html [L]What you lose on static export
- API routes (
pages/api/*,app/api/*) — they don't exist - SSR (
getServerSideProps) - ISR (
revalidateingetStaticProps) - Image optimisation via
next/image(served as-is, no responsive formats) - Middleware (edge functions)
- Dynamic routes that can't be enumerated at build time (e.g.,
/users/[id]with arbitrary IDs)
If your app needs none of these — static export on cPanel is excellent. Fast, cheap, no operational overhead.
Common export issues
Dynamic routes fail to build. If you have pages/blog/[slug].js, you need getStaticPaths to enumerate every valid slug. Arbitrary user-generated dynamic routes can't be statically exported — you need SSR on a VPS.
`next/font` errors during export. Some font loaders require runtime. Use Google Fonts CDN via standard <link> tags instead.
API route references. If you fetch('/api/data') from a page, it fails — the API doesn't exist. Move API calls to external services or pre-fetch data at build time with getStaticProps.
Path B: SSR / ISR on VPS
For the full Next.js feature set — SSR, API routes, ISR, image optimisation, middleware — you need a Node runtime. Our VPS plans are the right tier.
Production build
Use Next.js's standalone output mode for a slim deployment:
// next.config.js
const nextConfig = {
output: 'standalone',
};This copies all required files (node_modules subset, the build output, the server script) into .next/standalone/. Much smaller than full node_modules.
npm run buildStructure produced:
.next/standalone/server.js ← the entry point
.next/standalone/.next/...
.next/standalone/package.json
.next/static/ ← needs to be copied alongside
public/ ← needs to be copied alongsideDeploy package
Copy these to your VPS:
rsync -avz .next/standalone/ [email protected]:/home/user/myapp/
rsync -avz .next/static/ [email protected]:/home/user/myapp/.next/static/
rsync -avz public/ [email protected]:/home/user/myapp/public/Run with PM2
Install PM2 if you haven't (see our PM2 guide):
npm install -g pm2Create ecosystem.config.js at /home/user/myapp/:
module.exports = {
apps: [{
name: 'nextjs',
script: 'server.js',
instances: 2, // or 'max' for all cores
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '500M',
error_file: './logs/err.log',
out_file: './logs/out.log',
}]
};Start:
cd /home/user/myapp
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup systemd # follow the printed instructionsnginx reverse proxy
Create /etc/nginx/sites-available/myapp.conf:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Serve _next/static with long cache — Next.js hashes these for cache-busting
location /_next/static/ {
alias /home/user/myapp/.next/static/;
expires 365d;
add_header Cache-Control "public, immutable";
}
# Serve /public assets
location /static/ {
alias /home/user/myapp/public/static/;
expires 30d;
}
# Everything else → Next.js server
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
client_max_body_size 20m;
}Enable:
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxHTTPS via Let's Encrypt:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comEnvironment variables
Next.js reads .env.production at build time for client-accessible variables (anything prefixed NEXT_PUBLIC_). Server-only variables are read at runtime from process.env.
Strategy:
# On the server at /home/user/myapp/.env
DATABASE_URL=postgresql://...
STRIPE_SECRET=sk_live_...
NEXT_PUBLIC_SITE_URL=https://yourdomain.comLoad before starting PM2:
set -a
source .env
set +a
pm2 start ecosystem.config.js --env productionOr reference the file in ecosystem.config.js:
apps: [{
name: 'nextjs',
script: 'server.js',
env_file: '/home/user/myapp/.env',
...
}]See Environment Variables & Secrets Management for the full pattern.
Database connections — avoid the N+1 connection leak
In a Node.js app, opening a new database connection per request exhausts the connection pool fast. Use connection pooling:
// lib/db.js
import { Pool } from 'pg';
let pool;
export function getPool() {
if (!pool) {
pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // max connections
idleTimeoutMillis: 30000,
});
}
return pool;
}For Prisma — it handles pooling automatically, but you must cache the client across requests in dev:
// lib/prisma.js
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;CI/CD for Next.js
Same pattern as GitHub Actions → cPanel, adapted:
- name: Build
run: |
npm ci
npm run build
- name: Deploy via rsync
uses: burnett01/[email protected]
with:
switches: -avz --delete
path: .next/standalone/
remote_path: /home/user/myapp/
remote_host: ${{ secrets.VPS_HOST }}
remote_user: ${{ secrets.VPS_USER }}
remote_key: ${{ secrets.VPS_SSH_KEY }}
- name: Deploy static + public assets
uses: burnett01/[email protected]
with:
switches: -avz
path: .next/static/
remote_path: /home/user/myapp/.next/static/
# ... same auth
- name: Reload PM2
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /home/user/myapp
pm2 reload ecosystem.config.js --env productionCommon pitfalls
- Export mode with API routes. Export removes all server code — API routes silently disappear. Move to VPS + SSR or refactor API calls to an external service.
- `next/image` on static export without setting `unoptimized: true`. Build fails. Either set that flag (trade-off: no responsive image generation) or move to VPS.
- Wrong Node.js version on production. Next.js 14+ requires Node 18.17+. Check with
node -v. Our VPS plans let you install any Node version via nvm. - Serving `.next/static` through Node instead of nginx. Needlessly slow. Let nginx handle static assets directly.
- Missing `output: 'standalone'` on a VPS. Full
node_modulesdeploy is much larger and slower than standalone mode. - PM2 cluster mode with in-memory state. If your pages rely on in-memory cache, cluster workers each have their own — use Redis for shared state.
- Forgetting the `/public` folder in deploy. Static assets under
public/are not in.next/standalone/— must be copied separately. - Hardcoding the site URL. Use
NEXT_PUBLIC_SITE_URLenv var; let it differ between staging and production.
Frequently asked questions
Do I need a VPS for a Next.js blog?
If you use getStaticProps only and export statically — no, cPanel works fine. If you use ISR or want per-visit personalisation — yes, VPS.
Can I run Next.js on Vercel and still use Domain India for domain / email?
Yes. Register the domain with Domain India, point NS to Vercel's servers for the app subdomain (e.g., app.yourdomain.com), keep other records (MX for email, etc.) on DI's DNS.
How do I handle file uploads in Next.js on a VPS?
Use an external service (S3, Cloudflare R2) or a mounted directory on the VPS. Local disk uploads on VPS require proper client_max_body_size in nginx.
What Node.js version should I use?
Node 20 LTS (current) or 18 LTS (older, still supported). Next.js 14+ requires at least 18.17.
Can I deploy a Next.js app with authentication to cPanel statically?
Client-side auth libraries like Firebase Auth, Clerk, or Auth0 work on static sites (they use cookies / JWT, not server sessions). Server-side-rendered auth (NextAuth with database session) needs a Node server → VPS.
How do I run scheduled jobs with Next.js?
API route + cron hitting it. Or a separate Node worker running on the same VPS. Avoid putting cron logic in the Next.js process itself — harder to debug.
Edge functions / middleware — do they need Vercel?
Next.js middleware runs in a Node environment on your VPS just fine. Vercel's specific "Edge Runtime" (V8 isolates, not full Node) is Vercel-specific; your VPS deployment uses Node.js for middleware, which has more capabilities but slower cold start.
Need help picking between static export and VPS SSR for your specific Next.js app? [email protected] — we can review your app's data fetching patterns and recommend the right tier.