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 ofpublic_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.





