Next.js 15 App Router on a Hetzner VPS — Deployment, PM2, and Nginx Config

A production-ready walkthrough for self-hosting Next.js 15 App Router on a Hetzner VPS — PM2 cluster mode, Nginx reverse proxy, Cloudflare SSL, and environment variable management.

Table of Contents

Core principle

Self-hosting Next.js 15 on a VPS is not complicated, but the gaps between tutorials and production reality are where failures happen. This guide covers the full stack: application build, PM2 process management, Nginx reverse proxy, and Cloudflare SSL — with the configuration choices that reflect actual production use rather than minimal working examples.

This is the stack AKORNET uses for Dentalytic on a Hetzner VPS at 5.75.157.137, alongside PHP-based applications for other products on the same server.

Server prerequisites

Building the Next.js application

Next.js 15 App Router applications must be built for standalone output for efficient self-hosting. In next.config.js:


/** @type {import('next').NextConfig} */

const nextConfig = {

  output: 'standalone',

}



module.exports = nextConfig

The standalone build produces a .next/standalone directory that contains only the server files needed to run the application — no node_modules tree. This significantly reduces the deployment footprint.

Build on the server (or build locally and copy the output):


npm install

npm run build

After the build, copy the static and public assets into the standalone directory:


cp -r .next/static .next/standalone/.next/static

cp -r public .next/standalone/public

PM2 configuration

Create a ecosystem.config.js in the project root:


module.exports = {

  apps: [

    {

      name: 'dentalytic',

      script: '.next/standalone/server.js',

      instances: 'max',

      exec_mode: 'cluster',

      env: {

        NODE_ENV: 'production',

        PORT: 3000,

      },

      env_file: '.env',

      max_memory_restart: '512M',

      error_file: '/var/log/pm2/dentalytic-error.log',

      out_file: '/var/log/pm2/dentalytic-out.log',

    },

  ],

}

Key choices:

Start and persist the process:


pm2 start ecosystem.config.js

pm2 save

pm2 startup systemd

The last command outputs a systemctl command to run — this registers PM2 to start automatically on server reboot.

Nginx reverse proxy configuration

Create /etc/nginx/sites-available/dentalytic:


server {

    listen 80;

    server_name dental.akor.net;

    return 301 https://$host$request_uri;

}



server {

    listen 443 ssl;

    server_name dental.akor.net;



    ssl_certificate /etc/ssl/certs/cloudflare-origin.pem;

    ssl_certificate_key /etc/ssl/private/cloudflare-origin.key;

    ssl_protocols TLSv1.2 TLSv1.3;



    include snippets/security.conf;



    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;

    }



    location /_next/static/ {

        alias /var/www/dentalytic/.next/static/;

        expires 1y;

        add_header Cache-Control "public, immutable";

    }



    location /public/ {

        alias /var/www/dentalytic/public/;

        expires 30d;

    }

}

Note that include snippets/security.conf applies the shared security configuration from the Nginx + Cloudflare Security Hardening guide — .env blocking, security headers, and dotfile protection.

Enable the site:


ln -s /etc/nginx/sites-available/dentalytic /etc/nginx/sites-enabled/

nginx -t

systemctl reload nginx

SSL with Cloudflare origin certificates

With Cloudflare proxying the domain, the SSL certificate on the origin server is a Cloudflare Origin Certificate — not a Let's Encrypt certificate. This is the correct setup for Full (Strict) SSL mode.

Generate an origin certificate in the Cloudflare dashboard under SSL/TLS → Origin Server → Create Certificate. Download the certificate and key, then copy them to the server:


# Certificate

cp origin.pem /etc/ssl/certs/cloudflare-origin.pem

chmod 644 /etc/ssl/certs/cloudflare-origin.pem



# Private key

cp origin.key /etc/ssl/private/cloudflare-origin.key

chmod 600 /etc/ssl/private/cloudflare-origin.key

Update the Nginx ssl_certificate and ssl_certificate_key paths to match.

Environment variable management

The .env file for a Next.js application on this stack should:

Never place the .env file inside the webroot or any directory that Nginx could serve directly. The snippets/security.conf include blocks direct HTTP access to .env files, but the file placement itself is the primary defence.

Verifying the deployment


# Check PM2 process status

pm2 status



# Check application logs

pm2 logs dentalytic --lines 50



# Test Nginx config

nginx -t



# Check that the application responds on the local port

curl -I http://127.0.0.1:3000



# Check that .env is not publicly accessible

curl -I https://dental.akor.net/.env

# Should return 404

Summary

Self-hosting Next.js 15 on Hetzner with PM2 and Nginx is a cost-effective production setup for SaaS applications at early and growth stage. The key configuration points are: standalone output mode, PM2 cluster configuration with auto-restart and systemd integration, Nginx reverse proxy with static asset caching and shared security headers, and Cloudflare origin certificate for Full (Strict) SSL.

The setup runs multiple Next.js applications alongside PHP applications on the same server — different domains, different ports, same Nginx instance with shared security configuration.

AKORNET runs this stack in production. See our products at akor.net →

FAQ

Why self-host Next.js instead of using Vercel?

Vercel is the path-of-least-resistance for Next.js deployment, but self-hosting on a VPS is significantly cheaper at scale — a Hetzner CX22 at €4/month serves multiple Next.js applications. Self-hosting also gives full control over environment variables, server configuration, and co-location with other services on the same machine. The trade-off is operational responsibility for server management, updates, and uptime.

What is PM2 and why use it for Next.js?

PM2 is a process manager for Node.js applications. It keeps the Next.js server process running after crashes (auto-restart), enables cluster mode to use multiple CPU cores, provides log management, and integrates with systemd for automatic startup on server reboot. Without a process manager, a Next.js server that crashes stays down until manually restarted.

How does Nginx work with a Next.js application?

Nginx acts as a reverse proxy — it receives incoming HTTP/HTTPS requests on ports 80/443 and forwards them to the Next.js server running on a local port (typically 3000). This allows multiple applications to run on the same server on different domains, handles SSL termination, serves static files efficiently, and applies rate limiting and security headers before requests reach the application.

Should environment variables be in the .env file on the server?

The .env file is an acceptable storage mechanism on the server, but it must be outside the webroot, owned by the application user with 600 permissions, and never committed to version control. For applications where secrets rotation is frequent, consider a secrets management tool. At minimum, ensure the Nginx configuration blocks direct HTTP access to .env — see the Nginx + Cloudflare Security Hardening guide for the required location block.

Need help implementing this?

Talk with the AKORNET team about your project or SaaS infrastructure.

Get in Touch →