Deploying NestJS to Namecheap Shared Hosting is slightly different from deploying a static website or a PHP application.
NestJS must be compiled from TypeScript to JavaScript, its production dependencies must be installed, and cPanel's Node.js application manager must start it through Passenger. External services such as Redis must also be reachable from the shared server.
I learned those details while deploying a NestJS API with Bull queues, Redis, Swagger, PDF uploads, and a custom domain. This guide collects the complete working process—including the problems that initially left me with an empty deployment folder, missing build files, unavailable routes, and failed Redis connections.
How the Production Setup Works
The final request path looks like this:
Browser or API client
|
v
Domain and HTTPS
|
v
Apache / Passenger on Namecheap
|
v
app.js
|
v
dist/main.js
|
v
NestJS application
You do not need PM2 for this setup. Namecheap's Setup Node.js App interface and Passenger manage the application process.
Namecheap currently lets shared-hosting customers choose the Node.js version, application mode, application root, URL, startup file, and environment variables from cPanel. Its official instructions are available in How to work with Node.js App.
Prerequisites
Before starting, you need:
- A Namecheap Shared Hosting account with Node.js support
- A domain or subdomain assigned to the hosting account
- Access to cPanel
- SSH or cPanel Terminal access
- A working NestJS project
- A supported Node.js version compatible with the project
- Production credentials for any external database, Redis server, or API
Run the application locally before deploying:
npm install
npm run start:dev
Fix local build or startup errors first. Production adds enough moving parts without bringing local errors along for the ride.
1. Prepare NestJS for Production
Listen on the port supplied by the host
Namecheap's Passenger environment controls how requests reach the application. Do not rely exclusively on a hard-coded local port.
In src/main.ts, listen on process.env.PORT with a local fallback:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT || 4000);
}
bootstrap();
The fallback is useful locally. In production, the host-provided PORT value takes priority.
Add production scripts
Your package.json should include at least:
{
"scripts": {
"build": "nest build",
"start:prod": "node dist/main.js"
}
}
Keep runtime packages in dependencies
Packages needed after compilation belong in dependencies, not devDependencies.
This includes packages such as:
@nestjs/common
@nestjs/core
@nestjs/platform-express
reflect-metadata
rxjs
The Nest CLI, TypeScript, test tools, and linters can remain in devDependencies.
This distinction matters because a production-only install may omit devDependencies. The build can exist and still crash immediately if a required Nest package was classified as development-only.
2. Make the Build Produce dist/main.js
Namecheap runs JavaScript, not the TypeScript source directly. Compile the application:
npm run build
The expected entry file for this guide is:
dist/main.js
Use a focused tsconfig.build.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
Restricting the production build to src/ prevents root-level JavaScript files such as app.js from being type-checked and emitted. It also avoids a layout such as dist/src/main.js when the desired output is dist/main.js.
Verify the build:
ls -l dist/main.js
node dist/main.js
If the application starts, stop it with Ctrl+C.
3. Create Passenger's Startup File
Create app.js in the project root:
require('./dist/main');
Your deployment should now resemble:
application-root/
├── app.js
├── package.json
├── package-lock.json
├── dist/
│ └── main.js
└── src/
Passenger conventionally looks for app.js, although cPanel also lets you specify another startup filename. The cPanel documentation likewise recommends app.js as the standard startup file: How to Install a Node.js Application.
The startup bridge is deliberately small. It loads the compiled NestJS application; it does not create a second server.
4. Decide Where to Build
There are two sensible deployment strategies.
Option A: Build locally and upload dist
This is usually safer on shared hosting:
npm ci
npm run build
Upload:
app.jsdist/package.jsonpackage-lock.json
Then install only runtime dependencies on Namecheap:
npm ci --omit=dev
This approach reduces CPU and memory usage on the shared server.
If dist/ is listed in .gitignore, a Git-only deployment will not send it. Use an artifact upload, SFTP, rsync, or another deployment step that deliberately transfers the build output.
Option B: Build on Namecheap
Upload the source and install all build dependencies:
npm ci --include=dev
npm run build
npm prune --omit=dev
This keeps dist/ out of source control but requires enough server resources to run the Nest compiler.
Choose one strategy and make it explicit. A common failure is ignoring dist/, not building on the server, and then asking Passenger to load a file that does not exist.
5. Upload the Project
You can use:
- cPanel File Manager
- SFTP
- rsync over SSH
- cPanel Git Version Control
- A custom Git deployment remote
Do not upload:
- Your local
node_modules/ - Local logs
- Test coverage
- Editor configuration
- Development secrets
Install dependencies for the Node version selected in cPanel instead of copying node_modules from your computer. Native modules compiled on macOS, for example, may not run on the Linux hosting server.
If Git creates only a .git directory
If the remote folder contains .git but no application files, the pushed branch may not be the branch checked out by the server repository.
For example, the server may initialize master while you push main.
On the server:
cd ~/your-application-root
git branch -a
git checkout -f main
Replace main with the branch you actually pushed.
Renaming a local branch can also align the two sides:
git branch -m main master
The important detail is not whether the branch is called main or master; it is that the server worktree checks out the deployed branch.
6. Create the Node.js Application in cPanel
Open:
Namecheap Dashboard
→ Hosting List
→ Go to cPanel
→ Software
→ Setup Node.js App
→ Create application
Choose:
- Node.js version: Match the version supported by your project
- Application mode: Production
-
Application root: The folder containing
app.js - Application URL: Your domain or subdomain
-
Application startup file:
app.js
The application root must point to the directory that contains app.js, package.json, and dist/. Do not point it directly at dist.
Create the application.
7. Install Dependencies in the Correct Environment
The Node.js application page shows a command for entering its virtual environment through SSH. Copy and run that command exactly.
Then enter the application root:
cd ~/your-application-root
Install dependencies:
npm ci --omit=dev
If you chose to build on the server instead:
npm ci --include=dev
npm run build
npm prune --omit=dev
You can also use the Run NPM Install button in the cPanel Node.js application interface. Namecheap documents both the virtual-environment command and the cPanel install action in its Node.js App guide.
8. Configure Environment Variables
In Setup Node.js App, edit the application and add the required variables:
NODE_ENV=production
OPENAI_API_KEY=...
REDIS_URL=...
DATABASE_URL=...
Only add variables your application actually uses.
You may also use a .env file in the application root:
application-root/
├── .env
├── app.js
├── package.json
└── dist/
Do not put .env inside dist.
Protect it with .gitignore:
.env
.env.*
!.env.example
Do not put real credentials in .env.example. If a secret has ever been committed or published, removing it from the latest file is not enough—rotate the credential.
When a variable exists in both cPanel and .env, the existing process environment normally takes precedence. Make sure an old cPanel value is not overriding the current .env value.
Restart the application after changing environment variables.
9. Start or Restart the App
Return to:
cPanel → Setup Node.js App
Use Start App or Restart.
Do not keep the application alive by running npm run start:prod in an SSH terminal. The process will be disconnected from the hosting lifecycle, may conflict with Passenger, and can produce EADDRINUSE errors.
Passenger should own the production process.
For environments using a direct Passenger configuration, cPanel also documents the tmp/restart.txt mechanism. On Namecheap's managed interface, the restart button is simpler.
10. Test the Deployment
Start with a simple GET route:
https://api.example.com/v1
If you use NestJS URI versioning:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
then controller routes receive the /v1 prefix.
For example:
@Controller('api/parse-bank-statement')
export class PdfParserController {
@Get('job/:jobId')
getJobStatus() {}
}
becomes:
GET /v1/api/parse-bank-statement/job/:jobId
Test from a terminal:
curl -i https://api.example.com/v1
A NestJS JSON response—even a NestJS-formatted 404—proves that the request reached the application. It does not necessarily mean the proxy or deployment failed.
11. Understand Why Swagger May Return 404
Many applications intentionally disable Swagger in production:
if (process.env.NODE_ENV !== 'production') {
setupSwaggerDocs(app);
}
In that case:
GET /docs
returns:
{
"message": "Cannot GET /docs",
"error": "Not Found",
"statusCode": 404
}
That response is expected. The NestJS application is running, but the documentation route was never registered.
Do not use /docs as your only health check when production configuration disables it.
12. Read Namecheap Production Logs
Open the Passenger log configured for the application. It is often named stderr.log.
Display recent errors:
tail -n 100 stderr.log
Follow new entries:
tail -f stderr.log
Press Ctrl+C to stop following.
Find other logs:
find . -maxdepth 3 -type f -name "*.log"
Also check:
cPanel → Metrics → Errors
Namecheap says this interface displays the latest web-server error entries: cPanel control panel overview.
13. Connect External Services Carefully
A working HTTP route does not prove that Redis, a database, or another API is reachable.
For Redis-backed Bull queues, test the TCP port from Namecheap:
nc -vz <redis-host> <redis-port>
Then send a real Redis PING:
node -r dotenv/config -e 'const Redis=require("ioredis"); const r=new Redis(process.env.REDIS_URL,{connectTimeout:5000,maxRetriesPerRequest:1,retryStrategy:()=>null}); r.on("error",()=>{}); r.ping().then(console.log).catch(e=>console.error(e.code,e.message)).finally(()=>r.disconnect())'
Success returns:
PONG
If Redis works locally but Namecheap reports ECONNREFUSED or ETIMEDOUT, check:
- The Redis server's firewall
- Cloud security-group rules
- Redis source-IP allowlists
- Whether Namecheap permits the outbound destination port
In my deployment, Namecheap Support had to permit the Redis port before the production application could connect.
For the full Redis setup and debugging process, see How to Connect Redis to a Node.js App on Namecheap Shared Hosting.
14. Await Important Asynchronous Operations
Production networking exposes bugs that local development may hide.
For example, do not queue a job without awaiting the promise:
this.processPdfQueue.add(jobData);
Use:
const job = await this.processPdfQueue.add(jobData);
Without await, the controller can return “queued successfully” before Redis has accepted the job. A later unhandled rejection can also terminate the Node.js process.
Common Deployment Errors
| Error or symptom | Likely cause | What to check |
|---|---|---|
Only .git appears on the server |
The pushed branch is not checked out | Run git branch -a and check out the deployed branch |
Cannot find module './dist/main' |
Missing build or wrong output layout | Run the build and verify dist/main.js
|
EADDRINUSE |
Another process already uses the fallback port | Stop the manual process and let Passenger manage the app |
Cannot GET /docs |
Swagger is disabled in production | Test a real API or health route |
| NestJS-formatted 404 | The app is running, but that route does not exist | Check version prefixes and controller paths |
MaxRetriesPerRequestError |
Redis requests repeatedly failed | Find the underlying connection error |
ECONNREFUSED to an external service |
Port closed, service stopped, or connection rejected | Test with nc; inspect firewalls and hosting rules |
| Environment variable appears stale | cPanel value overrides .env, or app was not restarted |
Check both locations and restart |
| App works in terminal but not through domain | cPanel application root, URL, or startup file is wrong | Verify the Node.js App configuration |
| App crashes after production-only install | Runtime package is in devDependencies
|
Move runtime packages to dependencies
|
Production Checklist
Before considering the deployment complete, verify:
- The application builds locally
-
dist/main.jsexists - Root-level
app.jsloads./dist/main - Runtime Nest packages are in
dependencies -
node_moduleswas installed on the server, not copied from macOS or Windows - The cPanel application root contains
app.js - The cPanel startup file is
app.js - The selected Node.js version matches the project
-
NODE_ENVis set correctly - Secrets are not committed to Git
- The app was restarted after environment changes
- A simple GET route works through HTTPS
- Expected version prefixes such as
/v1are included - Production logs are accessible
- External databases and Redis are reachable from Namecheap
- Important asynchronous operations are awaited
Final Takeaway
Running NestJS on Namecheap Shared Hosting is entirely workable once the responsibilities are clear:
- Nest compiles the TypeScript application into
dist. -
app.jsgives Passenger a predictable startup file. - cPanel selects the Node version, root directory, URL, mode, and environment.
- Passenger owns the process and production port.
- Your deployment method must transfer or create
dist. - External services must accept connections from the Namecheap server.
-
stderr.logtells you what happened when the browser only shows a generic error.
Most of the difficult failures were not mysterious NestJS bugs. They were boundary problems between the build, Git worktree, Passenger configuration, environment variables, and network access. Test those boundaries one at a time, and the deployment becomes much easier to reason about.
Top comments (0)