Deploying a Next.js Website to a Virtual Private Server




When I initially began writing this article, after spending a week or so working on the functionality and writing for my renovated Next.js website, I assumed I would be able to deploy it to my existing hosting provider, Bluehost. Bluehost is a web hosting service that is primarily focused on supporting WordPress sites, and as such the tools and processes for deploying other types of applications are obscure and often poorly documented. After several hours of digging, a conversation with their tech support, and a trial run of using the output: 'export' option in Next.js, I concluded that either I'd have to live with substandard features, quadruple my monthly cost with Bluehost, or select a new service provider. I opted to select a new service provider.

A Word About Digital Ocean

My initial research lead me to Digital Ocean, which seemed like a good choice throughout the initial deployment to the VPS. Everything was reasonably smooth and straightforward, and easy to provision. However, when I then moved to the process of preparing to change the DNS over for my domain name, and investigating what I would need to do to swap my email over, I came across some very concerning information that lead me to rethink this decision. Unfortunately, entire blocks of Digital Ocean's available IP ranges are blacklisted by multiple spam prevention lists due to the convenience of setting up a mail server on their VPSs. (If you're curious about your VPS's specific IP, you can check sites such as UCEPROTECT, who runs one of the biggest blacklists that gets used by many organizations, or multirbl which checks the IP against a broad array of blacklists to see if it is listed.) From my reading, it seems that even if you do not serve email through their IP addresses, simply associating your domain with Digital Ocean can cause your domain to get blacklisted from some providers. This doesn't seem worth the risk to me, and for this reason I do not recommend using Digital Ocean as your VPS provider. However, I am leaving the tutorial available as it should apply equally well to any VPS hosting provider.

Setting Up The Virtual Private Server

When you first set up an account with a VPS provider, they should walk you through setting up your first virtual private server. For this tutorial, we'll be using a minimal VPS with largely default features.

VPS Creation Steps

These steps and screenshots demonstrate the process via Digital Ocean. Other providers will have similar options depending on the specifics of their service.

  1. Select your region and datacenter. Here I have selected San Fransisco and left the datacenter dropdown on default. alt text
  2. Select an image for the VPS. For easy deployment, we will want one that already comes with Node.js installed on it. This is as easy as searching for Node.js in the "Marketplace" tab of the image selection section. alt text
  3. Select the size you would like for the VPS. Here we are just using the default smallest size, which comes with the Basic Plan's Shared CPU, 10GB of SSD storage, 512MB of RAM, and a total transfer bandwidth of 500GB. If you know you have higher requirements, feel free to adjust the details here. alt text
  4. Choose an authentication method. Using an SSH key is strongly suggested. The rest of this tutorial assumes you have set up SSH rather than opting to use a password based login.
  5. Select any additional services such as metrics and backup that you want on your server. These are entirely up to your needs on the server.
  6. Finalize details. Here you can adjust how many instances of the VPS you want to deploy, and give your VPS a name and tags. You can also select which project to add it to if you have more than one project set up.

Deploying The Site On The VPS

There are a few things we will want to take care of here. First, we need to get the code onto the VPS, all the npm dependencies installed, and the website built. Then, we will need to swap the configuration of pm2 (which comes pre-installed) to run the Next.js server instead of its default Node server. We will run these commands in the terminal via ssh.

ssh into your virtual machine using the root account.

From the terminal, run:


Or, to select a specific SSH private key file:

ssh -i ~/.ssh/custom_rsa

On successful login, your terminal will update to show root@name-of-vps as the user, and display some welcome messaging from the operating system (including active IP addresses) and possibly your hosting provider. The first thing you will want to do is apply any updates, especially security updates, by running apt update followed by apt upgrade (You may see an alert about how many packages are available to update). Your provider may mirror the packages, which makes this process faster, but it still may take a few minutes depending on how many packages need to be installed or upgraded.

Pick a location to clone your Next.js app into.

This is entirely up to you, but for a bit of guidance, the Filesystem Hierarchy Standard specifies /srv as the location for "Site-specific data served by this system, such as data and scripts for web servers, data offered by FTP servers, and repositories for version control systems (appeared in FHS-2.3 in 2004)." Another common location for content that is served to the web is /var/www/. However, neither are strictly required by the NGINX setup this image comes with.

One word of caution: never use / or /root for this. You don't want your web server files to live in the same place as your user logins and other sensitive data.

Clone your repository.

If the repository is private, you need to set up access to clone the repository into this VPS. Assuming the repository is hosted on GitHub, creating a personal access token and cloning via HTTPS is one way to get the required access without setting up personal SSH keys on the VPS. Using a fine-grained access token even allows you to restrict the token specifically to the repository you are working with. Remember to select the "Contents" permission from the list of possible permissions for this repository if you go this route.

Install dependencies and build the project.

Change directory into the newly cloned repository, then run:

npm ci

Followed by:

npm run build

Install may take a few minutes, and due to the low CPU and memory available on the low cost VPS, build also takes a few minutes.

Turn off the hello app.

If you don't do this before you start your Next.js server, Next.js will attempt to start on the same port and quit with an error rather than starting successfully. Alternatively, if you want to keep hello or another app running on port 3000, you can start your Next.js server in the next step by adding a specific port, instead. If you're logged in as the root user, use:

sudo -u nodejs pm2 delete hello

Run the server.

In order to run the server and set up automatic restart when the VPS reboots, this VPS uses the pm2 (Process Manager) command. From the directory where you cloned and built your project (and assuming you are using the default commands in package.json), we'll run Next.js start with this command to give it a name of next-js in the pm2 interface:

sudo -u nodejs pm2 start npm --name "next-js" -- start

For variations on pm2 commands you might want to use here, this Stackoverflow answer goes into more detail.

Save your pm2 configuration.

Verify that pm2 is running the correct processes:

sudo -u nodejs pm2 list

Then save the active list:

sudo -u nodejs pm2 save

If Next.js shows an error in the information returned from pm2, for example if you tried to start it before removing hello and it quit with the port error, you can restart it (assuming you named it "next-js" as shown in the previous step):

sudo -u nodejs pm2 restart "next-js"

Edit NGINX configuration.

If you reload your site at your IP address now, you will already see your Next.js app, as it runs internally on the same port that the hello script was using. However, there are additional things to edit in the config file. To edit the config (for the default server, which comes as the hello app in this image) using nano, run (replacing nano with your favorite command line text editor if desired):

nano /etc/nginx/sites-available/default

There are several things to particularly pay attention to in this file.

The root and asset location directives should be updated to match the location of your static files and assets to let NGINX serve your static files. You can proxy everything to Next.js, however letting NGINX serve them directly when possible will be faster. See more info from NGINX.

Configure the root directive to match the Next.js public directory (in this example we cloned the your_site repo to the /srv folder):

root /srv/your_site/public

Configure the location directive for the images directory to point to the images directory in the Next.js repository, using the try_files directive:

location /images {
  try_files $uri $uri/ @proxy;

Then ensure you have your root directory configured to use your proxy setup also:

location / {
  try_files $uri @proxy

Then set up the named location for proxy configuration:

location @proxy {
  # proxy config here

The proxy configuration itself likely may be reused from the hello app with few or no changes.

To test that it is working correctly, you can load your site and verify a) everything that is present loads as expected, and b) that you get the correct 404 page from Next.js, and not a 500 error, if you attempt to view a image that does not exist.

Finally, the server_name field, which should be updated to your domain address or an appropriate wildcard.

Once you have finished any configuration updates, verify that the configuration file's syntax is correct with sudo nginx -t, then use sudo systemctl restart nginx to restart the NGINX server.

Congratulations! Your content is now available on your VPS.

Possible Additional Steps

There are some more things you may want to do on your VPS once the site is up and running, such as configuring the firewall and other security, setting up non-root users for everyday use, or configuring NGINX logging. We won't be covering the details here, but you may find these pages useful:

This post is tagged: