Heath SchweitzerHeath Schweitzer
← All posts

Deploying Next.js on a VPS Without Vercel

April 17, 2026|Heath Schweitzer|4 min read|22 views|Last Updated June 18, 2026

Technology
Diagram showing a Next.js app deployed on a VPS using GitHub Actions, PM2, Nginx, and CloudPanel without Vercel.

Vercel is perhaps the easiest way to deploy a Next.js application. It's made by the same team that builds Next.js, the integration is seamless, and for many use cases it's the right choice. But it's not the only choice, and for developers who already run their own servers, or who want more control over their infrastructure and costs, deploying Next.js on a VPS is entirely manageable.

Here's how I run heathschweitzer.com on a DigitalOcean Droplet without Vercel, and what I learned along the way.

Why Not Vercel

Vercel's free tier is generous for small projects, but once you need a database connection, custom server configuration, or higher traffic limits, the pricing scales quickly. More importantly, I already have a CloudPanel server running other projects. Adding another Next.js site to existing infrastructure is zero marginal cost.

There's also the control argument. With Vercel you're deploying into their platform — you get the abstraction they've built, not the server. That's usually fine, but it means less visibility into what's actually running and less flexibility when you need to do something non-standard. When I needed to add a maintenance mode that Nginx serves during deploys, Vercel would have made that significantly harder.

The Stack

My setup:

  • DigitalOcean VPS running Ubuntu
  • CloudPanel for server management, Nginx configuration, and SSL certificates
  • PM2 for Node.js process management
  • Nginx as the reverse proxy sitting in front of the Next.js app
  • GitHub Actions for CI/CD — every push to main triggers an automatic deploy

How the Deploy Works

The deploy script does five things in order:

  1. git pull — gets the latest code
  2. npm install — installs any new dependencies
  3. npx prisma migrate deploy — applies any pending database migrations
  4. npm run build — builds the Next.js production bundle
  5. pm2 restart — restarts the Node.js process with the new build

The whole process takes about 40 seconds. During the build and restart phase, Nginx serves a static maintenance page instead of proxying to the Node.js process — so users see a clean "brief maintenance" message rather than a 502 error.

PM2: The Critical Piece

If you've run Node.js applications in production before, you know the problem: node server.js dies when you close the terminal, or when the process crashes, or when the server reboots. PM2 solves this.

PM2 is a process manager for Node.js. It keeps your application running, automatically restarts it if it crashes, and manages startup configuration so it survives server reboots. The Next.js production server is just a Node.js process — PM2 wraps it the same way PHP-FPM wraps PHP for an Apache or Nginx setup.

Key commands I use regularly:

pm2 status              # check if the app is running
pm2 logs heathschweitzer --lines 50  # tail recent logs
pm2 restart heathschweitzer --update-env  # restart and pick up new env vars

The --update-env flag is important whenever you change environment variables. PM2 caches the environment from when it started the process, so new .env values aren't picked up by a plain restart.

Nginx Configuration

CloudPanel generates the base Nginx configuration for each site. For a Next.js app, the core of it is a proxy_pass directive that forwards requests to PM2's Node.js process:

location / {
  proxy_pass http://127.0.0.1:3001;
  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;
}

I added maintenance mode on top of this: a simple check for a flag file, and if present, Nginx returns a 503 with a static HTML page instead of proxying to Node. The deploy script creates the flag before building and removes it after restarting PM2.

GitHub Actions CI/CD

The CI/CD setup is about 15 lines of YAML:

name: Deploy to Production
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: ~/deploy.sh

GitHub Actions SSHes into the server and runs the deploy script. The SSH key is stored as a GitHub secret. Every push to main triggers an automatic deploy. I never manually SSH in for a routine deployment anymore.

What You Give Up

Self-hosting Next.js means managing things Vercel handles for you:

  • Edge functions: Vercel runs middleware and API routes at the edge globally. On a single VPS, everything runs in one region.
  • Automatic scaling: Vercel scales with traffic automatically. A VPS has fixed resources.
  • Preview deployments: Vercel creates a unique URL for every pull request. You'd have to build this yourself.
  • Analytics and observability: Vercel provides built-in performance metrics. On a VPS you need your own monitoring.

For a personal site with modest traffic, none of these matter. For a high-traffic application or a team that needs preview deployments, Vercel's value proposition gets stronger.

The Bottom Line

If you already run servers and you're comfortable with the command line, self-hosting Next.js is straightforward. The deploy pipeline I described took a day to set up and has been reliable since. The maintenance window is about 40 seconds every time I push code. The monthly cost is folded into server infrastructure I'm already paying for.

It's not for everyone — but for a developer who values control and already has the infrastructure, it's a perfectly viable alternative to Vercel.

Tagged

next.jsdeploymentvpsdevops

If this post was useful, consider buying me a coffee ☕ with ₿itcoin — no account needed, any amount welcome.

Bitcoin tip QR code
⚡ Open in Wallet