A brand new VPS. A Node.js app ready to deploy. Here’s the full setup — from raw Ubuntu server to production-ready stack running HTTPS with automatic restarts and zero-downtime deploys.
This guide uses Ubuntu 24.04 LTS, Nginx, Let’s Encrypt, and Oxmgr.
1. Initial Server Setup
Create a Non-Root User
Never run anything as root in production:
adduser deploy
usermod -aG sudo deploy
# Copy SSH keys to the new user
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy Log out and back in as deploy:
ssh deploy@your-server-ip Firewall Setup
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo ufw status Disable Root SSH Login
sudo nano /etc/ssh/sshd_config Set:
PermitRootLogin no
PasswordAuthentication no # key-only auth sudo systemctl restart ssh System Updates
sudo apt update && sudo apt upgrade -y Set up automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades 2. Install Node.js
Use NodeSource for the latest LTS:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node --version # v22.x.x
npm --version Or use nvm if you need multiple Node.js versions:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm use --lts 3. Install Oxmgr
npm install -g oxmgr
oxmgr --version 4. Deploy Your Application
Create a directory for your app:
sudo mkdir -p /var/www/myapp
sudo chown deploy:deploy /var/www/myapp Clone your repository:
cd /var/www/myapp
git clone https://github.com/yourname/myapp.git .
npm ci --omit=dev
npm run build Create oxfile.toml:
[processes.api]
command = "node dist/server.js"
instances = 2
restart_on_exit = true
restart_delay_ms = 1000
max_restarts = 10
[processes.api.env]
NODE_ENV = "production"
PORT = "3000"
[processes.api.health_check]
endpoint = "http://localhost:3000/health"
interval_secs = 30
initial_delay_secs = 10
unhealthy_threshold = 3 Test it starts:
oxmgr start
oxmgr status
curl http://localhost:3000/health 5. Install Nginx
sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx Configure Nginx as Reverse Proxy
Create the site config:
sudo nano /etc/nginx/sites-available/myapp server {
listen 80;
server_name your-domain.com www.your-domain.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Gzip
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
# Pass real client info
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Static files served directly by Nginx (much faster)
location /static/ {
alias /var/www/myapp/dist/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check — don't proxy to app, respond directly
location /nginx-health {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
} Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default # remove default
# Test config
sudo nginx -t
# Reload
sudo systemctl reload nginx Visit http://your-domain.com — your app should be accessible.
6. SSL with Let’s Encrypt
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com -d www.your-domain.com Certbot:
- Verifies domain ownership
- Gets the certificate
- Automatically edits your Nginx config to enable HTTPS
- Sets up auto-renewal
Your Nginx config now looks like:
server {
listen 443 ssl;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ... rest of your config
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
return 301 https://$server_name$request_uri;
} Test renewal:
sudo certbot renew --dry-run Certificates renew automatically via systemd timer (check with systemctl list-timers | grep certbot).
7. Load Balancing Multiple Instances
When you run instances = 2 in Oxmgr, each instance needs a unique port. Update your setup:
[processes.api]
command = "node dist/server.js"
instances = 2
restart_on_exit = true
[processes.api.env]
NODE_ENV = "production"
PORT = "3000" # instance 0 gets 3000, instance 1 gets 3001 In your app:
const port = parseInt(process.env.PORT) + parseInt(process.env.OXMGR_INSTANCE_ID || '0');
server.listen(port); Update Nginx to load-balance across both:
upstream api_backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
# Keep connections alive to backend
keepalive 32;
}
server {
listen 443 ssl;
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # for keepalive upstream
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;
}
} 8. Start Oxmgr on Boot
Register Oxmgr as a system service so it starts automatically on reboot:
sudo oxmgr service install Verify:
sudo systemctl status oxmgr Or follow the server startup guide to create a custom systemd unit.
9. Log Rotation
Prevent logs from filling your disk:
sudo nano /etc/logrotate.d/myapp /var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 deploy deploy
postrotate
oxmgr reload api > /dev/null 2>&1 || true
endscript
} Oxmgr also has built-in log rotation via oxfile.toml:
[processes.api.logs]
max_size_mb = 100
max_files = 7
compress = true 10. Monitoring
Check that everything is healthy:
# App status
oxmgr status
# Nginx status
sudo systemctl status nginx
# SSL cert expiry
sudo certbot certificates
# Disk space
df -h
# System load
uptime Set up process monitoring alerts as described in the Node.js process monitoring guide.
Final Checklist
Before calling it production-ready:
- SSH key authentication only (password disabled)
- Firewall enabled (ports 22, 80, 443 only)
- Node.js app not running as root
- Oxmgr with
restart_on_exit = trueandmax_restarts - Health check endpoint working
- Nginx with HTTPS (HTTP redirects to HTTPS)
- SSL certificate installed and auto-renewing
- Oxmgr starts on server reboot
- Log rotation configured
-
NODE_ENV=productionset
This setup handles most production workloads up to a few hundred requests/second on a single VPS. When you need to scale beyond a single server, add a load balancer and replicate this setup across multiple machines.