Running node server.js on your laptop is not a deployment. A real deployment means your app runs 24/7, restarts automatically after crashes, is accessible at a domain name over HTTPS, and can be updated without manually SSH-ing into the server. This guide covers all of that — from a blank Ubuntu VPS to a production-grade Node.js deployment with automated deploys on git push.
What You Need
- A VPS running Ubuntu 22.04 LTS — typical entry-level cost is $4–12/month
- A domain name pointing to the server's IP address
- Your Node.js app in a GitHub repository
- A local machine with SSH installed
Recommended VPS providers:
- DigitalOcean — $200 free credit for new users, best developer docs, $6/mo entry Droplet
- Hostinger VPS — starts at $4.99/mo, KVM virtualisation, NVMe SSD
- Cloudways — if you want managed infrastructure (Nginx + PHP + Redis pre-configured, no terminal needed)
Step 1: Initial Server Setup
When you create a new VPS, you typically get root access via a password or SSH key. Before anything else, create a non-root user with sudo privileges — running applications as root is a security risk.
# SSH into the server as root
ssh root@YOUR_SERVER_IP
# Create a deploy user
adduser deploy
usermod -aG sudo deploy
# Set up SSH key authentication for the deploy user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
Now you can SSH as the deploy user: ssh deploy@YOUR_SERVER_IP. Do the rest of this guide as that user.
Configure the Firewall
sudo ufw allow OpenSSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
sudo ufw status
This allows SSH, HTTP, and HTTPS — and blocks everything else, including direct access to your Node.js app's port (3000). All traffic will go through Nginx, which we set up later.
Step 2: Install Node.js with nvm
Don't install Node.js from apt — the version is usually outdated. Use nvm (Node Version Manager), which lets you install and switch between multiple Node.js versions:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# Reload shell profile
source ~/.bashrc
# Install Node.js 20 LTS
nvm install 20
nvm use 20
nvm alias default 20
# Verify
node --version # v20.x.x
npm --version # 10.x.x
Step 3: Clone Your Application
cd /var/www
sudo mkdir myapp
sudo chown deploy:deploy myapp
# Clone the repository
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git myapp
cd myapp
# Install production dependencies
npm ci --omit=dev
Set Environment Variables
Do not create a .env file on the server and check it into git. Instead, create it directly on the server and add it to .gitignore so it is never overwritten by git pull:
nano /var/www/myapp/.env
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_NAME=myapp_prod
DB_USER=myapp
DB_PASS=a_strong_password_here
APP_SECRET=a_random_64_char_string
Save the file. Make it readable only by the deploy user:
chmod 600 /var/www/myapp/.env
Step 4: Set Up PM2 Process Manager
PM2 is the standard process manager for Node.js in production. It keeps your app running, restarts it if it crashes, provides log management, and can start your app automatically when the server reboots.
npm install -g pm2
# Start your application
pm2 start /var/www/myapp/server.js --name myapp
# Check that it's running
pm2 status
# View logs
pm2 logs myapp --lines 50
Auto-Start on Server Reboot
# Save current process list
pm2 save
# Generate and install the startup script
pm2 startup
# PM2 will print a command — run it exactly as printed, e.g.:
sudo env PATH=$PATH:/home/deploy/.nvm/versions/node/v20.x.x/bin pm2 startup systemd -u deploy --hp /home/deploy
After this, PM2 will restart your app automatically on every server reboot.
PM2 Ecosystem File for Production
For more control, create an ecosystem file instead of passing everything on the command line:
// /var/www/myapp/ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'server.js',
instances: 'max', // use all CPU cores
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
}
}]
};
pm2 start ecosystem.config.js --env production
pm2 save
instances: 'max' with exec_mode: 'cluster' runs one Node.js process per CPU core, handling more concurrent requests. PM2 load-balances between them automatically.
Step 5: Nginx as a Reverse Proxy
Nginx sits in front of your Node.js app, handles SSL termination, serves static files efficiently, and forwards API/app requests to the Node.js process on port 3000.
sudo apt install nginx -y
sudo systemctl enable nginx
Create a site configuration:
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.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_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t # test config syntax
sudo systemctl reload nginx
Your app is now accessible at http://yourdomain.com via Nginx. Next: HTTPS.
Step 6: SSL Certificate with Certbot
Let's Encrypt provides free, auto-renewing SSL certificates. Certbot handles the installation and renewal automatically:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot modifies the Nginx config automatically to add SSL, redirect HTTP to HTTPS, and set up certificate paths. It also installs a cron job to auto-renew the certificate before it expires (Let's Encrypt certificates are valid for 90 days).
After running Certbot, your site is live at https://yourdomain.com.
Step 7: Automated Deployment via GitHub Actions
Now that the server is set up, automate future deployments so that merging to main automatically updates the server.
Create a dedicated deploy SSH key
# On your local machine
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy
# Don't add a passphrase — Actions needs it unprotected
cat ~/.ssh/github_deploy.pub
# Copy this output
# On the server — add the public key
echo "PASTE_THE_PUBLIC_KEY_HERE" >> /home/deploy/.ssh/authorized_keys
In GitHub → Settings → Secrets, add:
SSH_HOST— your server IPSSH_USER—deploySSH_PRIVATE_KEY— contents of~/.ssh/github_deploy(the private key)SSH_PORT—22(or your custom port)
The Deploy Workflow
Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --omit=dev
pm2 reload myapp --update-env
pm2 reload performs a zero-downtime restart — new processes start and accept connections before the old ones shut down. Your app stays online during the update.
Monitoring and Logs
Essential commands for maintaining the deployment:
# PM2 — real-time process monitoring dashboard
pm2 monit
# PM2 — stream application logs
pm2 logs myapp
# Nginx — access log (every request)
sudo tail -f /var/log/nginx/access.log
# Nginx — error log (only errors)
sudo tail -f /var/log/nginx/error.log
# Check application port is listening
ss -tlnp | grep 3000
# Check Nginx is serving correctly
curl -I https://yourdomain.com
Need a VPS to deploy your application? DigitalOcean offers a $200 credit for new users — enough to run a production server for months.
Get $200 DigitalOcean Credit
CREESOL