Self-hosting Next.js on an Ubuntu VPS gives you direct control over runtime configuration, domains, caching, logs, and deployment timing. A dependable production setup uses the built-in Next.js server for rendering, PM2 to supervise the Node.js process, Nginx as the public reverse proxy, and HTTPS for every request.
This guide builds that setup step by step. It applies to Next.js applications that use server-side rendering, Server Components, API routes, Server Actions, image optimization, or other features that require a running Node.js server.
How the production stack works
Visitors connect to Nginx on ports 80 and 443. Nginx handles the public connection, redirects HTTP to HTTPS, applies request limits, and forwards application traffic to Next.js on a private local port. PM2 keeps the Next.js process running and restores it after a server reboot.
- Next.js: builds and serves the application in production mode.
- PM2: supervises the Node.js process, restarts failures, and manages logs.
- Nginx: terminates HTTPS and reverse-proxies requests to Next.js.
- Certbot: obtains and renews the TLS certificate.
- Ubuntu: provides the operating system, firewall, users, and service controls.
Next.js recommends placing a reverse proxy such as Nginx in front of a self-hosted application instead of exposing the application server directly to the internet. The proxy can reject malformed or slow requests before they consume rendering capacity.
Prerequisites
- An Ubuntu VPS with a public IP address.
- A non-root SSH user with sudo access.
- A domain or subdomain pointing to the VPS.
- A Next.js project with working
buildandstartscripts. - Production environment variables and secrets stored outside version control.
Current Next.js releases require a supported Node.js version. Check the requirements for your installed Next.js version rather than relying on an old server image. For Next.js 16, the documented minimum is Node.js 20.9.
1. Prepare Ubuntu and the firewall
Connect over SSH, install operating-system updates, and add the packages needed by the deployment:
sudo apt update
sudo apt upgrade -y
sudo apt install nginx git build-essential -y
Confirm that Nginx starts and its configuration is valid:
sudo systemctl status nginx --no-pager
sudo nginx -t
If UFW is enabled, keep SSH open and allow web traffic:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw status
Do not expose the Next.js application port through the firewall. It should listen on the loopback interface and receive public traffic only through Nginx.
2. Install a supported Node.js release
Use the official Node.js packages or a maintained version manager. A version manager is useful when different applications require different Node.js releases. After installing it, install the current long-term support release and verify the tools:
nvm install --lts
nvm use --lts
node --version
npm --version
Record the chosen Node.js major version in the project documentation or a version file. This makes builds repeatable and reduces surprises after a future runtime upgrade.
3. Create the application directory
Keep the application under a dedicated deployment user rather than running it as root:
sudo mkdir -p /var/www/example.com
sudo chown -R $USER:$USER /var/www/example.com
cd /var/www/example.com
git clone https://github.com/your-org/your-app.git app
cd app
Replace the example domain and repository URL. For a private repository, use a read-only deploy key with access limited to that repository.
Install exactly the dependency versions recorded in the lock file:
npm ci
Create the production environment file using the variable names required by the project:
nano .env.production
chmod 600 .env.production
Variables prefixed for browser exposure are included in client-side bundles and must be treated as public. Database credentials, signing keys, API secrets, and private tokens must remain server-only.
4. Build and test Next.js in production mode
Build the optimized production application:
npm run build
A production build catches missing variables, type errors, invalid imports, and route-generation failures before traffic is switched. Start the app temporarily on the loopback interface:
npm run start -- --hostname 127.0.0.1 --port 3000
From a second SSH session, verify the local response:
curl -I http://127.0.0.1:3000
Stop the temporary process after the test. Never use next dev for production traffic; it is a development server with different performance and security characteristics.
5. Keep Next.js running with PM2
Install PM2 for the deployment user's active Node.js version:
npm install --global pm2
Create ecosystem.config.js in the project directory:
module.exports = {
apps: [{
name: 'example-nextjs',
cwd: '/var/www/example.com/app',
script: 'npm',
args: 'start -- --hostname 127.0.0.1 --port 3000',
env: {
NODE_ENV: 'production'
},
autorestart: true,
max_memory_restart: '1G'
}]
};
Start the process and inspect its state:
pm2 start ecosystem.config.js
pm2 status
pm2 logs example-nextjs --lines 100
Make the saved process list return after a reboot:
pm2 startup
pm2 save
The pm2 startup command prints a system-specific command. Review it, then run that exact command with sudo. If the Node.js installation path changes during an upgrade, regenerate the PM2 startup integration.
Start with one application instance unless you have designed shared caching and revalidation coordination. Multiple independent Next.js instances can otherwise disagree about cached or invalidated content.
6. Configure Nginx as a reverse proxy
Create a dedicated server block:
sudo nano /etc/nginx/sites-available/example.com
Use this configuration as a starting point:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_buffering off;
proxy_read_timeout 60s;
}
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
Disabling proxy buffering allows streamed server-rendered responses to reach the browser progressively. Set the upload limit and timeouts to match the application's real requirements rather than making them unnecessarily large.
Enable the site, test the full configuration, and reload Nginx:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx
If nginx -t fails, do not reload. Correct the reported file and line first.
7. Enable HTTPS with Certbot
Before requesting a certificate, verify that the A and AAAA records point to this VPS and that port 80 is reachable. Install Certbot using its current official instructions for Ubuntu, then run:
sudo certbot --nginx -d example.com -d www.example.com
Choose the HTTP-to-HTTPS redirect. Confirm certificate installation and simulate renewal:
sudo certbot certificates
sudo certbot renew --dry-run
Investigate a failed dry run immediately. Common causes include incorrect DNS, blocked port 80, a stale IPv6 record, or an Nginx server name that does not match the requested domain.
8. Deploy updates safely
Do not update production with an unchecked git pull followed by an immediate restart. Build first, preserve a rollback point, and switch only after validation.
cd /var/www/example.com/app
git fetch --all --prune
git checkout main
git pull --ff-only
npm ci
npm run build
pm2 reload ecosystem.config.js --update-env
Run database migrations according to the application's compatibility plan. A safe migration should work with both the old and new application versions during the deployment window.
For stronger rollback guarantees, deploy each revision into a versioned release directory and point a current symlink at the active release. Keep at least one known-good build and document the exact rollback command.
9. Monitor the production application
Check application and proxy health after every release:
pm2 status
pm2 logs example-nextjs --lines 200
sudo tail -n 200 /var/log/nginx/example.com.error.log
curl -I https://example.com
- Watch HTTP status codes, latency, CPU, memory, disk usage, and restart count.
- Create an external uptime check for a lightweight health endpoint.
- Alert before the disk fills with application or Nginx logs.
- Test backups by restoring them to an isolated environment.
- Re-run the certificate renewal dry run after major proxy or DNS changes.
Common deployment problems
Nginx returns 502 Bad Gateway
PM2 may not be running, the app may have failed during startup, or Nginx may target the wrong port. Check pm2 logs, then test curl http://127.0.0.1:3000 directly on the server.
Next.js reports that no production build exists
Run npm run build in the same project directory used by PM2. Confirm the deployment user can read the generated .next directory.
Environment variables do not update
Build-time variables require a new build. Runtime variables require a process reload with the updated environment. Keep a documented list of which variables are public, build-time, and server-only.
Static assets load from the wrong deployment
A rolling multi-instance deployment can create version skew. Ensure each HTML response and its referenced assets remain available together, or use a deployment strategy designed for immutable assets and coordinated releases.
Streaming responses arrive all at once
Check for proxy or CDN buffering between the browser and Next.js. The reverse proxy must permit streaming for Server Components and other progressively rendered responses.
Production checklist
- Use a supported Node.js release and commit the dependency lock file.
- Build successfully before switching traffic.
- Bind Next.js to a private loopback port.
- Place Nginx in front of the application server.
- Run the app as a non-root deployment user.
- Protect secrets and keep client-exposed variables non-sensitive.
- Configure PM2 startup restoration and test a reboot.
- Enable HTTPS and verify automated renewal.
- Monitor logs, resources, uptime, and process restarts.
- Maintain tested backups and a documented rollback path.
Manage Next.js hosting from a consistent control plane
A production Next.js deployment spans domains, certificates, reverse proxies, Node.js processes, logs, resource monitoring, backups, and repeatable releases. Explore Core Panel's server management features, review pricing and trial options, or compare requirements in our guide to choosing a hosting control panel for Node.js and Python.



