Congratulations on reaching the deployment stage with your Next.js app! If you’re looking to steer away from major cloud providers, such as Vercel, Amazon Amplify, or Netlify, either due to cost considerations, privacy concerns, or a desire for more control, you’re in the right place.

This article is your go-to resource for learning how to self-host or deploy your Next.js app on a virtual machine running Ubuntu. Our approach involves leveraging the Ubuntu operating system for your server, configuring the powerful NGINX web server to manage incoming traffic, and implementing PM2 to efficiently handle your Next.js application.

By the end of this tutorial, you’ll have a fully operational Next.js app accessible through your personalized domain. Just to clarify, I’ll be utilizing a Virtual Machine (VM) from Vultr throughout the guide. However, you’re free to opt for other VM providers like DigitalOcean, Linode, Amazon Lightsail, Microsoft Azure, or Google Cloud based on your preferences.

I’ve chosen Vultr primarily because they offer an appealing free tier, providing you with a $100 credit to explore their platform. To get this $100 test bonus, simply follow this link. Rest assured, I’ve been a satisfied user of their services for years, and I can vouch for their reliability and performance.

More practice:

How to Deploy or Self-Host a Next.js Application


Before proceeding with this article, ensure you have the following prerequisites in place:

  • GitHub account – We’ll be using GitHub to host our project and later pull the repository onto the VM.
  • VPS (Virtual Private Server) – This will serve as the hosting environment for our Next.js application. If you don’t have a VPS yet, no worries—I’ve dedicated a section that will guide you step by step on acquiring one.
  • Domain name – To access the Next.js application, you’ll need a domain name instead of relying on the IP address of the VM.

Setting Up the Next.js Application

I’ll assume you already have a Next.js project that you intend to deploy to production on a Virtual Machine. However, if you don’t have one, don’t worry—I’ve got you covered. Follow these steps to obtain a Next.js project from a previous article titled ‘How to Setup Redux Toolkit in Next.js 13 App Directory‘.

To accomplish this, clone the repository from Once cloned, open it in your preferred text editor and review the files to gain a brief understanding of the project. Execute the following commands to perform these steps:

  1. Clone the repository to your local machine:
    git clone
  2. Change your current working directory to the cloned repository:
    cd nextjs13-redux-toolkit
  3. Open the repository in Visual Studio Code:
    code .
  4. Install the required dependencies:
    yarn install
  5. Run the project:
    yarn dev

Setting up our Virtual Machine (VM)

Here’s a detailed, step-by-step guide on provisioning a VM through Vultr. While these steps are specific to Vultr, they should closely resemble the process on other platforms. Additionally, the choice of your VM provider won’t impact the deployment steps for the Next.js app, so feel free to use any VM provider you have access to.

  1. Sign In or Sign Up: Go to If you’re new to Vultr, take advantage of this link to sign up and receive $100 in credits for testing the platform. For existing users, simply login to your account.
  2. Initiate Server Deployment: Navigate to the dashboard, hover over the “Deploy” button, and select “Deploy New Server”.
    Click on the deploy button on Vultr and select add a new server
  3. Choose Server Type: Opt for the server type that aligns with your needs and budget. For instance, if you prefer “Cloud Compute“, navigate to the “CPU & Storage Technology” section and select “High Frequency”.
    Choose the Clould Compute
  4. Select Server Location: Choose a server location that is closest to your customers or users for optimal performance.
  5. Select Server Image: Opt for the Ubuntu image and select version 22.04 LTS x64 for your server.
    select ubuntu 22.04 LTS x64
  6. Select Server Size: Choose the server size that aligns with your budget. For this project, the $12 plan, offering 1 virtual CPU, 2GB memory, and 3TB bandwidth, is sufficient.
    Choose the VPS size based on the pricing
  7. Configure Auto Backups: If auto backups are unnecessary for your setup, feel free to disable this feature.
  8. Specify Server Hostname: Provide a name for your server, and upon completion, click the “Deploy Now” button to initiate the server deployment.
    enter a name for the VPS

Pointing Domain Name to the Server

After provisioning the Virtual Machine, you’ll be provided with an IP and a server password. To avoid accessing the Next.js website through the server’s IP, it’s crucial to associate a domain name with the server. While I manage my DNS records through the Ezoic platform, you can handle them directly on Namecheap or Cloudflare, and the steps should be similar.

If you don’t have a domain yet, you can easily obtain one from Namecheap. After purchasing the domain, follow these steps to link your domain to the VM.

  1. Log in to your Namecheap account: Open the Namecheap website and log in to your account.
    enter your credentials on the namecheap login page to sign in
  2. Access Domain List: In the upper left corner, hover over your username and click on “Dashboard”.
    click on dashboard in the upper left corner
    Locate and click on “Domain List” from the menu.
  3. Select Domain: Find the domain for which you want to add an A Record and click on “Manage” next to it.
    click on manage on the domain
  4. DNS Management: Look for the “Advanced DNS” or “DNS” tab. Click on it to access the DNS management section.
  5. Add A Record: Find the section for adding records. Look for an option like “Add a Record” or “Add New Record”.
  6. Fill in A Record Details:
    • Type: Select “A Record”
    • Host: Input @ for the host
    • Value: Input the IP address of your Virtual Machine.
    • TTL (Time to Live): You can leave the TTL as ‘automatic’.
      add the ip to the a record field
  7. Save Changes: After entering the details, save the changes, which may involve clicking the green check mark.
  8. Verify A Record:
    It may take some time for the new A Record to propagate. Let’s leave it while we continue. You can use an online tool like What’s My DNS to verify the A Record’s propagation status.

Connecting to the Server via SSH and Updating Server Packages

At this point, we are now ready to SSH into the server. Copy the IP of your VM and follow the steps below to SSH into it:

  • Open a Terminal or Command Prompt: On Linux or macOS, you can use the Terminal. If you’re on Windows, you can use either Windows Subsystem for Linux (WSL) or an SSH client like PuTTY.
  • Connect to the Server:
    1. Use the command ssh root@your_server_ip to connect to the server. Replace <your_server_ip> with the actual IP address of your server.

      It’s good practice to create a new user with sudo privileges and disable root login, but for this example, I will use the root since the same steps apply.
    2. Enter the server password when prompted.
  • Update Server Packages:
    1. Once connected, update the package lists to get the latest information about available packages using the command sudo apt update.
    2. Upgrade the installed packages to their latest versions using the command sudo apt upgrade.

      If prompted, simply proceed by accepting the default option, which is to ‘Keep the local version currently installed‘, and press Enter.
      configuring the ssh server

Installing Nginx & Certbot

The next thing we need to do is install Nginx, which will function both as a web server and a reverse proxy for our Next.js app. Additionally, we’ll install Certbot, a tool that will enable us to use Let’s Encrypt to manage SSL/TLS certificates for our domain for free.

To install Nginx and Certbot, run the following command in your terminal:

sudo apt install nginx certbot python3-certbot-nginx

After completing the installations, let’s proceed with basic firewall configurations. We’ll enable Nginx to handle both HTTP and HTTPS traffic and allow incoming traffic on the default SSH port. Execute the following commands:

sudo ufw allow "Nginx Full"
ufw allow OpenSSH
ufw enable

Now that the firewall is active, you can verify its status by accessing the VM’s IP directly in your browser. You should see the default Nginx page.

basic nginx page

Installing NPM and PM2

At this stage, we’ve completed the basic steps, and now we can proceed to install NPM. This will allow us to set up the Next.js project or install the necessary dependencies to run the Next.js app. To do this, run the following command:

apt install npm

The next step is to install a package called PM2. PM2 is a process manager designed to manage and run Node.js applications in production environments. It offers advanced features to guarantee the stability and reliability of your applications. It’s worth noting that while PM2 is often associated with Node.js applications, it is equally applicable to Next.js applications. To install PM2, use the following command:

npm install -g pm2

To verify the successful installation of PM2, execute the command pm2 status in your terminal. You should see an output similar to the screenshot below, confirming that PM2 is up and running.

pm2 status

Installing NVM and Node.js

Now, let’s proceed with the installation of Node.js. There are various methods to install Node.js, but for this guide, we’ll use NVM (Node Version Manager), a tool that allows us to easily switch between different Node.js versions. Execute the following command to install NVM:

curl -o- | bash

To get the latest version of NVM, you can visit their GitHub installation page and copy the curl script provided.

Next, run the following command to install the LTS version of Node.js using NVM:

exec $SHELL
nvm install --lts

To verify if Node.js is installed, execute the command node -v in your terminal. This will display the currently installed version of Node.js.

Creating the Next.js App or Pulling from Git Repository

Now, let’s dive into a crucial step of the tutorial – creating a new Next.js app or pulling one from your GitHub repository. In my case, I’ll be pulling a project we previously built. Start by navigating to the www folder using the command cd /var/www. Once inside, head to your GitHub repository, copy the repo link, and clone it into the www directory. If you don’t have a Next.js project, you can use the following command to clone the project we created in a previous article:

git clone

In this project, we utilized Yarn as our package manager. If you haven’t installed it globally yet, you can do so by running the command npm i -g yarn. Once the installation is complete, proceed with the following commands to navigate into the Next.js project, install all necessary dependencies, and build the project. If you are working with your own project, remember to replace nextjs13-redux-toolkit with the actual name of your project directory.

cd nextjs13-redux-toolkit/
yarn install
yarn build

Configuring NGINX

Moving on, let’s configure Nginx to manage traffic routing to our Next.js server and handle static assets for our Next.js app. To accomplish this, we’ll create a configuration file in /etc/nginx/sites-available. Execute the following command to generate the file and open it for editing. Ensure to replace nextjs13-redux-toolkit with your project name if you’re working with your own project.

cd /etc/nginx/sites-available
touch nextjs13-redux-toolkit
nano nextjs13-redux-toolkit

Next, copy the configurations below and paste them into the nextjs13-redux-toolkit file. Make sure to customize a couple of things in the configurations. First, replace with your domain name. Also, within the location /_next/static/ block, replace nextjs13-redux-toolkit with your project name if you are using your own project.

Once you’ve made these changes, press Ctrl + X, type Y, and press Enter to save the file.

#nginx config file for Nextjs App
#place in /etc/nginx/sites-available/name_of_config_file
server {
        listen 80;

        gzip on;
        gzip_proxied any;
        gzip_types application/javascript application/x-javascript text/css text/javascript;
        gzip_comp_level 5;
        gzip_buffers 16 8k;
        gzip_min_length 256;

        location /_next/static/ {
                alias /var/www/nextjs13-redux-toolkit/.next/static/;
                expires 365d;
                access_log off;

        location / {
                proxy_pass; #change to 3001 for second app, but make sure second nextjs app starts on new port in packages.json "start": "next start -p 3001",
                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;

We are required to copy this Nginx configuration file into the /etc/nginx/sites-enabled/ directory; however, this process can be tedious. To simplify, we’ll make a small modification inside the nginx.conf file, enabling us to keep the configuration only in the sites-available without worrying about copying it into the sites-enabled directory. Execute the following command to open the nginx.conf file.

cd ..
nano /etc/nginx/nginx.conf

Scroll down to the section where you find include /etc/nginx/sites-enabled/*; and modify it to include /etc/nginx/sites-available/*;. After making this change, save the file and execute the command systemctl restart nginx to restart the Nginx server.

cd ..
nano /etc/nginx/nginx.conf

To ensure that we haven’t introduced any errors into the Nginx configuration files, run the command nginx -t. A successful test result should be displayed in the terminal.

success response from nginx

Now, let’s remove the default Nginx configuration file from both the sites-available and sites-enabled directories. Execute the following commands:

cd /etc/nginx/sites-available
rm default
cd .. && cd /etc/nginx/sites-enabled
rm default

After completing the removal, restart the Nginx server by running the command systemctl restart nginx.

Launching Site with PM2

At this stage, we are prepared to launch the Next.js application using PM2. Navigate to the www directory with the command cd /var/www/. Once there, proceed to your project directory. In my case, as my project is named nextjs13-redux-toolkit, I will use the command cd nextjs13-redux-toolkit.

cd /var/www/nextjs13-redux-toolkit

Launch the Next.js application with PM2 by executing the following command. Ensure to replace nextjs13-redux-toolkit with your actual project name if you are working with a different project.

pm2 start npm --name nextjs13-redux-toolkit -- start

At this point, if you try to access the site through your domain, you’ll notice it won’t be reachable because we haven’t obtained an SSL certificate for the domain yet.

the nextjs app is not loading from the domain because of ssl

However, if you visit the IP directly, you should see the Next.js app.

the nextjs app is loading from the ip

Adding SSL to Domain Name

To secure your domain with an SSL certificate, run the following command using Certbot. Make sure to replace with your actual domain name:

sudo certbot --nginx -d

Upon running the command above, you’ll be prompted to answer a series of questions. Provide your email address and answer ‘Y’ to the remaining questions. After completing this process, Certbot will issue the SSL certificate for your domain.

prompts from letencrypt

To ensure the changes take effect, restart Nginx by executing the following command:

systemctl restart nginx

Now that your domain has been issued with the SSL certificate, visiting it in your browser should display the running Next.js app.

the app should work if you visit the domain


Congratulations on completing this tutorial! Throughout the guide, you’ve acquired the skills to deploy a Next.js app on a virtual machine using Ubuntu, Nginx, and Certbot. I trust you found this tutorial both helpful and enjoyable. Should you have any questions, please don’t hesitate to leave them in the comments section. Thank you for reading!