Skip to main content
Back to Blog

Deploy Next.js to DigitalOcean with NGINX and Let's Encrypt SSL (2026)

May 14, 2026
Safal Bhattarai
15 min read
DevOpsDigitalOceanNGINXNext.jsSSLLet's EncryptLinuxGitHub ActionsCI/CDTutorial

Vercel is great. But if you want full control, predictable cost, and the ability to host a Node.js API and a Next.js app on the same box, a $12 DigitalOcean droplet is unbeatable. This is the exact DevOps pipeline I run in production for vCardly, Himaal Pure, and every site I host myself.

By the end of this guide you'll have: a hardened Ubuntu droplet, Node.js + PM2 running your Next.js app, NGINX reverse-proxying with HTTP/2, automatic HTTPS via Let's Encrypt, and GitHub Actions deploying on every push to main.

Prerequisites

  • A DigitalOcean account (or any VPS — Hetzner, Linode, Vultr work identically).
  • A domain you control with DNS pointing to the droplet.
  • A Next.js project in a GitHub repo.
  • An SSH key on your machine (ssh-keygen -t ed25519).

Step 1 — Create the droplet

From the DigitalOcean dashboard:

  1. Image: Ubuntu 22.04 LTS x64.
  2. Plan: Basic, Regular CPU, 2 GB RAM / 1 vCPU / 50 GB SSD ($12/mo). The $6 droplet works for hobby projects, but 1 GB RAM starves once you add MongoDB or PostgreSQL on the same box.
  3. Datacenter: whichever is closest to your users.
  4. Authentication: paste your SSH public key. Never use password auth.

Once provisioned, point your domain's A record at the droplet IP.

Step 2 — Initial server hardening

SSH in as root:

ssh root@your-droplet-ip

Create a non-root user, give them sudo, and disable root SSH:

adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

sed -i 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh

From now on, log in as deploy:

ssh deploy@your-droplet-ip

Enable the firewall:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Install fail2ban to throttle brute-force SSH attempts:

sudo apt install -y fail2ban
sudo systemctl enable fail2ban

Step 3 — Install Node.js and PM2

Use the official NodeSource repo for the latest LTS:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v   # v22.x.x
npm -v

Install PM2 globally:

sudo npm install -g pm2

PM2 is the process manager. It restarts your app if it crashes, manages logs, and survives reboots.

Step 4 — Clone and build your app

Pick a directory under your user's home:

cd ~
git clone git@github.com:your-user/your-repo.git app
cd app
npm ci
npm run build

If your repo is private, add a deploy key on GitHub (Settings → Deploy keys) using cat ~/.ssh/id_ed25519.pub.

Start it with PM2:

pm2 start npm --name "web" -- start
pm2 save
pm2 startup systemd   # follow the printed sudo command

Your app is now running on localhost:3000 — but unreachable from the internet. That's intentional; NGINX will be the front door.

Step 5 — Install NGINX and configure the reverse proxy

sudo apt install -y nginx

Create /etc/nginx/sites-available/yourdomain.com:

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_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    client_max_body_size 10M;
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Hit http://yourdomain.com — you should see your Next.js app.

Step 6 — HTTPS with Let's Encrypt (free, auto-renewing)

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

Certbot edits your NGINX config, requests the cert, installs it, and adds an HTTP→HTTPS redirect. Certs renew automatically via a systemd timer; verify with:

sudo systemctl status certbot.timer
sudo certbot renew --dry-run

Your site now serves on HTTPS with HTTP/2.

Step 7 — Zero-downtime deploys with GitHub Actions

The deploy script you want on the server (~/app/deploy.sh):

#!/bin/bash
set -e
cd ~/app
git pull origin main
npm ci
npm run build
pm2 reload web

Make it executable: chmod +x ~/app/deploy.sh.

pm2 reload swaps the process gracefully — zero dropped requests.

Add a GitHub Actions workflow at .github/workflows/deploy.yml:

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: ~/app/deploy.sh

Add the three secrets in Settings → Secrets → Actions. Every push to main now ships in about 60 seconds, no manual SSH needed.

Step 8 — Monitoring and backups

  • Uptime: Uptime Robot or BetterStack pinging /health or / every minute. Free tier is plenty.
  • Logs: pm2 logs web for app logs, sudo journalctl -u nginx for NGINX, sudo tail -f /var/log/syslog for the system.
  • DB backups: the cron job from my MongoDB setup guide.
  • Off-box backups: rclone the dump folder to S3 or Backblaze B2 nightly. A backup on the same box is not a backup.
  • Error tracking: Sentry, free tier covers most personal projects.

Common pitfalls

  • Forgetting to set NODE_ENV=production — Next.js relies on it for cache and bundle behavior. Set it in PM2 or in your env file.
  • Skipping npm ci and using npm install in deploys — non-deterministic; you'll ship a different lockfile on different machines.
  • Running Node directly without a process manager — a single crash and your site is down until you SSH in.
  • Editing config files without nginx -t — a typo and NGINX refuses to reload, taking everything down.

Frequently asked questions

Why DigitalOcean over Vercel?

Cost predictability, full control, and the ability to host the database, API, and frontend on one box. Vercel is fantastic for pure Next.js apps; once you add a stateful backend, a VPS is often cheaper and simpler.

How much does this cost monthly?

$12 droplet + $0 DNS (Cloudflare free) + $0 SSL (Let's Encrypt) = $12. That's hosting a full-stack app with a database for less than a Netflix subscription.

Docker — yes or no?

Not required for a single Next.js app. The complexity isn't worth it. If you have many services or a team, then yes — but for a one-person product, PM2 + NGINX is simpler and just as reliable.

How do I scale this up?

Vertical first — resize the droplet. Horizontal scaling needs a load balancer, shared Redis for sessions, and a managed database. By the time you need it, your business can afford the migration.

What about Kubernetes?

No. If you're reading this guide, you don't need Kubernetes. Almost no one actually does.

Conclusion

That's the production DevOps stack I run. The first time it takes an evening; the tenth time it takes 30 minutes. Combined with my MongoDB and REST API guides, you have a complete production setup for less than the price of a coffee subscription.

Want help setting this up, or migrating from a managed platform? Get in touch — DevOps and deployment are part of every engagement I take.

Safal Bhattarai

Safal Bhattarai is a Full Stack Developer · Tech Lead · Product Builder based in Pokhara, Nepal, shipping production web apps with Next.js, Node.js, MongoDB, PostgreSQL, and Redis.