CloudPloy

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:

  1. npm ci --omit=dev to install production dependencies
  2. The start script from package.json to 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.