Client Area

Deploying Next.js: Shared Hosting vs VPS (Static Export vs SSR)

ByDomain India Team
8 min read22 Apr 20262 views

In this article

  • 1Three Next.js rendering modes
  • 2Decision table
  • 3Path A: Static export to cPanel
  • 4Configure static export
  • 5Build

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

ModeHosting neededFeatures lost on shared
Pure static (SSG only)cPanel / DirectAdmin / any static hostNone
API routesVPSServer-side code won't run on static
SSR / ISRVPSDynamic rendering won't work
Image optimisation via <Image />VPS or a separate image serviceBuilt-in optimiser needs Node
MiddlewareVPSSame reason
getServerSidePropsVPSSame 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:

javascript
/** @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

bash
npm run build
# Creates out/ directory with static HTML/CSS/JS

Upload 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 (revalidate in getStaticProps)
  • 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:

javascript
// 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.

bash
npm run build

Structure 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 alongside

Deploy package

Copy these to your VPS:

bash
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):

bash
npm install -g pm2

Create ecosystem.config.js at /home/user/myapp/:

javascript
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:

bash
cd /home/user/myapp
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup systemd          # follow the printed instructions

nginx reverse proxy

Create /etc/nginx/sites-available/myapp.conf:

nginx
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:

bash
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

HTTPS via Let's Encrypt:

bash
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Environment 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:

bash
# On the server at /home/user/myapp/.env
DATABASE_URL=postgresql://...
STRIPE_SECRET=sk_live_...
NEXT_PUBLIC_SITE_URL=https://yourdomain.com

Load before starting PM2:

bash
set -a
source .env
set +a
pm2 start ecosystem.config.js --env production

Or reference the file in ecosystem.config.js:

javascript
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:

javascript
// 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:

javascript
// 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:

yaml
- 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 production

Common pitfalls

  1. 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.
  2. `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.
  3. 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.
  4. Serving `.next/static` through Node instead of nginx. Needlessly slow. Let nginx handle static assets directly.
  5. Missing `output: 'standalone'` on a VPS. Full node_modules deploy is much larger and slower than standalone mode.
  6. 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.
  7. Forgetting the `/public` folder in deploy. Static assets under public/ are not in .next/standalone/ — must be copied separately.
  8. Hardcoding the site URL. Use NEXT_PUBLIC_SITE_URL env 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.

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket