Building Real-Time Node.js Apps on DreamHost

A practical guide based on real deployment experience, with all the gotchas and solutions.

Prerequisites

  • DreamHost Node.js Stack4 VPS
  • SSH access (using Kitty, Terminal, or similar)
  • Basic familiarity with Node.js and npm

Your VPS Info:

Node.js: v24.19.0
NPM: 11.17.0
PM2: 7.0.3
Web: https://node.example.com (adjust to your domain)

Part 1: Setup & Critical First Steps

1.1 SSH Into Your VPS

Using Kitty:

ssh debian@your-vps-ip

You should land in /home/debian/.

1.2 The One Rule You’ll Break Otherwise

⚠️ CRITICAL: Always deploy apps to /opt/nodejs-sample/

This is not a suggestion. nginx is hardcoded to proxy to /opt/nodejs-sample/server.js on port 3000. If you create your app anywhere else, you’ll waste hours debugging.

cd /opt/nodejs-sample

1.3 Fix Directory Permissions Immediately

The directory might be owned by root. Fix this before you do anything:

sudo chown debian:debian /opt/nodejs-sample
ls -la /opt/nodejs-sample  # should show "debian debian"

If you skip this: Every cat > and npm install will fail with “Permission denied”.

Part 2: Creating Your App (The Right Way)

2.1 Create package.json

cat > package.json << 'EOF'
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "socket.io": "^4.5.4"
  }
}
EOF

Verify:

cat package.json

2.2 Create server.js

cat > server.js << 'EOF'
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: { origin: "*" }
});

app.use(express.static('public'));

// Your app logic here

const PORT = process.env.PORT || 3000;  // ⚠️ DO NOT CHANGE THIS
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
EOF

Don’t change the port to 8000 or 5000. nginx expects 3000. You’ll learn this the hard way.

2.3 Create the public/ directory and index.html

mkdir public
cat > public/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <h1>Hello World</h1>
</body>
</html>
EOF

2.4 Install Dependencies

npm install

This creates node_modules/ and package-lock.json.

2.5 Verify Structure

ls -la

You should see:

-rw-rw-r-- package.json
-rw-rw-r-- server.js
drwxrwxr-x node_modules/
drwxrwxr-x public/

Part 3: Launching with PM2

3.1 Start Your App

pm2 start server.js --name "app"
pm2 save

Important naming: Use --name "app" exactly. The system might be looking for this.

3.2 Check It’s Running

pm2 list

Status should be online, not errored.

3.3 View Logs

pm2 logs app

You should see: Server running on port 3000

3.4 Test Locally

curl http://localhost:3000

You should get HTML back.

Part 4: Debugging Gotchas

Problem: “EADDRINUSE – port 3000 is already in use”

Cause: Old Node process running as root from a previous deployment.

Solution:

ps aux | grep node

If you see a process, kill it:

sudo kill -9 <PID>

Or nuke everything:

pm2 kill
sudo killall -9 node
pm2 start server.js --name "app"

Problem: “Permission denied” when creating files

Cause: Directory owned by root.

Solution:

sudo chown debian:debian /opt/nodejs-sample

Problem: Nginx still shows old sample app

Cause: nginx needs to restart, or your app isn’t in the right place.

Solution:

sudo systemctl restart nginx

Then visit https://node.example.com/ (use HTTPS, not HTTP).

Problem: App keeps restarting (↺ shows high numbers)

Cause: Usually port conflict or missing dependencies.

Check logs:

pm2 logs app --err

Look for EADDRINUSE or Cannot find module.

Part 5: File Transfer (FTP/SFTP)

DreamHost doesn’t support traditional FTP on Node.js stacks, but SFTP works perfectly with the same SSH credentials.

Using Kitty + SFTP

Kitty doesn’t have built-in SFTP, but you can:

  1. Use scp from your local machine: scp -r ~/my-app debian@your-vps-ip:/opt/nodejs-sample/
  2. Use an SFTP client (WinSCP, Cyberduck, FileZilla, etc.):
    • Host: your-vps-ip
    • Port: 22
    • Username: debian
    • Password: your SSH password
    • Path: /opt/nodejs-sample/
  3. Use rsync (best for repeated syncs): rsync -avz ~/my-app/ debian@your-vps-ip:/opt/nodejs-sample/

Quick Edit Loop

For rapid development:

# Edit locally, then sync
rsync -avz ~/my-app/ debian@your-vps-ip:/opt/nodejs-sample/

# On the VPS
pm2 restart app
pm2 logs app

Part 6: Real-Time Apps (Drawing, Element Movement, etc.)

The Good News

Socket.io works reliably for real-time interactions. Our latency measurements showed ~15-25ms for most users, which is perfectly usable.

The Reality Check

Jumpy/laggy? Only if you do it wrong. Here’s what determines smoothness:

FactorImpactSolution
Latency15-50ms is fineThis is usually good on shared networks
Update frequencySending 60/sec per user is expensiveSend only changes, not state
InterpolationWithout it, movement is “jumpy”Predict position between updates
Server load100 users = high CPUOptimize socket handlers

Example: Smooth Drawing App

// Client-side: Send only new points, not redraw
canvas.addEventListener('mousemove', (e) => {
  if (isDrawing) {
    socket.emit('draw', {
      x: e.offsetX,
      y: e.offsetY,
      color: currentColor
    });
  }
});

// Server-side: Broadcast to others
socket.on('draw', (data) => {
  socket.broadcast.emit('draw', data);
});

// Other clients: Draw immediately (no waiting for round-trip)
socket.on('draw', (data) => {
  ctx.fillStyle = data.color;
  ctx.fillRect(data.x - 2, data.y - 2, 4, 4);
});

Example: Smooth Element Movement

// Client sends position + velocity
socket.emit('move', {
  x: element.x,
  y: element.y,
  vx: velocityX,
  vy: velocityY
});

// Other clients interpolate between last known position
// and next position (predicted)
function interpolate() {
  element.x += element.vx * deltaTime;
  element.y += element.vy * deltaTime;
}

// When new position arrives, smoothly transition
socket.on('userMove', (data) => {
  tweenTo(element, data, 100); // animate over 100ms
});

Realistic Expectations

Drawing app with 5 users: Smooth, ~100ms latency feels instant.

Element movement with 20 users: Smooth if you interpolate; skippy if you don’t.

Real-time collaborative editing (10+ users): Needs operational transformation (OT) or CRDTs, not just socket.io.

When It Gets Laggy

  1. Too many events per second: Throttle updates to 30-60fps
  2. Broadcasting to everyone: Use rooms (socket.join('room'))
  3. Large JSON payloads: Send diffs, not full state
  4. No interpolation: Predicted positions feel smoother than waiting

Part 7: Production Checklist

Before you tell people about your app:

  • App runs on /opt/nodejs-sample/server.js
  • App listens on port 3000
  • pm2 save is run (survives reboots)
  • Logs look clean (pm2 logs app --err shows nothing)
  • Tested on HTTPS (not HTTP)
  • Tested with 2+ users simultaneously
  • No sensitive data in environment (use .env if needed)

Part 8: Common Tasks

Restart Your App

pm2 restart app
pm2 save

View Live Logs

pm2 logs app

Stop Your App (doesn’t delete it)

pm2 stop app

Delete Your App

pm2 delete app
pm2 save

Update Code and Restart

# Edit files (via SFTP, rsync, or SSH vim)
# Then restart:
pm2 restart app
pm2 logs app

Add a Custom Domain

sudo set-domain your-custom-domain.com
sudo certbot --nginx  # for SSL renewal

Part 9: Performance Tips

For Real-Time Apps

  1. Use rooms to limit broadcasts: socket.join('game-room-1'); socket.broadcast.to('game-room-1').emit('move', data);
  2. Throttle events: let lastEmit = 0; document.addEventListener('mousemove', (e) => { if (Date.now() - lastEmit > 16) { // 60fps max socket.emit('move', {x: e.x, y: e.y}); lastEmit = Date.now(); } });
  3. Store minimal state: const users = new Map(); // not an array users.set(socketId, { name, x, y }); // only essentials
  4. Use binary data for large payloads: // Instead of JSON strings, use Uint8Array for coordinates socket.emit('move', new Float32Array([x, y, vx, vy]));

Part 10: Troubleshooting Checklist

IssueFirst CheckFix
App not accessiblecurl http://localhost:3000Is it running? pm2 list
Still showing old appCheck /opt/nodejs-sample/server.jsIs your code there?
Port 3000 in useps aux | grep nodeKill old process
npm install failsls -la /opt/nodejs-sampleFix permissions: sudo chown debian:debian
Socket.io not connectingBrowser console errors?Check CORS: cors: { origin: "*" }
High latencyping your-vps-ipNormal on shared networks; optimize code

Summary: The Fast Path (Copy-Paste)

# 1. Connect
ssh debian@your-vps-ip
cd /opt/nodejs-sample

# 2. Fix permissions
sudo chown debian:debian /opt/nodejs-sample

# 3. Create app
cat > package.json << 'EOF'
{"name":"app","version":"1.0.0","main":"server.js","dependencies":{"express":"^4.18.2","socket.io":"^4.5.4"}}
EOF

mkdir public
cat > public/index.html << 'EOF'
<!DOCTYPE html><html><body><h1>Hello</h1><script src="/socket.io/socket.io.js"></script></body></html>
EOF

cat > server.js << 'EOF'
const express = require('express');
const http = require('http');
const io = require('socket.io');
const app = express();
const server = http.createServer(app);
const socketIo = io(server, {cors: {origin: "*"}});
app.use(express.static('public'));
socketIo.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  socket.on('disconnect', () => console.log('User left'));
});
server.listen(3000, () => console.log('Server running on port 3000'));
EOF

# 4. Deploy
npm install
pm2 start server.js --name "app"
pm2 save

# 5. Check
pm2 logs app

Visit: https://node.example.com/

Resources

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *