How to Deploy a Next.js App to a VPS: PM2, Nginx and SSL Guide

How does Next.js VPS deployment work?
Next.js VPS deployment means you build the app with the standalone output, copy the result to your server, run the Node process under PM2, put an Nginx reverse proxy in front, and turn on HTTPS with Let's Encrypt. Those five steps form the skeleton of a single-server setup.
We wrote this guide for site owners and developers who manage their own VPS. You can follow the commands in order. We also explain why each step exists, because most problems in a VPS deployment come from a wrong assumption, not from a typo.
We are a digital marketing and web team, not a hosting company. So the explanation rests on the official documentation of Next.js, PM2, Nginx and Certbot. When we were not sure about a command or a version number, we left it out and wrote "current stable release" instead.
The examples use example.com as the domain and 203.0.113.10 as the IP address. Replace them with your own values. Never paste real passwords, keys or production domains into a file you copied from a tutorial or into a repository.
Next.js VPS deployment or Vercel: which should you choose?
It depends on your team and your workload. Vercel is the managed platform from the company behind Next.js, and it takes server maintenance off your hands. A VPS gives you full control, but it also hands you the work of updates, security, backups and monitoring.
The table below shows the axes that matter for the decision. We did not compare prices, because price lists change often. Check the current pricing on each provider's own page.
| Criterion | Managed platform (for example Vercel) | Your own VPS |
|---|---|---|
| Setup effort | You connect the repository and the platform builds | You set up the server, Node, the proxy and SSL |
| Maintenance duty | Server patches belong to the platform | OS and package updates belong to you |
| Cost model | Usually usage-based pricing | Usually a fixed monthly server fee |
| Control | As much as the platform settings allow | Location, configuration and data placement are your call |
| Scaling | The platform handles it | You plan capacity and extra servers |
| Best fit | Small team, fast launch, uneven traffic | Fixed budget, data placement needs, an existing server |
In short, if you cannot spend time on server work, a managed platform is safer. If you already own a VPS or want a flat cost, a VPS makes sense. We also compared hosting options in our guide on how to choose web hosting.
What should you decide before a VPS deployment?
Make three decisions before you start a VPS deployment. First, decide whether your app needs a Node server at all. Second, decide where you will build. Third, decide whether you will run one process or several.
The Next.js documentation also describes static export. If every page can exist at build time, you can serve the output straight from Nginx. However, server-side features, request-time rendering or the default image optimization need a Node server.
- If your pages are fully static, consider static export; you need no Node process and no PM2.
- If you use server components, API routes or request-time data, run a Node server.
- Building on a small VPS can strain memory, so consider building elsewhere and shipping the output.
- Start with one process, because several processes need extra cache and key settings.
Where you build is also a security choice. A build on the server brings compilers and development dependencies onto the production machine. Building elsewhere and shipping only the standalone folder keeps the server lean. That is the route we recommend here, and it makes a small VPS deployment lighter.
This guide covers the most common case, a Node server. Also, the steps follow the approach in the official Next.js self-hosting documentation.
How do you prepare the VPS for Next.js?
Stop working as root first. Create an unprivileged user, run the app with it, and log in over SSH with a key. The commands below assume a Debian-based distribution such as Ubuntu; on other distributions the package manager command differs.
sudo apt update
sudo apt upgrade
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo apt install nginx ufw
Next, turn on the firewall. Open only the SSH, HTTP and HTTPS ports to the outside. The Next.js process will listen on port 3000, but the internet should never reach that port.
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Warning: add the SSH rule before you enable the firewall. Otherwise, you cut your own connection, so order matters. We covered general web vulnerabilities in our article on the OWASP Top 10 web security vulnerabilities.
How do you install Node.js on the server?
There is no single right way to install Node.js. You can use your distribution's package repository, a version manager such as nvm, or the official package sources. Pick the method from the nodejs.org install page, because versions and repository addresses change over time.
Also, check two things when you pick a version. The install page of Next.js states the minimum Node version it needs. Also, for production, prefer the current stable release line (LTS). That way you receive security patches for longer.
After the install, confirm the version. Make sure your development machine and the server use the same one. A version gap is the most common cause of "it worked on my machine" bugs.
node --version
npm --version
How do you create the Next.js standalone output?
Standalone output means Next.js traces which files the app needs during the build and copies only those into a separate folder. According to the official docs, that folder appears under .next/standalone and runs on its own without installing node_modules.
To switch it on, you add one setting to next.config.js.
module.exports = {
output: 'standalone',
}
After the build, Next.js does not copy the public and .next/static folders on its own. The documentation shows how to copy them by hand. Otherwise the page loads, but images and scripts return 404.
npm ci
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
If you use a monorepo, you need one more setting: outputFileTracingRoot. It tells Next.js where the tracing starts. As a result, shared files outside the project folder also reach the output.
How do you start the standalone server by hand?
Start the server by hand and test it before you move to PM2. That way you will not blame the process manager for a plain app error. According to the docs, the PORT and HOSTNAME environment variables decide which port and address the server listens on.
cd .next/standalone
PORT=3000 HOSTNAME=127.0.0.1 node server.js
We chose 127.0.0.1 here. In other words, only the server itself can reach the process. Because Nginx will be the only door to the outside, this is the cleaner choice for security.
Then check the answer from a second terminal. You should see a 200 status code in the header line.
curl -I http://127.0.0.1:3000
If all is well, stop the process and move on to PM2. If not, read the log output first. A missing environment variable and a wrong Node version are the two most frequent causes.
How do you manage environment variables?
Next.js supports both build-time and runtime environment variables. By default, only the server sees them. To expose a variable to the browser, its name must start with NEXT_PUBLIC_, and Next.js inlines those values into the JavaScript bundle during next build.
As a result, changing a NEXT_PUBLIC_ value on the server and restarting the process does not help. You must rebuild. Secret keys must never start with this prefix, because anyone can read them in the browser.
- Keep secrets out of the repository; store them on the server in a file that only the app user can read.
- Use the
NEXT_PUBLIC_prefix only when a value truly has to reach the browser. - After you change a variable, restart the process and test that the new value arrives.
- Do not paste real passwords or keys into sample files, screenshots or chat messages.
According to the documentation, the server evaluates values it reads during dynamic rendering at runtime. As a result, you can run one build output in different environments with different values.
How do you run Next.js with PM2?
PM2 is a process manager that runs Node processes in the background, restarts them when they crash and collects their logs. The ecosystem file from the official docs gathers all settings in one place. So you do not retype a long command every time.
module.exports = {
apps: [{
name: 'site',
script: 'server.js',
cwd: '/var/www/example.com/current/.next/standalone',
max_memory_restart: '500M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '127.0.0.1'
}
}]
}
The max_memory_restart value is only an example; measure your own app's memory use and set it from that. According to the docs, this setting restarts the process once it exceeds the memory you define.
pm2 start ecosystem.config.js --env production
pm2 status
pm2 logs site
A sibling article of ours covers PM2 in more depth. Here we only take the part that Next.js needs.
How does the app come back after a reboot?
PM2 does not start on boot by itself. You need to generate a startup script and save the process list. The PM2 docs tell you to run pm2 startup first, then the sudo command it prints.
pm2 startup
pm2 save
The first command detects your init system (systemd on most modern distributions) and gives you a command to copy and run. Run it exactly as printed; the user name and the Node path are already inside it.
Then the second command saves the list of running apps. You must save the list again after you add or remove an app. Finally, to test it safely, reboot the server once and watch the app return on its own.
Can you use systemd instead of PM2?
Yes, you can. On modern Linux distributions systemd already is the process manager. So running the Node process from a unit file gives the same result without an extra tool. PM2, on the other hand, adds convenience for logs and multi-process use.
An example unit file looks like this. You save it as /etc/systemd/system/site.service and then enable it.
[Unit]
Description=Next.js site
After=network.target
[Service]
User=deploy
WorkingDirectory=/var/www/example.com/current/.next/standalone
Environment=NODE_ENV=production PORT=3000 HOSTNAME=127.0.0.1
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now site
sudo systemctl status site
Check the full path of Node with which node; the path differs if you use nvm. The choice is a matter of taste. If your team knows PM2, use PM2. If you want a lean setup, systemd fits.
How do you set up an Nginx reverse proxy for Next.js?
The Next.js documentation recommends a reverse proxy such as Nginx instead of exposing the server directly. A proxy absorbs malformed requests, slow-connection attacks, payload limits and rate limiting. Then Next.js can focus on rendering.
On Debian and Ubuntu, you create the site file under sites-available and link it into sites-enabled. A sample configuration follows.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
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;
}
}
After you save the file, test the syntax and then reload the configuration. Testing first keeps one bad setting from taking down the whole site.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Which Nginx settings matter for Next.js?
Three settings matter most. The first is the Host header. According to the Nginx docs, the default value is $proxy_host, so the app may think its own address is 127.0.0.1:3000. That is why we added Host $host above.
The second is the X-Forwarded-Proto header. The app learns from it that the request came over HTTPS. If it is missing, redirects and absolute links may come out with http addresses.
The third is streaming. According to the Next.js docs, the App Router supports streaming, but Nginx buffers responses by default. To keep streaming alive, the docs show how to set the X-Accel-Buffering header to no; you can add it through headers() in next.config.js.
- The default
proxy_read_timeoutin Nginx is 60 seconds according to its docs, so keep that in mind for long requests. - For forms that upload files, check the
client_max_body_sizelimit. - If you add a load balancer, make sure every layer in between passes streamed responses through without buffering.
To verify the setting names against the official page, read the Nginx proxy module documentation.
How do you install a Let's Encrypt SSL certificate?
You can use Certbot for HTTPS. With its Nginx plugin, Certbot gets the certificate and adds the HTTPS block to your configuration. The install method changes by distribution, so choose yours on the Certbot site and follow the current instructions.
You cannot get a certificate until the domain's DNS record points to your server's IP address. First check the A and AAAA records with our DNS lookup tool.
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
According to the Certbot docs, most installs come with automatic renewal, a scheduled task that runs certbot renew regularly. Still, do not skip the renew --dry-run test. You can also look at the output of systemctl list-timers to see whether the timer exists.
After the install, verify the chain with the SSL checker. If you wonder what the certificate does, read what an SSL certificate is.
How does Next.js image optimization work on a VPS?
According to the docs, image optimization with next/image works with no extra setup when you run next start. However, this work uses your server's CPU and memory, because Next.js optimizes images at runtime, not at build time.
When you run the standalone output, check that the sharp package reaches the output; if needed, use the outputFileTracingIncludes example from the docs. The docs also note that glibc-based Linux systems may need an extra memory allocator setting.
- On a small VPS, do not upload huge source images; resize them first.
- If you want a separate image service, you can define a custom image loader.
- You can also turn optimization off and prepare the images yourself.
For the SEO side of image size, see our guide to image optimization for speed and SEO.
How do cache and ISR behave on a single VPS?
Next.js keeps generated pages and data in a server cache. According to the docs, that cache lives on the local disk of each Next.js instance by default. On a single server with a persistent disk, this works with no extra setup.
However, the picture changes if you duplicate the process. Each instance keeps its own cache, so a revalidation on one instance does not reach the other. The fix is a custom cache handler and a shared store such as Redis.
A practical tip for small and mid-size sites: start with one process. Move to several instances only when traffic or the need for zero-downtime updates truly arrives. We also explained the caching logic in our article on how caching works with Redis and Memcached.
How do you set up updates and rollback after a VPS deployment?
For a safe VPS deployment, put every release in its own folder and publish it through a symbolic link called current. When a new release is ready, you switch the link. If something breaks, you return to the old release in seconds.
/var/www/example.com/
releases/
20250101-1200/
20250108-0930/
current -> releases/20250108-0930
Building on your own computer or in CI and shipping the standalone folder also lowers memory pressure on a small server. For the transfer, rsync is enough.
rsync -az --delete .next/standalone/ deploy@203.0.113.10:/var/www/example.com/releases/20250108-0930/
ssh deploy@203.0.113.10 "ln -sfn /var/www/example.com/releases/20250108-0930 /var/www/example.com/current"
ssh deploy@203.0.113.10 "pm2 reload ecosystem.config.js --env production"
For a rollback, point the link at the previous folder and reload the process. After the reload, check the output of pm2 describe site and the response headers to confirm that the right release runs. Keep in mind that this only rolls back code. If you changed the database schema, think ahead about whether the old code works with the new schema. For backups, see our website backup strategy guide.
What changes with multiple instances and zero-downtime reloads?
The cluster mode of PM2 allows zero-downtime reloads. However, the Next.js docs ask for extra settings once you run more than one instance. Otherwise you will see odd errors.
- Start every instance from the same build output; if needed, create a consistent build ID with
generateBuildId. - Give Server Functions the same
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYon every instance, or you will see "Failed to find Server Action" errors. - Use the
deploymentIdsetting to avoid version skew. - Define a cache handler that moves the cache into a shared store.
This list takes its detail from the self-hosting guide of Next.js itself. Read the Next.js self-hosting guide from start to finish before you apply it.
How does server setup affect SEO?
Search engines reach your site through your server. So the server configuration affects visibility indirectly. A slow first response, a wrong redirect and a long 5xx outage are the best-known risks; in other words, a VPS deployment decision is an SEO decision too.
The rule is simple: every URL should open in one canonical form. So set up a single-step permanent redirect between http and https, and between www and non-www. Redirect chains also slow down both visitors and crawlers.
If you do planned maintenance, tell visitors and search engines with the right status code. Showing an empty page with a 200 code during maintenance sends the wrong signal. Besides, a server location far from your audience can lengthen the time to first byte.
We discuss the effect of speed on rankings below. Next.js can send content as HTML thanks to server-side rendering, but that advantage only helps when the server is healthy.
What should you test after a VPS deployment?
Do not look only at the home page after launch. Some checks matter for marketing and SEO. A wrong redirect or a wrong status code can quietly lower search visibility.
- Check with the redirect checker that http goes to https, and www goes to the main host if you want that, in one step.
- Confirm that a URL that does not exist returns a 404 status code.
- Make sure
robots.txtand your sitemap load from the server. - Measure page speed and first-byte time with a Lighthouse performance test.
- Check the browser console for static files that return 404.
We looked at the link between speed and ranking in how site speed affects SEO. A short checklist for after each release keeps you from making the same mistake twice.
How do you set up logs and monitoring?
A live site needs logs, because you cannot debug what you cannot read. PM2 shows the app output with pm2 logs. Nginx keeps access and error logs; on Debian and Ubuntu the default location is the /var/log/nginx/ folder. These files grow over time, so check the log rotation setup.
PM2 has a log rotation module. Before you install it, read the current instructions on the module's official page.
pm2 install pm2-logrotate
pm2 logs site --lines 50
In addition, set up outside monitoring. A simple check tells you that the site is down before your visitors do. For a quick check, you can use the is it down tool.
Also watch the disk. Logs, old release folders and the cache can fill it. So build a simple cleanup habit that keeps the last few releases and deletes the older ones.
What are the most common errors and fixes?
The table below gathers the symptoms people often meet during a deployment and the first place to check. It is a checking order that we derived from the official docs; it may not match every environment.
| Symptom | Likely cause | First check |
|---|---|---|
| 502 Bad Gateway | The Next.js process is down or on the wrong port | pm2 status, pm2 logs and the proxy_pass port |
| Page loads, no styles or images | public and .next/static were not copied | Contents of the standalone folder |
| http addresses in links | X-Forwarded-Proto is missing | The Nginx proxy_set_header lines |
| Failed to find Server Action | Key mismatch between instances | NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| App gone after a reboot | pm2 save or startup was skipped | The pm2 startup output |
| Environment variable shows the old value | The NEXT_PUBLIC_ value sits inside the build | Rebuild |
Still stuck? Collect evidence instead of guessing. Read the process log, the Nginx error log and the request headers side by side. Often one of the three shows the layer where the problem lives. Then change only that layer; changing several settings at once hides what worked.
Work in order: first the process, then the proxy, and finally the domain and the certificate. Skipping the order wastes time, and a rushed VPS deployment usually costs more later.
When should you not do this yourself?
Let us be honest: running a VPS is not right for every team. If nobody will update, watch and back up your server, a managed solution is safer. On a store that takes payments in particular, downtime means lost revenue directly.
- Nobody to track server patches and security updates? Choose a managed platform.
- No on-call routine for outages? Count the risk before you start.
- Law, data placement or a contract that demands a dedicated server? Plan it together with your provider.
- Sudden traffic jumps may overwhelm a single VPS, so plan for headroom.
Want to plan the setup together? Our custom software development service covers projects like this. Containers are another option, and our guide on what Docker is works as a starting point.
What order should you follow for a VPS deployment?
In short, the order for a VPS deployment is: decide, prepare the server, create the standalone output, test by hand, move to PM2, set up Nginx, turn on SSL and build the update flow. Verifying the previous step at each stage keeps mistakes small.
Finally, our most important warning: do not copy commands you do not understand. Check what each command does in the official docs, run it on a test server first and only then apply it to the live site. That lowers both the risk of errors and the downtime.
Also remember that deployment is not a one-time job. Updates, backups and monitoring never stop. If you cannot take them on, a managed solution may be the smarter choice than a VPS.
If you want help with the technical foundation and visibility of your site, you can ask our team. We are glad to look at your setup with you.



