Comprehensive Guide to Deploying a Next.js Application with Nginx, PM2, and Certbot

Fri Jul 12 2024

Hello everyone! In this tutorial, we are going to create a production-ready Next.js application using the app directory, Nginx, PM2, and Certbot to provide an SSL certificate and enable HTTPS. Let's get started!

Prerequisites

First of all, before we start, we will need a Linux-based server. In this tutorial, I will use Ubuntu 22, as it is a popular choice for many users and organizations with many advantages. You can use Digital Ocean, AWS, GCP, Hetzner, or any other cloud provider which offers free credits to get a server with a Linux distribution installed.

Next, we will need a domain name. I will use dev-soproniuk.com (which stands for my blog). However, this option is optional, and you can still deploy your application without a domain. I recommend Cloudflare as a domain registrar because it offers excellent CDN and protection features that you can use right away.

Finally, we will need the Next.js application itself. You can host your code on any popular repository hosting service, such as GitHub or GitLab, to easily make and pull modifications to the code.

Step 1. Installing Node.js, pnpm and Git

To run our Next.js application, we need to install Node.js. I recommend using nvm (Node Version Manager) to install the LTS version of Node.js.

Using apt for this purpose may result in installing an outdated version due to the way Debian and Ubuntu manage their repositories and packages..

lang: text
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v{version}/install.sh | bash
lang: text
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v{version}/install.sh | bash

Replace {version} with the latest version. For me, it was 0.39.7.

Then, run nvm ls-remote to view all available versions for installation. Choose the LTS version and install it. (For me, it was v20.15.1.)

lang: text
nvm install 20.15.1
nvm alias default 20.15.1
node -v

After successfully installing Node.js with npm using nvm, let's install pnpm and pm2 using the npm package manager, which comes with Node.js by default.

lang: text
npm install -g pnpm pm2
pm2 --version
pnpm -v

Finally, let's install Git using apt. Before that, I'll update package index using the apt update command.

lang: text
sudo apt update
sudo apt install git
git --version

Step 2. Running Next.js application

We have already installed all the required packages to run our Next.js app. So, let's begin by cloning a repository and running it on the default port 3000. Later, we will restrict access to that port using the firewall. I will clone the Next.js Blog App repository as an example.

Inside /var/, I have created a new folder named www. Please navigate to that folder and clone the repository there.

lang: text
git clone git@github.com:timtbdev/Next.js-Blog-App.git next-blog-app
cd /var/www/next-blog-app
pnpm install --prod

Note that some applications may require packages from dev dependencies. In such cases, use the pnpm install command without --prod.

After cloning and installing dependencies for our application, create a .env file if it is required. For simplicity, I will copy the contents from .env.example into .env.

Now, let's build our app.

lang: text
pnpm run build

We have already installed pm2, so let's define a configuration file. I recommend using an ecosystem.config file for better organizational purposes, especially when managing multiple applications with PM2.

Let's create this file.

lang: text
sudo nano ecosystem.config.js

Fill the file with the content below.

ecosystem.config.jslang: javascript
module.exports = {
apps: [
{
name: 'site-name',
cwd: '/var/www/site-path', // must have absolute path
script: 'pnpm',
args: 'run start',
env: {
NODE_ENV: 'production',
},
},
],
};

Change the name of your project to an appropriate one and update the 'cwd' parameter with the absolute path to your Next.js project.

Then, run the project.

lang: text
pm2 start ecosystem.config.js

With that, our application is successfully running on port 3000. You can use the pm2 logs blog-app command to view logs for the blog-app application.

pm2 list command

Step 3. Setup firewall rules

Our application is successfully running on port 3000. We can use the public IP address and port 3000 to access our application. However, before accessing the URL, we need to set up the firewall. Many cloud providers (such as AWS or GCP) block access to our server using their own firewall. In that case, we need to manually allow users to connect to the specified port. In my case, I will use ufw to block or allow connections to the server since I don't have a cloud-provided firewall.

First things first, I will enable ufw and add SSH and port 3000 to the allow list.

lang: text
sudo ufw enable
sudo ufw allow ssh
sudo ufw allow 3000
sudo ufw status

We also need to open port 80 for HTTP and port 443 for HTTPS on our nginx server. You can use the following command to allow HTTP and HTTPS requests:

lang: text
sudo ufw allow 'Nginx Full'

And to view all enabled rules, type the next command:

lang: text
sudo ufw status

ufw status command

For now, we have successfully set up firewall rules for port 3000 and for HTTP/HTTPS. The next step is to install Nginx.

Step 4. Setup and configure Nginx for Next.js app

Let's start with installing Nginx.

lang: text
sudo apt update
sudo apt install nginx
sudo systemctl status nginx

The status should be active (running).

active (running) status of nginx service

Next, let's create a configuration file for our website. I'm going to use the subdomain 'blog' of my main domain, dev-soproniuk.com, so I will name the file blog.dev-soproniuk.com

lang: text
sudo nano /etc/nginx/sites-available/blog.dev-soproniuk.com

Apply the following configuration (redirecting to port 3000 on localhost with additional settings). Also, ensure the settings for the 'www' subdomain prefix are included.

lang: text
server {
server_name blog.dev-soproniuk.com;
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header Connection $http_connection;
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_read_timeout 1m;
proxy_connect_timeout 1m;
proxy_pass http://127.0.0.1:3000;
}
}
# for www
server {
server_name www.blog.dev-soproniuk.com;
return 301 https://blog.dev-soproniuk.com$request_uri;
}

Next, add a symbolic link to the sites-enabled directory to enable our site and check the configuration.

lang: text
sudo ln -s /etc/nginx/sites-available/blog.dev-soproniuk.com /etc/nginx/sites-enabled/
sudo nginx -t

Restart nginx if the configuration file and syntax are okay

lang: text
sudo systemctl restart nginx

If the A-record was properly configured, our website should now be available using the HTTP protocol.

Finally, let's block port 3000 using the firewall command ufw delete."

lang: text
sudo ufw status numbered
sudo ufw delete 7

My port was numbered 7.

Step 4. Install and configure certbot for SSL encryption

Let's install Certbot and a package for Nginx.

lang: text
sudo apt update
sudo apt install certbot python3-certbot-nginx

Afterward, we can apply that plugin to our domains.

lang: text
sudo certbot --nginx -d blog.dev-soproniuk.com -d www.blog.dev-soproniuk.com

For the first time, you will be asked to enter an email address and agree to the terms of service. It will also prompt you to indicate whether you would like to receive updates on new products from the Let's Encrypt Foundation. Once completed, the certificates should be successfully installed.

setup ssl encryption for domains

You can also view Certbot's auto-renewal status by typing the command below.

lang: text
sudo systemctl status certbot.timer

Or you can manually run renewal

lang: text
sudo certbot renew --dry-run

Conclusion

In this tutorial, we successfully set up a production-ready Next.js application using Nginx, PM2, and Certbot on an Ubuntu 22 server. By following all the steps, you now have a secure, scalable, and maintainable Next.js application running in a production environment. This setup ensures your application is accessible via HTTPS and is efficiently managed using PM2 and Nginx.