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:
- Use scp from your local machine:
scp -r ~/my-app debian@your-vps-ip:/opt/nodejs-sample/ - Use an SFTP client (WinSCP, Cyberduck, FileZilla, etc.):
- Host: your-vps-ip
- Port: 22
- Username: debian
- Password: your SSH password
- Path:
/opt/nodejs-sample/
- 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:
| Factor | Impact | Solution |
|---|---|---|
| Latency | 15-50ms is fine | This is usually good on shared networks |
| Update frequency | Sending 60/sec per user is expensive | Send only changes, not state |
| Interpolation | Without it, movement is “jumpy” | Predict position between updates |
| Server load | 100 users = high CPU | Optimize 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
- Too many events per second: Throttle updates to 30-60fps
- Broadcasting to everyone: Use rooms (
socket.join('room')) - Large JSON payloads: Send diffs, not full state
- 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 saveis run (survives reboots)- Logs look clean (
pm2 logs app --errshows nothing) - Tested on HTTPS (not HTTP)
- Tested with 2+ users simultaneously
- No sensitive data in environment (use
.envif 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
- Use rooms to limit broadcasts:
socket.join('game-room-1'); socket.broadcast.to('game-room-1').emit('move', data); - 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(); } }); - Store minimal state:
const users = new Map(); // not an array users.set(socketId, { name, x, y }); // only essentials - 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
| Issue | First Check | Fix |
|---|---|---|
| App not accessible | curl http://localhost:3000 | Is it running? pm2 list |
| Still showing old app | Check /opt/nodejs-sample/server.js | Is your code there? |
| Port 3000 in use | ps aux | grep node | Kill old process |
| npm install fails | ls -la /opt/nodejs-sample | Fix permissions: sudo chown debian:debian |
| Socket.io not connecting | Browser console errors? | Check CORS: cors: { origin: "*" } |
| High latency | ping your-vps-ip | Normal 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
- Socket.io Docs: https://socket.io/docs/
- Express Docs: https://expressjs.com/
- PM2 Docs: https://pm2.keymetrics.io/
- DreamHost Node.js: https://help.dreamhost.com/hc/en-us/articles/360041889551