Deploying a Next.js Website to a Virtual Private Server
Anna R Dunster
Posted on September 25, 2023
Originally published September 14, 2023 on my personal site.
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.
- Select your region and datacenter. Here I have selected San Fransisco and left the datacenter dropdown on default.
- 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.
- 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.
- 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.
- 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.
- 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:
ssh root@your.ip.here
Or, to select a specific SSH private key file:
ssh -i ~/.ssh/custom_rsa root@your.ip.here
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.
NOTE:
Make sure you have the right user when working withpm2
. It's easy to miss an important piece of information here: by default, thepm2
process belongs to thenodejs
user. If you runpm2
again as root, you'll end up with two instances running. Be sure to either log in as thenodejs
user (instructions to find the login data are in the welcome message), or usesudo -u nodejs
before anypm2
commands to avoid this.
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.
NOTE:
For more detail on what you might want to change in NGINX config, a good place to start is NGINX's Pitfalls and Common Mistakes article.
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:
- Configure NGINX Access and Error Logs (Note this page is documentation for NGINX +, so double check if any particular feature you want to use is supported by the base version before use.)
- How To Secure Your Anonymous VPS on bitlaunch.io
- How to set up continuous deployment of a website on a VPS using GitHub Actions
Posted on September 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.