How I Deployed a Next.js Website on a VPS

Abdessamad Ely
Abdessamad Ely
Software Engineer
Updated on   •   Next.js

In this tutorial, I will share with you the exact steps I used to deploy my Kolora website which is built with Next.js. We will tackle all the necessary parts of deploying a website to a VPS.

Requirements

The following are the requirements to follow along with this tutorial:

  • A domain name
  • A Next.js website
  • A Linux VPS running Ubuntu

If you don’t yet have a server, I recommend getting a VPS from Hostinger. I’ve been using it for 3 years, and it’s been working perfectly.

Note: The shared commands were tested on Hostinger’s VPS running “Ubuntu 24.04.3 LTS” operating system.

Setting Up our Domain Name

For a domain name to point to our server (VPS), we need to go to the domain DNS settings, and create:

  • An A record with our server IP as value.
  • A CNAME record with a www alias to our main domain.

Each provider may have a different interface for managing DNS records, but all provides the same features, for example I also use Hostinger for my kolora.app domain name, but my DNS records are managed by Cloudflare.

Before connecting to our VPS via SSH, I updated my domain DNS settings to point to my VPS:

Cloudflare A record DNS settings

That’s it for setting up our domain name

Connecting to our VPS using SSH

Most VPS providers offer a way, through their interface, to add your local computer’s public SSH key to the VPS authorized_keys file. This allows you to connect via SSH without entering your password every time.

I already added the public SSH key of my laptop using Hostinger’s dashboard:

Cloudflare A record DNS settings

Go ahead and do the same. Then open a terminal and connect using the default root user with ssh root@host where the host is your VPS IP Address.

Once connected, it’s always good to run apt update and apt upgrade to update packages, and apply any available security patches.

Creating a Non-Root User

It’s recommended to always use a non-root user when deploying applications, so the web server doesn’t have unlimited access to our system.

Once you log in to your VPS, go ahead and run the following commands in order to create a custom Linux user. In my case, I will use kolora as the user’s username.

  • To create a new user
    • adduser kolora
      • You’ll be prompted for a password, to use when login in to your system using the created user
      • Then for general information for the user profile

Because our new user is not a root user, we need to give him permissions to create new directory and apply changes when needed like when clone a new repository or pull new changes.

To do that, let’s create a new directory that will group all our Next.js projects using: mkdir -p /var/www/nextjsproject.

Then, we need to create a new Linux group and give it ownership of our new nextjsproject directory:

  • groupadd nextjsapps: create a new group named nextjsapps.
  • usermod -aG nextjsapps kolora: append the new group to our user’s groups.
  • chown -R :nextjsapps /var/www/nextjsproject : give the new group ownership of our nextjsproject directory.
  • chmod -R 775 /var/www/nextjsproject: give the new group write, read, and execute permissions.
  • chmod g+s /var/www/nextjsproject: makes new files and directories inherit the group ownership of the directory.

Install Nginx on Ubuntu

While we’re still logged in as root, let’s go ahead and install Nginx:

  • apt update make sure all is updated
  • apt install nginx: install Nginx, it will ask for confirmation type y
  • Enable UFW firewall
    • ufw app list: List app recognized by the firewall.
    • ufw allow OpenSSH: allows SSH connections, so you don’t get locked out of the server.
    • ufw allow 'Nginx Full': allows HTTP and HTTPS connections, because we want to redirect HTTP to HTTPS.
    • ufw show added: double check both rules are added.
    • ufw enable: finally enable the firewall.

Install and Set up Nginx as a Reverse Proxy for Next.js

Nginx is needed in order to route HTTP requests to our Next.js server. It works something like:

  1. An HTTP request comes to our VPS (Port 80 or 443 for SSL)
  2. Nginx is listening for HTTP requests to handle
  3. Nginx configuration decides how to handle these requests.

In our Next.js scenario, we will configure Nginx as a reverse proxy, where any request coming to our domain name will be forwarded to the Next.js server to handle it.

Create a new nginx config file with vim /etc/nginx/sites-available/kolora.app . once opened click i then past the following configuration. Then click ESC key and save with :q.

null
server {
	server_name kolora.app www.kolora.app;

	location / {
    proxy_pass http://127.0.0.0:3000;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_cache_bypass $http_upgrade;
	}
}

The main things we need to understand are:

  • Any request will be forwarded to our application running at http://localhost:3000.
  • We set the necessary proxy attributes to support working with web socket stuff.

Now we need to create a symlink of our configuration to be loaded by nginx, and check the configuration syntax:

  • ln -s /etc/nginx/sites-available/kolora.app /etc/nginx/sites-enabled/: create a symlink.
  • rm /etc/nginx/sites-enabled/default: remove default configuration file as we don’t need it.
  • nginx -t: check that we didn’t make a syntax error or a typo.
  • systemctl restart nginx: restart our nginx to load the new configuration.

DigitalOcean’s How to Install and Configure Nginx on Ubuntu tutorial.

Log in to Our Non-Root User

Great, now that our new user has full access to the /var/www/nextjsproject directory, we can go ahead and log out from our root user with exit, then log in with our non-root user with ssh kolora@host.

Note: we don’t need to give this new user root access (or add it to the sudo group), because we will use it only for tasks that don’t require root access:

  • Login: later when we integrate CI/CD
  • Clone and pull new changes
  • Owner of our project’s directory

Generating new ssh keys

Before moving further, let’s start by creating a new SSH keys for the user, so we can use them to clone and pull changes from our project:

  • Make sure you’re logged in with the right user (non-user)
    • whoami: to verify current user (e.g. kolora)
  • ssh-keygen -t ed25519 -C "{replace_with_your_email}"
    • You will be prompted to enter a key name (~/.ssh/id_ed25519), leave default and click enter.
    • You will be prompted to enter a passphrase, also leave it empty and click enter.
  • eval "$(ssh-agent -s)" to make sure the ssh-agent is running
  • ssh-add ~/.ssh/id_ed25519 to add the new SSH private key to the ssh-agent

We need an SSH key without a passphrase because GitHub Actions runs in a non-interactive environment and cannot prompt for user input during deployment.

At this point, we should be good to start using our new SSH keys

Ref: GitHub’s Generating a new SSH key guide.

Setting up Deploy Keys

With deploy keys, our server can access our GitHub repository using SSH.

When using GitHub deploy keys, and we execute a git command similar to git pull , GitHub uses our repository in place of an organization or a GitHub user.

In the previous section, we created a new set of SSH keys for our repository to avoid getting blocked by Error: Key already in use when trying to create a new deploy key for our project.

Because the whole process is automated, we want to avoid being prompted for a username and password when we pull new changes with git pull from our repository.

Let’s create a new Deploy key for our server:

  • Go to your Next.js repository
  • Open the Settings tab, and search for Deploy keys
  • Create new deploy key
    • Give it a title something like: “Linux VPS”
    • Then we use cat ~/.ssh/id_ed25519.pub to get the public key from the SSH keys generated in the previous step
    • Then paste the content into the Key field
GitHub Deploy Keys

You can check if it’s working as expected by running:

  • git pull if you already cloned your Next.js project
  • or git clone git@github.com:{your_username}/{your_repository}.git to clone your project

If all went well, then everything is set, and we can move on to the next step.

Ref: GitHub’s Deploy keys guide.

Cloning Our Next.js Project

First, make sure git is installed on your system. If not, install it with apt install git.

Then, navigate to our nextjsproject directory using cd /var/www/nextjsproject/, and finally clone the project using git clone git@github.com:abdessamadely/kolora.app.git .

Make sure you don’t use the https repository URL, so we can use our deploy keys and avoid being prompted for a password. This is important for our CI/CD workflow later.

You will be prompted to add github.com to your known hosts, once done you should be able to clone your project.

Install Node.js with NVM

  • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash : Install NVM and make it available in terminal
  • source ~/.bashrc: Reload the current shell configuration without reopening the terminal.
  • nvm install --lts : Install LTS version of Node.js

Ref: NVM’s Installing and Updating guide.

Run our Next.js project and setup PM2

Before moving on to the Nginx configuration, we will make sure our Next.js project runs without errors and run it in the background using PM2.

Run Next.js project

The following are the commands needed to install our Next.js NPM dependencies, and build it for production:

  • cd /var/www/nextjsproject/kolora.app: Make sure we are inside our project root directory
  • npm install : Install dependencies
  • npm run build : Build our project for production
  • npm start

If all is good, click “Ctrl + C” to stop Next.js server and let’s continue.

Install and setup PM2

PM2 is process manager for Node.js, we will use it to start our Next.js server and keep track of the npm start process.

  • npm install pm2 -g : Install PM2 globally
  • cd /var/www/nextjsproject/kolora.app: Make sure we are inside our project root directory
  • pm2 start npm --name "kolora" -- start : This will run npm start and lets PM2 manage the process.

You can also do pm2 logs to see the server logs, and curl http://localhost:3000 to make sure the website is responding as expected.

Secure our Domain with Nginx and Let’s Encrypt

First, make sure you’re logged in as the root user.

In our Nginx configuration, we didn’t include listen 80; because it will be added and managed by our Nginx–Let’s Encrypt integration.

Next, install Certbot, and it’s the Nginx plugin using: sudo apt install certbot python3-certbot-nginx.

Once installed, let’s use it to secure our domain using: sudo certbot --nginx -d kolora.app -d www.kolora.app. This step depends on the DNS configuration we did above.

  • You’ll be asked to provide your email to receive expiration notice.
  • Then you have to agree to their policy

Once the SSL certificates are generated correctly, go ahead and verify that the Auto-Renewal is enabled with: systemctl status certbot.timer

Refs: DigitalOcean’s How To Secure Nginx with Let’s Encrypt on Ubuntu tutorial.

What’s next?

First, congrats you can go ahead and visit your website publicly.

Next, it’s always a good idea to automate the deployment process using a CI/CD pipeline, as a next step I have another tutorial for you:

Conclusion

At this point, you should have a basic understanding of how to deploy a Next.js application to a VPS using Node.js and Nginx as a proxy.

Abdessamad Ely
Abdessamad Ely
Software Engineer

I am a web developer from Morocco Morocco flag and the founder of abdessamadely.com, a personal website where I create content to help developers grow and improve their programming skills.