Node.js on CloudPloy - Complete Guide
CloudPloy supports Node.js applications alongside its PHP offerings. Whether you're running an Express API, a Next.js server-side rendered frontend, a real-time application using WebSockets, or a background processing service, CloudPloy deploys your Node.js app in a Docker container with automatic health checks, zero-downtime deployments, and persistent storage for uploads and data.
How CloudPloy Detects Node.js Apps
When you connect a repository, CloudPloy checks for package.json in the root. If found and no PHP framework markers are present, it classifies the app as Node.js and uses the cloudploy/node:20 base image (Node.js 20 LTS with npm and yarn pre-installed).
CloudPloy then runs:
npm ci --omit=devto install production dependencies- The
startscript frompackage.jsonto launch the app
Your package.json must have a start script:
{
"scripts": {
"start": "node server.js",
"build": "tsc -p tsconfig.json"
}
} Dockerfile for Node.js
For production deployments, a custom Dockerfile gives you control over the build process - especially useful for TypeScript compilation, Next.js builds, or apps with native dependencies.
Standard production Dockerfile for Express/Node.js:
FROM cloudploy/node:20
WORKDIR /app
# Install dependencies (cached layer if package.json unchanged)
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Copy application
COPY . .
# Expose the port your app listens on
EXPOSE 3000
# Start the application
CMD ["node", "server.js"] For TypeScript apps:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
# Stage 2: Production
FROM cloudploy/node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"] For Next.js with standalone output:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"] Enable standalone output in next.config.js:
module.exports = {
output: 'standalone',
} Environment Variables
Configure all environment variables in App > Settings > Environment Variables. They are available as process.env.VARIABLE_NAME at runtime.
| Variable | Production Value | Notes |
|---|---|---|
NODE_ENV | production | Enables production mode for most frameworks |
PORT | 3000 | CloudPloy routes traffic to this port via Nginx |
HOST | 0.0.0.0 | Bind to all interfaces so Nginx can reach the process |
DATABASE_URL | From CloudPloy database panel | Full connection string including credentials |
REDIS_URL | From CloudPloy Redis panel | Required for session stores and caching |
SECRET_KEY | Random 32+ byte string | For JWT signing, CSRF tokens, session secrets |
LOG_LEVEL | error or warn | Reduces log noise in production |
Never hardcode environment variables in your Dockerfile or source code. Use process.env everywhere and let CloudPloy inject the values at runtime.
Process Management
Node.js runs single-threaded by default. In production, a single unhandled exception can crash your entire app. CloudPloy's container restarts handle this at the infrastructure level, but you can also make Node.js more resilient at the application level.
Option 1: Let CloudPloy Handle It
CloudPloy monitors your container's health check endpoint every 30 seconds. If the container crashes or the health check fails three times, CloudPloy automatically restarts the container. For many apps this is sufficient - typical restart time is under 5 seconds.
Option 2: Node.js Cluster Module
Use Node's built-in cluster module to spawn one worker per CPU core. If one worker crashes, the master process restarts it without affecting the others:
import cluster from 'cluster';
import os from 'os';
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.error(`Worker ${worker.process.pid} died, restarting`);
cluster.fork();
});
} else {
// Your Express/HTTP server starts here
const app = createApp();
app.listen(process.env.PORT || 3000);
} Option 3: PM2
PM2 is a process manager that handles clustering, restarts, and logging. Install it as a dependency and use it as the container entry point:
# package.json
"scripts": {
"start": "pm2-runtime start ecosystem.config.js"
} // ecosystem.config.js
module.exports = {
apps: [{
name: 'app',
script: 'dist/server.js',
instances: 'max', // One per CPU
exec_mode: 'cluster',
max_memory_restart: '500M',
env: {
NODE_ENV: 'production'
}
}]
} Use pm2-runtime (not pm2 start) in containers - it runs in the foreground and passes signals to workers correctly so Docker can stop the container cleanly.
Health Checks
Add a health check endpoint to your application. CloudPloy polls it every 30 seconds:
// Express
app.get('/health', async (req, res) => {
try {
// Check database connection
await db.query('SELECT 1');
res.json({ status: 'ok', uptime: process.uptime() });
} catch (err) {
res.status(503).json({ status: 'error', message: err.message });
}
}); Configure the health check path in App > Settings > Health Check to /health.
Optionally declare it in your Dockerfile:
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1 WebSocket Support
CloudPloy supports WebSocket connections. No special configuration is needed - Nginx is configured to proxy WebSocket upgrade requests to your container. Ensure your app listens for upgrades on the same port as HTTP:
import http from 'http';
import { WebSocketServer } from 'ws';
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// Handle incoming messages
});
});
server.listen(process.env.PORT || 3000); For Socket.io, use the same pattern - create the HTTP server first, then attach Socket.io to it, and listen on process.env.PORT.
Static File Serving
For apps that serve static files (single-page apps, Next.js public directory), configure Express to serve them efficiently:
import express from 'express';
import path from 'path';
const app = express();
// Serve static files with cache headers
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1y', // 1 year for fingerprinted assets
etag: true,
lastModified: true,
}));
// SPA fallback - serve index.html for unknown routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
}); Alternatively, use CloudPloy's Nginx to serve static files directly without involving the Node.js process. Configure a static directory in App > Settings > Static Files.
Persistent Storage
Node.js containers are stateless - files written inside the container are lost on deployment. For files that need to persist (user uploads, generated exports, SQLite databases), add volume mounts in App > Settings > Volumes.
Common paths to mount:
| Directory | Contents |
|---|---|
/app/uploads | User-uploaded files |
/app/data | SQLite databases, local data files |
/app/logs | Application log files (prefer stderr instead) |
/app/.cache | Build caches that should persist between deployments |
For production file uploads, prefer S3-compatible storage (AWS S3, Cloudflare R2, DigitalOcean Spaces) over local volumes. This decouples your app from the server and enables horizontal scaling.
Logging
Write logs to stdout and stderr. CloudPloy captures these and displays them in App > Logs. Avoid writing log files inside the container - they do not rotate and fill the overlay filesystem.
// Using pino (fast structured logging)
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// No file transport - pino defaults to stdout
});
// Writes JSON to stdout - searchable in CloudPloy Logs
logger.info({ userId: 123, action: 'login' }, 'User logged in'); Common Issues
| Problem | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Node.js not listening on the correct port | Ensure app.listen(process.env.PORT || 3000) and PORT=3000 env var is set |
| App starts then crashes immediately | Missing required environment variable | Check App > Logs for "Cannot read properties of undefined" - add the missing env var |
| WebSocket connections dropping | Nginx proxy timeout | Contact CloudPloy support to increase proxy_read_timeout for your app |
| npm install fails during build | package-lock.json out of sync | Run npm install locally to regenerate lock file and commit it |
| Native module compilation fails | Missing build tools in base image | Add RUN apk add --no-cache python3 make g++ before npm ci in Dockerfile |
| High memory usage | Memory leaks or missing max_old_space_size limit | Set NODE_OPTIONS=--max-old-space-size=512 and add PM2 memory restart |
| Slow cold starts | Large node_modules or missing build caching | Use multi-stage builds and Docker layer caching (copy package.json before source) |
Node.js Guides
Detailed step-by-step guides for Node.js on CloudPloy:
- Node.js Deployment Guide - Connect your repository, configure the start command, set environment variables, and deploy your first Node.js app end to end.
- PM2 Process Manager Setup - Configure PM2 for production, set up cluster mode for multi-core utilization, configure memory limits, and view PM2 logs.
- Node.js Performance Optimization - Profiling with the Node.js built-in profiler, identifying memory leaks, configuring connection pools, and measuring improvements.
Questions about your Node.js app? Check App > Logs for startup errors, use App > Terminal to run commands inside the container, or contact CloudPloy support.