Deploying a Next.js Website to a Virtual Private Server
Tags: typescript, nextjs, virtualmachine, devops
Published: 2023-09-25 01:40:37

Originally published [September 14, 2023 on my personal site](https://www.annardunster.com/posts/2023/deploying-nextjs-application).

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](https://www.reddit.com/r/webdev/comments/n3wf0c/heads_up_if_youre_on_digitalocean_almost_their/) of Digital Ocean's available IP ranges are [blacklisted by multiple spam prevention lists](https://www.digitalocean.com/community/questions/spamhaus-blacklisting) due to the convenience of [setting up a mail server](https://discourse.mailinabox.email/t/digital-ocean-ips-being-blacklisted-by-more-and-more-esps/8502) on their VPSs. (If you're curious about your VPS's specific IP, you can check sites such as [UCEPROTECT](https://www.uceprotect.net/), who runs one of the biggest blacklists that gets used by many organizations, or [multirbl](https://multirbl.valli.org/) which checks the IP against a broad array of blacklists to see if it is listed.) From [my reading](https://news.ycombinator.com/item?id=26064722), 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.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8chv05zs6t3e1p1aeatz.png 'Screenshot of DigitalOcean dashboard showing region and datacenter selection.')
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.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/meumqzq1toximrympxov.png 'Screenshot of DigitalOcean dashboard showing image marketplace search.')
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.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pk4k3tiu2agultt96br7.png 'Screenshot of DigitalOcean dashboard showing VPS size selection.')
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:

```bash
ssh root@your.ip.here
```

Or, to select a specific SSH private key file:

```bash
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](https://en.wikipedia.org/wiki/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](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#not-using-standard-document-root-locations). 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](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-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:

```bash
npm ci
```

Followed by:

```bash
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 with `pm2`. It's easy to miss an important piece of information here: by default, the `pm2` process belongs to the `nodejs` user. If you run `pm2` again as root, you'll end up with two instances running. Be sure to either log in as the `nodejs` user (instructions to find the login data are in the welcome message), or use `sudo -u nodejs` before any `pm2` 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:

```bash
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:

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

For variations on `pm2` commands you might want to use here, [this Stackoverflow answer](https://stackoverflow.com/a/70361967/12869266) goes into more detail.

#### Save your `pm2` configuration.

Verify that `pm2` is running the correct processes:

```bash
sudo -u nodejs pm2 list
```

Then save the active list:

```bash
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):

```bash
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](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/) 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):

```bash
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](https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/) 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](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#proxy-everything).

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

```nginx
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:

```nginx
location /images {
  try_files $uri $uri/ @proxy;
}
```

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

```nginx
location / {
  try_files $uri @proxy
}
```

Then set up the named location for proxy configuration:

```nginx
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](https://nginx.org/en/docs/http/server_names.html).

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](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/) (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](https://bitlaunch.io/blog/how-to-secure-your-anonymous-vps/)
- [How to set up continuous deployment of a website on a VPS using GitHub Actions](https://dev.to/knowbee/how-to-setup-continuous-deployment-of-a-website-on-a-vps-using-github-actions-54im)