How to Run a Node.js Puppeteer App on cPanel
Deploy Puppeteer on cPanel with Passenger, configure Chromium, restart safely, fix launch errors, and choose an API when shared hosting is not enough.
Yes, you can run a Node.js Puppeteer app on cPanel when your host enables Node.js, Passenger, SSH/package installation, and the Linux libraries Chromium needs. The deployment model is a Passenger-managed Node.js application. Passenger routes the public request to your app and controls the listening port; you do not expose a standalone node process on an arbitrary public port.
The reliable path is:
- Confirm that the provider supports Node.js, Passenger, headless Chromium, and the required system libraries.
- Create an application directory with
app.js, the default Passenger startup filename. - Install Puppeteer and your other npm dependencies with the host-provided Node.js toolchain.
- Make the server listen on Passenger’s assigned port.
- Register the app in cPanel Application Manager or deploy it through the provider’s Websites hub.
- Test locally, then test through the domain.
What cPanel is managing
cPanel commonly runs Node.js through Phusion Passenger and Apache. Passenger starts workers, proxies requests from your domain, and performs reverse port binding. cPanel documentation states that Passenger controls the port on which a Node.js application listens when it handles HTTP requests. Bind to process.env.PORT; do not hard-code a public port or open one in the firewall.
Node.js availability is provider-controlled. On supported RHEL-based installations, cPanel documents EasyApache packages such as ea-nodejs16, ea-nodejs18, ea-nodejs20, and ea-nodejs22, together with Passenger and the Apache environment module. Ubuntu, AlmaLinux 9 or later, and Rocky Linux 9 or later use the documented ea-apache24-mod-passenger package path. Your provider may expose a different version or no Node.js interface at all.
Check compatibility before writing code
| Requirement | What to confirm | Why it matters |
|---|---|---|
| Node.js | A supported version and npm or another permitted package manager | Puppeteer and your application must run on the selected runtime. |
| Passenger | Application Manager, Websites hub, or another supported registration path | Your domain needs Passenger to start and route the app. |
| SSH and filesystem access | Permission to create an app directory and install dependencies | You need to install Puppeteer and inspect logs. |
| Chromium execution | Headless browser processes are allowed and have adequate memory/time limits | Some shared hosts prohibit browser processes or terminate them. |
| Linux libraries | GTK, NSS, GBM, audio, font, and related shared libraries | Node.js can be installed correctly while Chrome still fails to launch. |
| Operating system | Whether the host uses a supported glibc-based distribution | Chrome does not support Alpine out of the box. |
Ask the provider specifically whether Puppeteer or Chromium is permitted, which executable path is available, whether the account can launch sandboxed browser processes, and what memory and process limits apply. If the provider cannot install the required libraries, a VPS or dedicated server is usually a better fit.
Build a minimal Puppeteer app
Create a directory in your cPanel home directory, for example ~/nodejsapp. Use app.js unless you have a reason to configure a custom startup file.
package.json
{
"name": "cpanel-puppeteer-app",
"private": true,
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"puppeteer": "^24.0.0"
}
}
Use the Puppeteer version supported by the Node.js version your host provides. If the host blocks Puppeteer’s browser download, install the npm package and point Puppeteer at a browser already installed by the administrator.
app.js
const http = require('node:http');
const { URL } = require('node:url');
const puppeteer = require('puppeteer');
const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || '127.0.0.1';
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH || undefined;
let browserPromise;
function getBrowser() {
if (!browserPromise) {
browserPromise = puppeteer.launch({
headless: true,
executablePath: browserPath,
// Add --no-sandbox only when your host administrator explicitly requires it.
args: []
}).catch((error) => {
browserPromise = undefined;
throw error;
});
}
return browserPromise;
}
function sendJson(res, status, value) {
const body = JSON.stringify(value);
res.writeHead(status, {
'content-type': 'application/json; charset=utf-8',
'content-length': Buffer.byteLength(body)
});
res.end(body);
}
const server = http.createServer(async (req, res) => {
const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
if (requestUrl.pathname === '/healthz') {
return sendJson(res, 200, { ok: true });
}
if (requestUrl.pathname !== '/screenshot') {
return sendJson(res, 404, { error: 'Use /screenshot?url=https://example.com' });
}
const target = requestUrl.searchParams.get('url');
if (!target) return sendJson(res, 400, { error: 'Missing url query parameter' });
let parsed;
try {
parsed = new URL(target);
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Only HTTP(S) URLs are allowed');
} catch {
return sendJson(res, 400, { error: 'url must be an absolute HTTP(S) URL' });
}
let page;
try {
const browser = await getBrowser();
page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(parsed.href, { waitUntil: 'networkidle2', timeout: 45000 });
const image = await page.screenshot({ type: 'png', fullPage: true });
res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'no-store' });
res.end(image);
} catch (error) {
console.error(error);
sendJson(res, 502, { error: 'Screenshot failed', message: error.message });
} finally {
if (page) await page.close().catch(() => {});
}
});
server.listen(port, host, () => {
console.log(`Listening on ${host}:${port}`);
});
This example reuses one browser process and creates a fresh page per request. It validates the URL, bounds navigation to 45 seconds, closes pages in a finally block, and exposes a health endpoint. In production, add authentication and an allowlist or SSRF protection before accepting arbitrary URLs.
Install and test through SSH
- Open SSH as the cPanel account user and enter the application directory.
- Use the Node.js binary supplied by the host. A documented cPanel pattern is
/opt/cpanel/ea-nodejs**/bin/node; the exact path varies by installation. - Install dependencies:
cd ~/nodejsapp
npm install
If your host requires an explicit binary, use its matching npm path, for example:
/opt/cpanel/ea-nodejs20/bin/npm install
Run a temporary local test:
node app.js
In another SSH session, call the local health endpoint and screenshot route:
curl -i http://127.0.0.1:3000/healthz
curl -o local.png --get \
--data-urlencode 'url=https://example.com' \
http://127.0.0.1:3000/screenshot
Stop the temporary process after testing. Passenger will start the managed instance after registration.
Register the application with cPanel Application Manager
- Open Software → Application Manager.
- Create an application using the domain or subdomain, a base URL, and the source path
nodejsapp(or your chosen directory). - Select the deployment environment, usually production.
- Set environment variables such as
PUPPETEER_EXECUTABLE_PATHif the administrator supplied a system browser path. - Enable npm dependency installation when the interface offers that option.
- Open the base URL and request
/healthz.
Passenger searches for app.js by default. If your entry file is named differently, configure PassengerStartupFile, PassengerAppType node, and PassengerAppRoot in the host’s supported Apache configuration, then rebuild the HTTPD configuration and restart Apache as an administrator. A normal cPanel user generally cannot run those system commands.
Deploy with the Websites hub
Some hosts expose cPanel’s Websites hub or Meridian AI App Hosting instead of classic Application Manager:
- Choose Add Website and select an existing or new domain.
- Choose AI App Hosting.
- Deploy from a Git repository or upload a ZIP.
- In advanced settings, check the Node.js version, package manager, build output directory, and environment variables.
- Deploy and test the assigned domain.
Git deployments support redeploy and rollback. ZIP deployment is intended for an app that will not change often. cPanel documents a limit of four apps per account for this path.
Configure Chromium correctly
Installing Node.js does not install every Linux dependency Chrome needs. Puppeteer’s troubleshooting guidance identifies missing system dependencies as a common Linux launch failure and recommends checking the browser binary with ldd chrome | grep not.
Common required libraries include:
libnss3libgbm1libgtk-3-0libasound2- font packages and related X11 or graphics libraries
On shared hosting, you normally cannot install these packages yourself. Ask the provider to install them or provide a supported Chromium executable. Then set:
export PUPPETEER_EXECUTABLE_PATH=/path/provided/by/host/chrome
Do not add --no-sandbox automatically. Use it only when the host administrator explicitly requires it and understands the isolation trade-off. A browser that launches with weaker isolation can increase the impact of an application compromise.
Restart after code changes
Passenger uses a restart trigger in the application root. After editing code or dependencies, touch tmp/restart.txt:
mkdir -p ~/nodejsapp/tmp
touch ~/nodejsapp/tmp/restart.txt
cPanel’s documented behavior is that tmp/restart.txt directs mod_passenger to restart the app. Touch the file each time changes need to be loaded. Review the application’s log directory, commonly /home/USER/nodejsapp/logs, when the restart does not appear to work.
Useful Puppeteer options
| Need | Example | Operational note |
|---|---|---|
| Wait for a page to settle | waitUntil: 'networkidle2' |
Pages with long polling may never become idle; use a selector or bounded delay instead. |
| Wait for application content | await page.waitForSelector('.content', { timeout: 15000 }) |
Prefer a meaningful selector over an unlimited wait. |
| Full-page image | page.screenshot({ fullPage: true }) |
Very tall pages consume more memory. |
| One element | const el = await page.$('.card'); await el.screenshot({ path: 'card.png' }) |
Handle a missing element explicitly. |
| Mobile viewport | page.setViewport({ width: 390, height: 844, isMobile: true }) |
Set the viewport before navigation when responsive layout matters. |
| Custom headers | page.setExtraHTTPHeaders({ Authorization: 'Bearer …' }) |
Never log secrets or accept arbitrary caller-supplied authorization headers. |
| Cookies | page.setCookie({ name, value, domain }) |
Use a controlled cookie jar and clear it between jobs. |
| Block resources | page.setRequestInterception(true) |
Blocking fonts, scripts, or images can change the rendered result. |
Security and reliability checklist
- Authenticate the screenshot endpoint.
- Allow only
http:andhttps:URLs. - Block requests to localhost, private IP ranges, cloud metadata endpoints, and internal hostnames to reduce SSRF risk.
- Set navigation, selector, and overall job timeouts.
- Close every page, including error paths.
- Limit concurrent pages so the account does not exceed memory or process quotas.
- Reuse a browser process, but recreate it after a crash.
- Keep temporary files outside publicly served directories.
- Redact cookies, authorization headers, and target URLs from logs when they contain sensitive data.
- Use a queue for slow or bursty workloads instead of holding Passenger workers indefinitely.
Performance, limits, and cost
Browser startup is expensive, so a long-lived browser with short-lived pages usually performs better than launching Chrome for every request. Full-page screenshots and pages with many fonts, videos, or third-party scripts use more memory and take longer. Shared hosting can terminate workers that exceed CPU, memory, process, or request-time limits. Measure with the limits your provider actually enforces; there is no single cPanel performance profile.
Passenger workers are still web workers. If a capture can take tens of seconds, a job queue and a status endpoint are safer than keeping the original HTTP request open. Store completed images in private storage and return an authorization-controlled URL.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Node.js option is missing in cPanel | The provider has not enabled Node.js or Passenger. | Ask the host to enable the supported Node.js/Passenger stack or move to a plan that includes it. |
Error: Failed to launch the browser process |
Missing shared libraries, wrong executable path, permissions, or a prohibited browser process. | Run ldd chrome | grep not, verify the provider’s browser path and permissions, and ask whether Chromium is allowed. |
ENOENT for Chrome |
Puppeteer expects a downloaded browser that is not present. | Set PUPPETEER_EXECUTABLE_PATH to the administrator-provided executable or allow the required browser download. |
| Works over SSH but not through the domain | The app is not registered correctly, is using the wrong source path, or Passenger has not restarted. | Check Application Manager settings, confirm app.js, touch tmp/restart.txt, and inspect logs. |
| Port already in use or unexpected port | The app assumes a fixed public port. | Listen on process.env.PORT. Passenger owns external port routing. |
| Changes are ignored | Passenger is serving an old worker. | Touch tmp/restart.txt and wait for the worker to recycle. |
| Navigation times out | The target is slow, blocked, waiting on long polling, or requires authentication. | Use a bounded timeout, choose an appropriate waitUntil, wait for a specific selector, and inspect the target response. |
| Blank or incomplete image | JavaScript content has not rendered, lazy images have not loaded, or the page height is still changing. | Wait for a content selector, scroll or trigger lazy loading, and capture only after layout stabilizes. |
| Process is killed during large captures | Memory or process quotas on shared hosting. | Reduce concurrency, capture an element instead of the full page, limit image size, or use a VPS/managed browser service. |
| Alpine Linux launch failure | Chrome does not support Alpine out of the box. | Use a supported glibc-based environment or complete the additional compatibility work and validate it with the host. |
When shared cPanel hosting is the wrong fit
Choose a VPS or dedicated server when you need to install system libraries, control the Chromium version, run several concurrent browsers, use a queue, or tune memory and process limits. A managed cPanel plan is reasonable for a small, authenticated capture endpoint when the provider explicitly supports headless browser workloads.
Or skip the browser setup
ScreenshotNeo provides a hosted website screenshot API, so your cPanel app can make one request instead of installing Passenger-compatible Chromium dependencies. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for the complete option list. A minimal call is:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Does cPanel run Puppeteer as a normal background Node process?
No. The supported web deployment is a Passenger-managed Node.js application. Passenger starts the app and routes the domain request to it.
Can I choose any public port?
No. Listen on the port in process.env.PORT; Passenger controls the externally routed port.
Why does installing the npm package not fix Chrome?
Puppeteer and Chromium depend on operating-system libraries and permissions that npm cannot provide on a restricted host.
What is the default entry file?
Passenger looks for app.js. A different filename requires Passenger startup-file configuration by the host.
How do I apply a code change?
Touch tmp/restart.txt in the application root and inspect the app logs if the new worker does not start.


