Technology Encyclopedia Home >Deploy a Node.js App on a Cloud Server — From Zero to Live in 20 Minutes

Deploy a Node.js App on a Cloud Server — From Zero to Live in 20 Minutes

Running a Node.js application on your own cloud server gives you full control over the environment, no usage caps, and a predictable monthly cost. Once the setup is done, deploying new versions takes seconds.

This guide covers everything: provisioning the server, installing Node.js, running the app with PM2, setting up Nginx as a reverse proxy, getting HTTPS, and automating deploys via GitHub Actions.

I use Tencent Cloud Lighthouse for this. Lighthouse has a pre-built Node.js application image — select it at instance creation and Node.js is already installed and configured when the server starts up, so you can skip the NVM installation steps. It also includes a built-in OrcaTerm browser terminal so you can manage the server without opening a local SSH client, and the snapshot feature means you can take a full server backup before any major deployment change.


Table of Contents

  1. Why Run Node.js on Your Own Server?
  2. What You Actually Need
  3. Part 1 — Spin Up the Server (5 min)
  4. Part 2 — Install Node.js the Right Way (NVM)
  5. Part 3 — Get Your App onto the Server
  6. Part 4 — PM2: The Thing That Keeps Your App Alive
  7. Part 5 — Nginx as a Reverse Proxy
  8. Part 6 — HTTPS in Two Commands
  9. Part 7 — Auto-Deploy on Every Git Push
  10. The One Gotcha That Bit Me
  11. When Things Break: My Troubleshooting Checklist
  12. Six Months In — Honest Verdict

Key Takeaways

  • Use the Lighthouse Node.js application image to skip manual NVM/Node.js installation
  • PM2 keeps your app running continuously — set it up before anything else
  • Nginx handles HTTPS and proxies to your app's internal port — never expose Node directly
  • pm2 reload myapp provides zero-downtime updates
  • pm2 startup && pm2 save ensures your app survives server reboots

Why Run Node.js on Your Own Server? {#why-leave-paas}

Managed PaaS platforms are a great choice when you want to get an app running quickly without touching server configuration. They handle a lot of the operational complexity for you.

Self-hosting on a VPS makes sense when you want more control or predictability:

  • Always-on processes — your app runs continuously under PM2, with no cold starts or inactivity timeouts
  • Full root access — install any system library, choose your Node.js version, configure the environment exactly as needed
  • Multiple apps, one server — run several projects on the same instance using separate PM2 processes and Nginx virtual hosts
  • Background workers — cron jobs and background processes are just additional PM2 entries, no extra cost
  • Predictable pricing — a flat monthly rate regardless of traffic or request volume

The setup takes about 20 minutes. After that, deploying updates is automated.


What You Actually Need {#prerequisites}

What Notes
A Tencent Cloud Lighthouse account Free to sign up
A Node.js app Express, Fastify, whatever — any HTTP server works
Basic comfort with a terminal If you can cd and run commands, you're fine
A domain name (optional) Only needed if you want HTTPS with a real domain

Cost: Lighthouse plans start at $5–6/month. Check current new-user promotions before signing up.


Part 1 — Spin Up the Server (5 min) {#part-1}

Create the instance

Log into the console, go to LighthouseNew.

  • Image: Two options:
    • ⚡ Recommended: Click Application Images → select Node.js — Node.js is pre-installed and ready. Skip Part 2 (NVM install) entirely.
    • Alternative: Select System Images → Ubuntu 22.04 LTS if you need to control the exact Node.js version via NVM.
  • Plan: I started on Basic (2 vCPU / 4 GB RAM) for a production API. Starter (2 GB) works fine for low-traffic apps.
Plan vCPU RAM Good for
Starter 2 2 GB Hobby project, low-traffic API
Basic 2 4 GB Production app, most side projects
Standard 4 8 GB High concurrency, multiple apps
  • Region: Pick the one closest to your users. My users are mostly US-based so I went with US East (Virginia).

Open the firewall ports

Click your instance → FirewallAdd Rule:

Port Protocol Purpose
22 TCP SSH (keep this)
80 TCP HTTP — Nginx handles this, not Node directly
443 TCP HTTPS

One thing worth flagging: don't open port 3000 (or whatever port your Node app uses) to the public. Nginx sits in front and handles the public traffic. Your Node process stays on localhost. This is both safer and cleaner.


Part 2 — Install Node.js the Right Way (NVM) {#part-2}

⚡ Skip this section if you selected the Node.js application image in Part 1. Node.js is already installed. Run node --version to confirm and jump to Part 3.

SSH in (or click OrcaTerm in the Lighthouse console for a browser terminal — I use this constantly):

ssh ubuntu@YOUR_SERVER_IP

First, update the system:

sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl build-essential

Now install Node.js via NVM. I've seen people do apt install nodejs and immediately regret it when they need a different version or want to update without breaking everything. NVM is the right move:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Load NVM in current session
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"

# 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

Part 3 — Get Your App onto the Server {#part-3}

Clone from GitHub (the way I do it)

mkdir -p ~/apps && cd ~/apps
git clone https://github.com/your-username/your-app.git
cd your-app
npm install --production

Handle environment variables properly

This is where I see people make security mistakes. Never hardcode secrets. Create a .env file:

nano .env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=make-this-a-long-random-string

Lock down the permissions:

chmod 600 .env

.env goes in .gitignore. Always.

Quick sanity check

node server.js
# In another OrcaTerm tab:
curl http://localhost:3000

If you get a response, press Ctrl+C and move on. We'll use PM2 to run it properly.


Part 4 — PM2: The Thing That Keeps Your App Alive {#part-4}

PM2 is a process manager for Node.js. Without it, your app dies the moment you close the SSH session. With it, your app restarts automatically if it crashes, starts back up when the server reboots, and keeps logs you can actually read.

npm install -g pm2

# Start your app
pm2 start ~/apps/your-app/server.js --name "my-api"

# Check it's running
pm2 status

# Watch logs live
pm2 logs my-api

The step people always forget: boot persistence

pm2 startup
# PM2 prints a command — copy and run it exactly
# It looks like: sudo env PATH=$PATH:/home/ubuntu/.nvm/... pm2 startup systemd -u ubuntu --hp /home/ubuntu

pm2 save

Do this now. I've forgotten it before and wondered why my app wasn't running after a server restart. Do it now.

Useful PM2 day-to-day commands

pm2 reload my-api    # Zero-downtime restart — use this in production
pm2 restart my-api   # Hard restart — brief downtime, use for debugging
pm2 logs my-api      # Last N lines of logs
pm2 monit            # Live dashboard with CPU/memory per process

Part 5 — Nginx as a Reverse Proxy {#part-5}

Your Node app runs on port 3000 internally. Nginx listens on port 80/443 publicly and forwards traffic to Node. This setup:

  • Handles SSL termination (HTTPS decryption) so Node doesn't have to
  • Compresses responses with gzip
  • Serves static files directly without hitting Node
sudo apt install -y nginx

sudo nano /etc/nginx/sites-available/my-api
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Serve static files directly (optional)
    location /static/ {
        root /home/ubuntu/apps/your-app/public;
        expires 30d;
    }

    # Everything else goes to Node
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # WebSocket support
        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;
        proxy_read_timeout 86400s;
    }

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_vary on;
}
sudo ln -s /etc/nginx/sites-available/my-api /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t       # Must say "test is successful"
sudo systemctl reload nginx

Visit http://YOUR_SERVER_IP — your API should respond.


Part 6 — HTTPS in Two Commands {#part-6}

Once your domain's DNS A record points to the server IP (wait 5–30 minutes for propagation), run:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

When it asks about HTTP-to-HTTPS redirect: choose yes. Certbot edits your Nginx config and sets up auto-renewal. You don't have to touch SSL again.

Verify renewal works:

sudo certbot renew --dry-run

Your API is now live at https://yourdomain.com. 🎉


Part 7 — Auto-Deploy on Every Git Push {#part-7}

I set this up on day one and haven't manually SSH'd in to deploy since. Here's how it works: you push to main, GitHub Actions SSHs into your server and runs a deploy script.

The deploy script (on the server)

nano ~/deploy.sh
#!/bin/bash
set -e

APP_DIR=~/apps/your-app

echo "=== Deploy started: $(date) ==="
cd $APP_DIR

git fetch origin
git reset --hard origin/main

npm install --production
pm2 reload my-api --update-env

echo "=== Deploy done ==="
chmod +x ~/deploy.sh

The GitHub Actions workflow (in your repo)

Create .github/workflows/deploy.yml:

name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH deploy
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ubuntu
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: ~/deploy.sh

Add two secrets in your GitHub repo settings (Settings → Secrets → Actions):

  • SERVER_IP = your Lighthouse public IP
  • SSH_PRIVATE_KEY = paste the full contents of your ~/.ssh/id_ed25519 private key

Push to main. Watch the Actions tab. Done.


The One Gotcha That Bit Me {#gotcha}

After I set everything up and deployed my first update via GitHub Actions, the app was returning stale data. PM2 showed it was running fine. Nginx was fine. Everything looked green.

Turns out: PM2 reload re-reads the code, but it doesn't automatically pick up changes to the .env file unless you pass --update-env. My deploy script had pm2 reload my-api without that flag — so the new code was running with the old environment variables.

The fix is the --update-env flag I already have in the deploy script above. Make sure you include it:

pm2 reload my-api --update-env

If you're seeing mysterious behavior after a deploy and the logs look clean, check whether your env vars are actually current:

pm2 env my-api
# Shows all environment variables the process sees

When Things Break: My Troubleshooting Checklist {#troubleshooting}

Symptom First thing to check Fix
502 Bad Gateway Is PM2 running? pm2 statuspm2 restart my-api
App not responding after reboot PM2 startup configured? pm2 startup && pm2 save
HTTPS cert expired Auto-renewal cron sudo certbot renew --dry-run
Env vars not updating --update-env missing pm2 reload my-api --update-env
Port already in use Another process on 3000 lsof -i :3000 → kill the PID
EACCES on port 80 Node binding directly Use Nginx proxy instead, never bind Node to port 80

Six Months In — Honest Verdict {#verdict}

Here's what running Node.js on a VPS looks like in practice:

Self-hosted VPS (Lighthouse $6/mo)
Monthly cost Flat rate, predictable
Process uptime Continuous — PM2 keeps it running
Root access Full
Multiple apps Yes — separate PM2 processes per app
Background workers Yes — additional PM2 entries
Deploy speed Git push → ~15 seconds via GitHub Actions
Maintenance System updates, monitor PM2 status

When a VPS is the right choice: side projects, internal tools, production APIs where you want predictable costs and full environment control.

When managed platforms make more sense: if you need to scale to many instances automatically and prefer not to manage infrastructure at all, managed PaaS is easier for that use case.

For most individual developers and small teams, a $6/month VPS running PM2 + Nginx handles production workloads comfortably.


Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

Do I need to install Node.js manually on Tencent Cloud Lighthouse?
No — select the Node.js application image and Node.js is pre-installed. Skip the NVM installation section entirely.

What is PM2 and why use it?
PM2 keeps your app running continuously, restarts it automatically on failure, and persists across reboots. Running node server.js directly stops when you close the terminal.

Can I run multiple Node.js apps on one server?
Yes. Each app runs as a separate PM2 process on a different internal port. Nginx routes public traffic to the right app by domain name.

How do I deploy updates without downtime?
Use pm2 reload myapp for zero-downtime reload — PM2 restarts workers one at a time.

What port should my Node.js app listen on?
Any unprivileged port (3000, 4000, 5000) internally. Nginx handles public 80/443 and proxies to your app.


Set up your server today:
👉 Tencent Cloud Lighthouse — Developer-friendly VPS
👉 Current pricing and new user promotions
👉 Explore all active deals and offers