DEV Community

Muhammad Uzair
Muhammad Uzair

Posted on

VPS Deployment Guide (NEXT JS/REACT JS + GITHUB ACTIONS)

If you follow this exactly, your app will:

  • Deploy from GitHub with .env replaced each time
  • Serve over HTTPS automatically
  • Keep running after reboots
  • Renew SSL certs without downtime

0️⃣ Prerequisites

  • VPS with Ubuntu/Debian OR CentOS/AlmaLinux/Amazon Linux
  • Domain pointing to VPS IP (A record set in DNS)
  • GitHub repo with app code
  • SSH access to VPS
  • GitHub Actions enabled

1️⃣ Install Required Packages

Ubuntu/Debian

sudo apt update && sudo apt upgrade -y
sudo apt install curl git nginx ufw -y
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
Enter fullscreen mode Exit fullscreen mode

CentOS / AlmaLinux / Amazon Linux

sudo yum update -y
sudo yum install curl git nginx -y
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo yum install -y nodejs
sudo npm install -g pm2
Enter fullscreen mode Exit fullscreen mode

2️⃣ Create a Repo-Level Deploy Key

ssh-keygen -t rsa -b 4096 -C "deploy-key"
# Save as: /home/ubuntu/.ssh/deploy_key_example
Enter fullscreen mode Exit fullscreen mode

Press Enter twice for no passphrase.


3️⃣ Add Public Key to GitHub Repo

cat ~/.ssh/deploy_key_example.pub
Enter fullscreen mode Exit fullscreen mode
  • GitHub Repo → Settings → Deploy Keys → Add Deploy Key
  • Name: VPS Deploy Key
  • Paste the public key
  • ✅ Check “Allow write access” (optional)
  • Save

4️⃣ Configure SSH for GitHub

nano ~/.ssh/config
Enter fullscreen mode Exit fullscreen mode

Paste:

Host github-example
  HostName github.com
  User git
  IdentityFile ~/.ssh/deploy_key_example
  IdentitiesOnly yes
Enter fullscreen mode Exit fullscreen mode

5️⃣ Clone the Repo

sudo mkdir -p /var/www
sudo chown $USER:$USER /var/www
cd /var/www
git clone git@github-example:username/repo-name.git example-app
cd example-app
Enter fullscreen mode Exit fullscreen mode

6️⃣ Install Dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

7️⃣ Build the App

Next.js

npm run build
pm2 start npm --name "example-app" -- start
Enter fullscreen mode Exit fullscreen mode

React

npm run build
Enter fullscreen mode Exit fullscreen mode

8️⃣ Install SSL

Ubuntu/Debian

sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --nginx -d example.com
Enter fullscreen mode Exit fullscreen mode

CentOS / AlmaLinux / Amazon Linux

sudo yum install certbot python3-certbot-nginx -y
sudo certbot certonly --nginx -d example.com
Enter fullscreen mode Exit fullscreen mode

9️⃣ Nginx Configuration (With SSL)

Ubuntu/Debian

sudo nano /etc/nginx/sites-available/example.com
Enter fullscreen mode Exit fullscreen mode

CentOS / AlmaLinux / Amazon Linux

sudo nano /etc/nginx/conf.d/example.com.conf
Enter fullscreen mode Exit fullscreen mode

Next.js (SSR)

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://localhost:<PORT_OF_YOUR_APP>;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
Enter fullscreen mode Exit fullscreen mode

React (Static)

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example-app/build;
    index index.html index.htm;

    location / {
        try_files $uri /index.html;
    }
}
Enter fullscreen mode Exit fullscreen mode

🔟 Enable Config & Restart Nginx

Ubuntu/Debian

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled
sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

CentOS / AlmaLinux / Amazon Linux

sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

1️⃣1️⃣ Enable PM2 Auto-Start (Next.js Only)

pm2 save
pm2 startup
Enter fullscreen mode Exit fullscreen mode

Follow the command PM2 outputs and run it.


1️⃣2️⃣ Enable Automatic SSL Renewal

sudo certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

Add a cron job:

sudo crontab -e
Enter fullscreen mode Exit fullscreen mode

Paste:

0 3 * * * /usr/bin/certbot renew --quiet && /bin/systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

1️⃣3️⃣ GitHub Actions Deployment Pipeline

Next.js

name: Deploy Next.js to VPS

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Deploy via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_KEY }}
          script: |
            cd /var/www/example-app
            git pull origin main
            rm -f .env
            echo "${{ secrets.ENV_FILE }}" > .env
            npm install
            npm run build
            pm2 restart example-app
Enter fullscreen mode Exit fullscreen mode

React

name: Deploy React to VPS

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Deploy via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_KEY }}
          script: |
            cd /var/www/example-app
            git pull origin main
            rm -f .env
            echo "${{ secrets.ENV_FILE }}" > .env
            npm install
            npm run build
            sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

1️⃣4️⃣ Add GitHub Secrets

In GitHub Repo → Settings → Secrets → Actions:

  • VPS_HOST → VPS IP
  • VPS_USERubuntu (or VPS username)
  • VPS_KEY → contents of ~/.ssh/deploy_key_example
  • ENV_FILE → entire .env file contents

✅ Key Differences

Feature Next.js (SSR) React (Static)
Build Command npm run build npm run build
Serve Method PM2 Node.js process Nginx static serve
Nginx Role Reverse proxy to Node server Directly serve /build
Port Required <PORT_OF_YOUR_APP>
PM2 Required

Top comments (1)

Collapse
 
eqvps profile image
EQVPS

Solid end-to-end guide — the SSH-action + PM2 + certbot combo is a clean pattern. One thing worth flagging from having been bitten by it: building on the production VPS (npm run build inside the deploy script) is the step that quietly breaks things on smaller boxes.

Next.js builds are memory-hungry — a non-trivial app can spike past 1.5–2 GB during build. On a 2–4 GB VPS that's already running your app plus nginx plus a database, the build and the live process end up fighting for RAM, and the OOM-killer picks a victim. Worst case the build dies halfway and you're left with a half-updated .next and a down site — right when a deploy was supposed to be safe.

Two fixes that have saved me:

Cap the build memory so it fails loud instead of taking the box down with it: NODE_OPTIONS="--max-old-space-size=1536" npm run build. And gate the restart on build success — npm run build && pm2 restart app, never an unconditional restart, so a failed build leaves the old working version running instead of a broken one.

Or build in CI, ship the artifact. Run npm run build on the GitHub Actions runner (which has way more RAM than most VPS), then rsync/scp the .next output to the server and just restart. The production box never spends memory building — it only serves. Cleaner and faster, especially once builds get heavy.

The memory ceiling is the thing nobody warns you about until a deploy takes the site down at the worst moment. Good writeup otherwise — the SSH key + secrets flow is exactly right.