The Final Step: Getting It Live
You've built it. Now let's ship it. In this final post we deploy our pure Node.js app to a Ubuntu VPS using systemd (to keep it running) and nginx (as a reverse proxy for HTTPS and static file serving).
You'll need: A Ubuntu 22.04+ VPS (DigitalOcean, Linode, Hetzner, or any provider), a domain name pointed to your VPS IP, and SSH access.
Step 1: Server Setup
# SSH into your VPS
ssh root@your-server-ip
# Update system
apt update && apt upgrade -y
# Install Node.js via NodeSource (LTS)
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
# Install nginx
apt install -y nginx
# Create a dedicated user (don't run Node as root!)
useradd -m -s /bin/bash nodeapp
mkdir -p /home/nodeapp/app
chown nodeapp:nodeapp /home/nodeapp/app
Step 2: Deploy Your Code
# From your local machine, copy the app
rsync -avz --exclude 'node_modules' --exclude '.git' \
./my-app/ root@your-server-ip:/home/nodeapp/app/
# Or use git on the server
ssh root@your-server-ip
su - nodeapp
cd /home/nodeapp/app
git clone https://github.com/yourusername/your-app.git .
# No npm install needed — zero dependencies!
# Just verify the app runs
node server.js
# Ctrl+C to stop — we'll use systemd next
Step 3: Add Graceful Shutdown to Your Server
Before deploying, add graceful shutdown handling so active requests complete before the server exits:
// server.js — add this at the bottom
// Health check endpoint for monitoring
server.on('request', (req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
}));
}
});
// Graceful shutdown
let isShuttingDown = false;
function shutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`Received ${signal}. Graceful shutdown...`);
server.close(() => {
console.log('All connections closed. Exiting.');
process.exit(0);
});
// Force exit after 10 seconds if connections don't close
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10_000);
}
process.on('SIGTERM', () => shutdown('SIGTERM')); // systemd stop
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C
Step 4: systemd Service
systemd keeps your app running after crashes and restarts it on server reboot:
# Create the service file
nano /etc/systemd/system/nodeapp.service
[Unit]
Description=Pure Node.js App
After=network.target
[Service]
Type=simple
User=nodeapp
WorkingDirectory=/home/nodeapp/app
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
# Environment variables
Environment=NODE_ENV=production
Environment=PORT=3000
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodeapp
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
# Enable and start the service
systemctl daemon-reload
systemctl enable nodeapp
systemctl start nodeapp
# Check status
systemctl status nodeapp
# View logs live
journalctl -u nodeapp -f
Step 5: nginx Reverse Proxy
nginx sits in front of Node.js, handles HTTPS, and serves static files efficiently:
# /etc/nginx/sites-available/nodeapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL certificates (from Let's Encrypt — see below)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Serve static files directly (bypasses Node.js = faster)
location /static/ {
alias /home/nodeapp/app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /home/nodeapp/app/uploads/;
expires 30d;
}
# Proxy everything else to Node.js
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; # for WebSockets
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# Enable site
ln -s /etc/nginx/sites-available/nodeapp /etc/nginx/sites-enabled/
nginx -t # test config
systemctl reload nginx
# Get free HTTPS cert from Let's Encrypt
apt install -y certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com -d www.yourdomain.com
Step 6: Zero-Downtime Deployment
# deploy.sh — run this on your local machine
#!/bin/bash
set -e
echo "Deploying to production..."
# Push code to server
rsync -avz --exclude 'node_modules' --exclude '.git' --exclude 'data' \
./ nodeapp@your-server-ip:/home/nodeapp/app/
# Restart gracefully via systemd
# systemd sends SIGTERM → our shutdown handler closes connections → exits
ssh root@your-server-ip "systemctl restart nodeapp"
# Wait and verify
sleep 2
curl -sf https://yourdomain.com/health | jq .
echo "Deploy complete!"
Environment Variables
// In your app, always use process.env
const PORT = parseInt(process.env.PORT) || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
const SECRET_KEY = process.env.SECRET_KEY; // for session signing
if (!SECRET_KEY) {
console.error('SECRET_KEY env var is required');
process.exit(1);
}
# Add to systemd service, or create a .env file
# /home/nodeapp/app/.env (don't commit this to git!)
SECRET_KEY=your-super-secret-key-here
DATABASE_URL=...
# Load in systemd:
EnvironmentFile=/home/nodeapp/app/.env
Key takeaways:
- Never run Node.js as root — create a dedicated system user
- systemd's
Restart=on-failureauto-restarts your app on crashes - Graceful shutdown: handle
SIGTERM, finish active requests, then exit - nginx in front of Node handles HTTPS, rate limiting, and static files efficiently
- Use
proxy_set_header Upgradefor WebSocket proxying /healthendpoint for monitoring and deployment verification
🎉 Series Complete!
You've built four full web apps in pure Node.js — a Todo App, Dashboard, Real-time Chat, and Mini Store — with zero npm packages. You now understand exactly what Express, Socket.io, body-parser, multer, and express-session do under the hood. That's the foundation of a great engineer.
What You've Learned Across All 12 Posts
- HTTP server lifecycle, event loop, request/response cycle
- URL routing, pattern matching, URL parameters
- Serving static files with MIME types and caching headers
- Parsing form data and JSON request bodies from scratch
- File-based CRUD with atomic writes and UUID generation
- HTML templating with XSS protection via string escaping
- Vanilla JS, Fetch API, and HTML5 Canvas charts
- WebSocket protocol — TCP upgrade, frame parsing, SHA-1 handshake
- Real-time chat with rooms, broadcast, and client tracking
- Sessions and cookies — Set-Cookie, parsing, expiry
- Multipart file uploads, MIME validation, stream-based serving
- Production deployment: systemd + nginx + graceful shutdown + HTTPS