← Back to Blog
Node.jsTutorial

Hosting Node.js Apps on Bare Metal: Express, Next.js, Fastify

Node.js runs everywhere — but running it well in production requires more than node server.js. This guide covers deploying Express, Next.js, and Fastify on a bare metal server with PM2 process management, Nginx reverse proxy, SSL, and systemd — all for $6/mo on RAW.

Why Bare Metal for Node.js

Node.js is single-threaded by default. On shared cloud VMs, your event loop competes with other tenants for CPU time. On bare metal, your Node process gets dedicated cores with consistent latency. No cold starts, no noisy neighbors, no CPU throttling at the worst possible moment.

A RAW raw-2 ARM server gives you 2 dedicated cores, 4 GB RAM, and 40 GB NVMe for $6/mo. That is enough to run multiple Node.js apps behind Nginx with room to spare.

Step 1: Provision and Install Node.js

# Deploy a RAW server
npx rawhq deploy

# SSH in and install Node.js 22 LTS
ssh root@your-server-ip
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs

# Verify
node -v   # v22.x
npm -v    # 10.x

Step 2: Deploy Your Application

Clone your repo, install dependencies, and build:

# Clone and install
cd /opt
git clone https://github.com/you/your-app.git app
cd app
npm ci --production

# For Next.js, build first
npm run build

Step 3: PM2 Process Manager

PM2 keeps your Node.js app running, restarts on crash, and manages logs. It is the industry standard for Node.js process management on Linux.

# Install PM2 globally
npm install -g pm2

# Start an Express or Fastify app
pm2 start server.js --name my-api -i max

# Start a Next.js production server
pm2 start npm --name my-nextjs -- start

# Save the process list and enable startup
pm2 save
pm2 startup systemd

The -i max flag enables cluster mode, spawning one worker per CPU core. On a 2-core RAW server, that gives you two workers handling requests in parallel — doubling throughput for CPU-bound workloads.

Key PM2 Commands

  • pm2 list — show all running processes
  • pm2 logs — stream logs from all apps
  • pm2 monit — real-time CPU and memory dashboard
  • pm2 reload my-api — zero-downtime restart
  • pm2 delete my-api — stop and remove a process

Step 4: Nginx Reverse Proxy

Nginx sits in front of Node.js to handle SSL termination, static files, gzip compression, and load balancing. Node.js should never face the internet directly.

# Install Nginx
apt install -y nginx

# Create site config
cat > /etc/nginx/sites-available/my-app << 'EOF'
server {
    listen 80;
    server_name yourdomain.com;

    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';
        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;
    }
}
EOF

# Enable and reload
ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 5: SSL with Certbot

# Install Certbot
apt install -y certbot python3-certbot-nginx

# Obtain and auto-configure SSL
certbot --nginx -d yourdomain.com

# Auto-renewal is enabled by default
certbot renew --dry-run

Certbot modifies your Nginx config to redirect HTTP to HTTPS and installs the certificate. Renewals happen automatically via systemd timer.

Step 6: Systemd Integration

PM2 handles process management, but systemd ensures PM2 itself starts on boot. The pm2 startup systemd command from Step 3 creates the service file automatically. Verify it works:

# Reboot and verify everything comes back
reboot

# After reconnecting
pm2 list   # all processes should be online

Framework-Specific Notes

Express

Express apps typically listen on a port and are production-ready as-is. Use pm2 start server.js -i max for cluster mode. Set NODE_ENV=production to disable verbose error pages and enable caching.

Next.js

Run npm run build first, then pm2 start npm --name nextjs -- start. Next.js standalone output mode reduces the deployment footprint — set output: 'standalone' in next.config.js to generate a self-contained server.

Fastify

Fastify is already optimized for throughput. Use pm2 start server.js -i max. Ensure you listen on 0.0.0.0 (not localhost) if running behind Nginx on the same machine — or use 127.0.0.1 to restrict direct access.

Cost Comparison: Bare Metal vs PaaS

PlatformSpecsCost/mo
RAW (raw-2 ARM)2 CPU, 4 GB, 40 GB NVMe$6
Vercel ProServerless, 1 TB bandwidth$20/user
RailwayShared vCPU, usage-based$5 + usage
Heroku Basic1 dyno, 512 MB RAM$7
Heroku Standard1 dyno, 512 MB, metrics$25

Vercel works great for hobby projects but hits $20/user/mo the moment you need a team. Railway and Heroku charge per-usage, making costs unpredictable under load. A $6/mo RAW server handles all three frameworks simultaneously with resources to spare.

Performance Tips

  • Enable gzip in Nginx — compress JSON and HTML responses before they leave the server
  • Use cluster mode — PM2 -i max utilizes all available CPU cores
  • Serve static files from Nginx — bypass Node.js entirely for images, CSS, and JS bundles
  • Set NODE_ENV=production — disables dev tooling and enables internal optimizations
  • Monitor with PM2 monit — watch memory usage and restart leaky processes automatically

Deploy Node.js on RAW

npx rawhq deploy

7-day free trial. 13 seconds to deploy. Run Express, Next.js, and Fastify on dedicated hardware for $6/mo.