# Deploy Next.js to a VPS: Every Step, No Skips

> Deploy Next.js to a VPS step by step: Node.js 24, PM2, Nginx, Let's Encrypt and GitHub Actions, plus the monitoring most guides skip and a self-hosted option.

David Viejo · Published 2026-03-20 · Updated 2026-10-20 · 12 min read

Canonical: https://temps.sh/blog/deploy-nextjs-to-vps-manual

You can deploy a Next.js app to a VPS without Vercel by running Node.js + PM2 + Nginx on any Linux VPS with 2 GB of RAM or more — or by using a platform like Temps that handles all of that automatically from a single git push. This guide covers both paths so you can choose what fits your situation.

> **TL;DR:** Manual deployment means managing Node.js, PM2, Nginx, Certbot, deploy scripts, and GitHub Actions — plus separate services for monitoring, error tracking, and analytics. A self-hosted deployment platform collapses all of that into one tool. Either way, this guide walks every step.

---

## How to Deploy a Next.js App to a VPS

The fastest answer: SSH into a fresh Ubuntu server, install Node.js 24 LTS, clone your repo, run `npm ci && npm run build`, start the app with PM2, put Nginx in front for SSL, and wire up GitHub Actions to re-run those steps on every push to `main`.

The rest of this guide explains exactly how to do that, why each piece exists, and what it still leaves unsolved.

---

## Manual vs. Platform: What You're Actually Choosing

Before diving into the steps, it helps to know what you're signing up for:

|                    | Manual (PM2 + Nginx)              | Temps (self-hosted)                                                     | Vercel                                     |
| ------------------ | --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------ |
| **Setup time**     | 30–60 min per app                 | \~15 min total                                                          | Minutes                                    |
| **Cost**           | VPS + your time                   | VPS + Temps free                                                        | Free tier → see pricing page               |
| **Deployments**    | Custom deploy.sh + GitHub Actions | git push auto-deploy                                                    | git push                                   |
| **Zero-downtime**  | PM2 cluster mode (manual)         | Health-checked cutover; the old version serves until the new one passes | Yes                                        |
| **SSL**            | Certbot (manual renewal)          | Automatic                                                               | Automatic                                  |
| **Analytics**      | Separate tool required            | Built-in (add the analytics provider)                                   | Vercel Web Analytics (usage-billed on Pro) |
| **Error tracking** | Sentry/separate                   | Built-in                                                                | Sentry/separate                            |
| **Rollback**       | Manual git revert + redeploy      | Redeploy a previous deployment (dashboard or CLI)                       | One-click                                  |
| **Self-hosted**    | Yes                               | Yes (dual MIT / Apache 2.0)                                             | No                                         |
| **Vendor lock-in** | None                              | None                                                                    | Yes                                        |

For a priced comparison with Vercel see [Temps vs Vercel](https://temps.sh/compare/vs-vercel) and the [Next.js hosting cost breakdown](https://temps.sh/blog/nextjs-deployment-cost-calculator). Temps is a self-hosted platform, written in Rust, that runs your apps in Docker containers and provides the git-push deployment workflow plus built-in analytics, error tracking, session replay, uptime monitoring, and managed databases — free to self-host, with no per-seat fees and no bandwidth bills. Temps Cloud, a hosted control plane for your own servers, is coming soon from $29/month. If you want the manual approach for full control or learning, keep reading. If you want to skip straight to a platform, [see how to deploy Next.js to your own server with Temps](https://temps.sh/deploy/nextjs).

---

## What You Need (Manual Path)

- A VPS from any provider (Hetzner, DigitalOcean, Linode, Vultr). 2 GB RAM minimum for a Next.js app with a build step. 4 GB if you're running a database on the same box.
- A domain name pointed at your server's IP address.
- SSH access to the server.
- A Next.js app that builds successfully with `npm run build` locally.

This guide assumes Ubuntu 22.04 or 24.04. Debian works too with minor differences.

## Step 1: Set Up the Server

SSH into your new server:

```bash
ssh root@your-server-ip
```

Update packages and install the basics:

```bash
apt update && apt upgrade -y
apt install -y curl git ufw
```

Set up the firewall (the [VPS security checklist](https://temps.sh/blog/vps-security-checklist-2026) covers the rest of the hardening). Open SSH, HTTP, and HTTPS. Close everything else:

```bash
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable
```

Create a non-root user (running everything as root is asking for trouble):

```bash
adduser deploy
usermod -aG sudo deploy
```

Copy your SSH key to the new user:

```bash
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
```

Log out and log back in as the `deploy` user from now on.

## Step 2: Install Node.js

Don't install Node from apt. The version in Ubuntu's default repos is usually ancient. Use the [NodeSource](https://github.com/nodesource/distributions) repository:

```bash
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
```

Verify:

```bash
node --version  # Should be 24.x
npm --version
```

If your project uses a specific Node version (check `.nvmrc` or `engines` in `package.json`), match it here. Mismatched Node versions between local and server are one of the most common causes of "works on my machine" deployment failures.

## Step 3: Clone and Build Your App

```bash
cd /home/deploy
git clone https://github.com/your-username/your-nextjs-app.git
cd your-nextjs-app
npm ci
```

`npm ci` instead of `npm install`. It installs from the lockfile exactly, which is what you want in production. `npm install` can resolve to different versions.

Set your environment variables:

```bash
cp .env.example .env.production
nano .env.production
# Fill in your production values: DATABASE_URL, API keys, etc.
```

Build:

```bash
npm run build
```

If this fails, fix it before continuing. Common issues: missing env vars that the build needs at compile time (Next.js bakes `NEXT_PUBLIC_*` variables into the client bundle during build), or native dependencies that need build tools (`sudo apt install -y build-essential`).

## Step 4: Run the App with PM2

You need a process manager. If you just run `npm start` in your terminal and disconnect, the process dies. [PM2](https://pm2.keymetrics.io/) keeps it running, restarts it if it crashes, and manages logs.

```bash
sudo npm install -g pm2
```

Start your app:

```bash
pm2 start npm --name "nextjs-app" -- start
```

Check it's running:

```bash
pm2 status
pm2 logs nextjs-app
```

By default, Next.js starts on port 3000. Verify it's listening:

```bash
curl http://localhost:3000
```

Tell PM2 to start your app on server boot:

```bash
pm2 startup
pm2 save
```

The `pm2 startup` command prints a line you need to copy and run with sudo. Don't skip it, or your app won't survive a server reboot.

## Step 5: Install and Configure Nginx

Nginx sits in front of your Next.js app. It handles SSL termination, serves static files, and proxies dynamic requests to your Node process.

```bash
sudo apt install -y nginx
```

Create a site config:

```bash
sudo nano /etc/nginx/sites-available/your-app
```

```nginx
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;
    }
}
```

Enable the site:

```bash
sudo ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```

The `proxy_set_header Upgrade` and `Connection 'upgrade'` lines are for WebSocket support. If your app uses real-time features, these headers are required.

At this point, your app should be accessible at `http://yourdomain.com`. No SSL yet.

## Step 6: SSL with Let's Encrypt

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

Certbot modifies your Nginx config to add SSL, sets up automatic certificate renewal, and redirects HTTP to HTTPS.

Verify the renewal timer is active:

```bash
sudo systemctl status certbot.timer
```

Certificates expire every 90 days. Certbot's timer renews them automatically. If you skip this check and the timer isn't running, your site goes down in 3 months with an expired cert. It happens more often than you'd think.

## Step 7: Set Up Deployments

Your app is live. Now you need a way to update it when you push new code.

Create `/home/deploy/deploy.sh`:

```bash
#!/bin/bash
set -e

cd /home/deploy/your-nextjs-app

echo "Pulling latest code..."
git pull origin main

echo "Installing dependencies..."
npm ci

echo "Building..."
npm run build

echo "Restarting..."
pm2 restart nextjs-app

echo "Done."
```

```bash
chmod +x /home/deploy/deploy.sh
```

### Automating with GitHub Actions

```yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: deploy
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: /home/deploy/deploy.sh
```

Add `SERVER_IP` and `SSH_PRIVATE_KEY` to your repo's GitHub Secrets.

### What This Doesn't Handle

The deploy script above has real gaps:

- **No health check and no safe build.** The script builds into the same `.next` directory the live app is serving from, so requests can fail while the build runs. If the build fails, `set -e` stops the script before `pm2 restart`, and the running app is left on a half-written `.next` that the next restart or reboot will boot. PM2 has no previous version to fall back to.
- **No rollback.** If the deploy breaks the app, you have to manually revert.
- **No preview environments.** Every push to `main` goes straight to production.
- **Downtime during restart.** PM2's restart kills the old process and starts the new one. There's a 1–3 second gap with 502 errors.

You can solve each of these individually (building each release in its own directory and switching a `current` symlink, blue-green deploys with Nginx upstream toggling, a webhook server, PM2's cluster mode). But each solution adds complexity, and by the time you've built all of them, you've built a deployment platform. For a deeper look at how these platforms automate this pipeline, see [how git-push deployments work under the hood](https://temps.sh/blog/how-git-push-deployments-work).

## Step 8: Monitoring (The Part Most Tutorials Skip)

Your app is deployed. How do you know it's still running tomorrow?

**Logs:** PM2 handles application logs. Rotate them or they'll fill your disk:

```bash
pm2 install pm2-logrotate
```

**Uptime:** You need something that pings your site and alerts you when it's down. Free options: [UptimeRobot](https://uptimerobot.com/), [Betterstack](https://betterstack.com/) (free tier), or a cron job that curls your health endpoint. See our guide on [building an uptime monitoring system](https://temps.sh/blog/how-to-build-uptime-monitoring-system) for more depth.

**Error tracking:** When your app throws an unhandled exception in production, how do you find out? [Sentry](https://sentry.io/) (free tier), [LogRocket](https://logrocket.com/), or parsing PM2 logs manually. We covered the options in [error tracking without Sentry](https://temps.sh/blog/how-to-set-up-error-tracking-without-sentry).

**Analytics:** Google Analytics, [Plausible](https://plausible.io/), [Umami](https://umami.is/), or similar. If you want to avoid third-party scripts entirely, see [how to add web analytics without third-party scripts](https://temps.sh/blog/how-to-add-web-analytics-without-third-party-scripts).

Each of these is a separate tool, a separate account, a separate dashboard.

## The Full Stack of What You Just Built

Let's count:

**Running on your server:**

1. Node.js (runtime)
2. PM2 (process manager)
3. Nginx (reverse proxy, SSL termination)
4. Certbot (certificate renewal)
5. Git (code delivery)
6. GitHub Actions (build automation)

**Still need but didn't set up:**
7\. Uptime monitoring (external service)
8\. Error tracking (external service)
9\. Analytics (external service)
10\. Log management (PM2 + logrotate, or external service)

That's 6 things on your server and 4 external services for a single Next.js app.

---

## The Alternative: Auto-Detected Builds with Temps

If you'd rather not manage Nginx, PM2, Certbot, and four separate observability tools, Temps is a self-hosted deployment platform that replaces all of it.

**Three things Temps does that the manual approach doesn't:**

1. **Auto-detected builds.** Point Temps at your repository. It detects Next.js from `next.config.js`, `next.config.mjs` or `next.config.ts`, runs your build script on Node.js 22 and starts the app with `next start`, so there's no Dockerfile or build script to maintain and you don't need `output: 'standalone'`. An app with a custom server such as `server.js` needs its own Dockerfile, which Temps uses as-is.

2. **Health-checked cutover.** Before switching traffic, Temps requests your app (`/`, or the path set in `.temps.yaml`) until it gets 2 consecutive good responses. If the new version doesn't become healthy within the startup timeout (5 minutes by default, configurable per project), the deployment is marked failed and the current version keeps serving. Rolling back a deploy that already went live is a manual redeploy of a previous version. The manual PM2 approach has no equivalent gate.

3. **Built-in observability stack.** Analytics, error tracking, session replay, and uptime monitoring are included — not separate accounts. Temps replaces PostHog/Plausible, Sentry, FullStory, and Pingdom alongside its deployment engine.

**Self-host Temps on the same VPS** (dual MIT / Apache 2.0 license, free). Download the installer, read it, then run it with bash; it installs Docker if needed, sets up the database and creates your admin account:

```bash
curl -fsSL https://temps.sh/deploy.sh -o deploy.sh
less deploy.sh
bash deploy.sh
```

Then connect your Next.js repo and push. The [quickstart](https://temps.sh/docs/quickstart) covers the installer's setup modes, and [Deploy Next.js to your own server](https://temps.sh/deploy/nextjs) covers the Next.js specifics.

---

## Or: Other Platforms Worth Knowing

- **[Vercel](https://vercel.com/)** — They built Next.js. Free tier is generous. [Costs scale fast with traffic](https://temps.sh/blog/vercel-pricing-complete-guide-2026).
- **[Coolify](https://coolify.io/)** — Open-source, self-hosted. Handles Docker, Traefik proxy, SSL, and git-push deploys.
- **[Dokploy](https://dokploy.com/)** — Open-source, simpler than Coolify, focused on minimal configuration.
- **[Kamal](https://kamal-deploy.org/)** — From the Rails team. Deploys Docker containers to any server over SSH with minimal abstraction.

---

## Frequently Asked Questions

### How do I deploy a Next.js app without Vercel?

Run it on a server you control, either by hand (Node.js, PM2, Nginx and Certbot, as this guide shows) or with a self-hosted platform such as Temps. [Deploy Next.js without Vercel](https://temps.sh/blog/5-ways-to-deploy-nextjs-app) compares the options, including managed platforms.

### Can I deploy Next.js to a VPS without Docker?

Yes. The manual path in this guide (Node.js + PM2 + Nginx) runs the app directly without Docker. Temps also supports Dockerfile-based builds if you prefer containerization.

### Do I need a Dockerfile to deploy Next.js with Temps?

No. Temps detects Next.js from a `next.config.*` file, builds it on Node.js 22 and runs `next start`. Add your own Dockerfile only if the app needs a custom server such as `server.js`; Temps then builds that Dockerfile as-is.

### What's the minimum VPS size for a Next.js app?

2 GB RAM is the practical minimum for a Next.js app that builds on the server. 1 GB servers frequently run out of memory during `next build`. If you're also running a database on the same box, use 4 GB.

### Does Temps support Next.js App Router and Server Components?

Yes. Temps runs Next.js as a standard Node.js application. App Router, Server Components, API routes, and Server Actions all work as expected — there are no serverless function limitations or Edge runtime constraints.

### How does Temps handle zero-downtime deployments for Next.js?

The old version keeps serving while Temps health-checks the new one. Traffic switches only after 2 consecutive successful responses; if the new version isn't healthy when the startup timeout (5 minutes) runs out, the deployment is marked failed and traffic never moves. There's no automatic rollback after a deploy has gone live: to go back, redeploy a previous deployment from the dashboard or with `bunx @temps-sdk/cli rollback`.

---

Source: https://temps.sh/blog/deploy-nextjs-to-vps-manual · Index of all pages: https://temps.sh/llms.txt
