StratifyIQ

cPanel & Passenger Deploy Runbook for Next.js

A hands-on runbook for deploying Next.js apps on cPanel/Passenger where managed hosting requires it.

The Stratify IQ Team5 min read
◆ How it works
Section 1 visual overview
01 · Discover
Section 2 visual overview
02 · Plan
Section 3 visual overview
03 · Build
Section 4 visual overview
04 · Measure

Deploying Next.js on cPanel with Phusion Passenger is not the path of least resistance, but it is a well-understood process once the specific constraints of the environment are clear. The constraints are: Passenger requires a Node.js entry point file, the standalone output format is required for self-hosted deployments, and native binaries (particularly sharp for image optimisation) must be built against the server's Node version rather than a local development environment.

This runbook documents the exact sequence — build, package, upload, configure, validate — that produces a working deployment with a reliable rollback path.

Prerequisites

Before starting a deployment:

  • Confirm the cPanel host's Node.js version. For Ventra IP and similar Australian managed hosts, this is typically selectable in the cPanel Node.js interface (Node 18.x or 20.x recommended)
  • Confirm SSH access — uploads via file manager are possible but slow and error-prone for large builds; SCP or SFTP is more reliable
  • Know the application directory path on the server (usually ~/app/ or a subdirectory of public_html/)
  • Have the current production build archived before beginning — this is your rollback target

Step 1: Configure the Next.js build for standalone output

In next.config.js, set the output mode to standalone:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}
module.exports = nextConfig

The standalone output bundles the minimal set of files needed to run the server, including node_modules dependencies, without requiring a full npm install on the server. This is important for Passenger deployments where running install commands on the production server is impractical.

Step 2: Build locally against the correct Node version

Build the application using the same Node version as the production server. If your local environment runs a different version, use a version manager (nvm or fnm) to switch before building.

nvm use 20
npm install
npm run build

After the build completes, the output structure will be:

.next/
  standalone/          # the self-contained server
  static/              # compiled assets — must be copied manually
public/                # static files — must be copied manually

Step 3: Assemble the deployment package

The standalone directory does not include .next/static/ or public/ by default — these must be copied into position before packaging. This is the most common cause of deployment failures (missing assets, 503 errors, broken images).

cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public

After copying, the structure under .next/standalone/ should be:

.next/standalone/
  server.js            # the Next.js standalone server entry point
  node_modules/        # bundled dependencies
  .next/
    static/            # compiled CSS/JS assets
  public/              # static files

Create the deployment zip:

cd .next/standalone
zip -r ../../deploy.zip .

Step 4: Create the server.js shim

Passenger requires a Node.js application with an entry point file. The standalone output's server.js is a suitable entry point, but it may need environment variables set before launch. Create a server.js shim in the application root if one is not already present:

const { createServer } = require('http')
const { parse } = require('url')
const next = require('./node_modules/next')

const dev = process.env.NODE_ENV !== 'production'
const hostname = process.env.HOSTNAME || 'localhost'
const port = parseInt(process.env.PORT || '3000', 10)

const app = next({ dev, hostname, port })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer(async (req, res) => {
    const parsedUrl = parse(req.url, true)
    await handle(req, res, parsedUrl)
  }).listen(port, () => {
    console.log(`Server listening on port ${port}`)
  })
})

In many cPanel/Passenger configurations, the standalone server.js itself serves as the entry point without modification — test this first before adding a custom shim.

Step 5: Upload to the server

Using SCP or the cPanel file manager, upload the deployment zip to the application directory and extract it. Verify the directory structure after extraction matches the expected layout above.

If using SCP:

scp deploy.zip user@server:/home/user/app/deploy.zip
ssh user@server "cd /home/user/app && unzip -o deploy.zip"

Step 6: Configure Passenger in cPanel

In cPanel, navigate to the Node.js section (often labelled "Setup Node.js App" or similar):

  • Set the Node.js version to match the build version
  • Set the application root to the directory containing server.js
  • Set the application startup file to server.js
  • Enable Passenger for the domain

Passenger reads these settings and serves the Node.js application behind its process manager, handling restarts and process supervision.

Step 7: Validate the deployment

After restarting Passenger:

  • Load the homepage and verify it renders correctly
  • Test at least one service page and one blog page to confirm routing
  • Submit a test form and verify the submission is received
  • Check that images are loading (confirms .next/static/ was copied correctly)
  • Check the browser's network tab for any 404 errors on static assets

If assets are returning 404, the most likely cause is that .next/static/ was not copied into .next/standalone/.next/static/ before packaging. Re-run the copy step and re-deploy.

Rollback preparation

Before every deployment, archive the current production build:

ssh user@server "cp -r /home/user/app /home/user/app-backup-$(date +%Y%m%d)"

If the deployment fails validation, restoring is straightforward:

ssh user@server "rm -rf /home/user/app && mv /home/user/app-backup-YYYYMMDD /home/user/app"

Then restart Passenger to serve the restored build.

Common issues and fixes

503 error after deployment: most commonly caused by missing .next/static/ or public/ directories, or a Node version mismatch between build and server. Check Passenger error logs (accessible in cPanel) for the specific error.

Images not loading: .next/static/ was not copied into the standalone directory. Re-run the copy step.

sharp binary errors in logs: the sharp package includes native binaries compiled for the build platform. A Windows-built binary will not run on Linux. Build on a Linux environment (or WSL) to match the server platform, or build directly on the server via SSH.

Passenger not restarting after upload: use the "Restart" button in cPanel's Node.js interface, or touch the tmp/restart.txt file in the application root if the host supports Passenger restart via that mechanism.

Related content

Frequently asked questions

Is cPanel/Passenger recommended for Next.js?

It works for marketing sites when a managed host is required, but modern platforms like Vercel, Railway, or Render simplify the workflow significantly and remove the Node version compatibility and server configuration constraints. Use cPanel/Passenger when the hosting environment is fixed by a client requirement or existing contract, and document the specific server configuration so future deployments are repeatable.

How do you handle static assets and caching on cPanel?

The .next/static/ directory should be copied into the .next/standalone/.next/static/ path so the standalone server can serve it. The public/ directory should sit at the root alongside server.js. For cache headers, configure them in next.config.js using the headers() function, or add a .htaccess file if using Apache as the reverse proxy in front of Passenger.

What Node version should you use for Next.js on cPanel?

Match the Node version to what your cPanel host supports. Ventra IP and most Australian shared hosts running Passenger support Node 18.x and 20.x. Next.js 14+ requires Node 18.17+ at minimum. Check the host's Node version list before building — building locally against a newer Node version than the server supports will produce binary incompatibilities for native modules like sharp.

◆ Sources & further reading

  1. Passenger docsPhusion Passenger
  2. cPanel documentationcPanel
◆ Work with us

Web Development

Under a second to load. Three times the conversion. Reported weekly in revenue, not pageviews.

See the full service →
◆ Ready to do, not just read?

Turn what you just learned
into revenue.

15-minute strategy call. We'll map what's holding your business back and walk you through the exact solution — live, no slides.