Loading...

Deploying a Nuxt 3 App on Ubuntu with Apache, PM2 & Certbot

Last update: 4/20/2025
Title Image of the post on Deploy Nuxt 3 on Ubuntu with Apache, PM2 & SSL (HTTPS)

Introduction

A Nuxt 3 application can be run in various environments. If you're using a VPS and want to serve your Nuxt 3 application via HTTPS (port 443) alongside other websites or applications, you cannot directly bind Nitro — the web server Nuxt 3 builds on — to port 443. Instead, you must use a dedicated web server to handle incoming HTTPS traffic and forward it internally to your Nuxt application using a reverse proxy.

In This Guide

This guide walks you through the process of deploying a Nuxt 3 application on a VPS (Ubuntu Server 22.04 LTS). The Nuxt 3 process will be managed by the PM2 process manager, and served securely using Apache HTTP Server as a reverse proxy. You'll also configure free HTTPS certificates using Certbot and Let’s Encrypt, ensuring your application is served over a secure connection.

By the end, your Nuxt 3 app will be accessible via HTTPS, and automatically restarted on system reboots by systemd.

Stack

  • Nuxt 3.16.1
  • PM2 process manager
  • Apache HTTP Server 2.4
  • Certbot (Let’s Encrypt)
  • Ubuntu Server 22.04 LTS

Prerequisites

  • A VPS or dedicated server running Ubuntu Server 22.04 LTS
  • A user with sudo privileges
  • A registered domain with DNS pointing to your server’s IP
  • Node.js and npm installed
  • UFW (Uncomplicated Firewall) installed and properly configured
  • Understanding of Nuxt 3 and its rendering modes
  • Basic knowledge of Linux terminal, Apache and ufw

Creating a User and Preparing the Server

For security reasons, we don’t want to run the Node.js processes as a root user. Instead, we want to run it with a dedicated user with limited permissions, and its own directory to hold the application. Therefore, create a user called nodeuser with the following command:

sudo adduser nodeuser

This command creates a new user and prompts you to set a password along with some optional details. It also generates a home directory at /home/nodeuser, which is owned and writable only by the nodeuser. We'll use this home directory to host the Nuxt 3 application.

To keep things organized, create a subdirectory within it specifically for your application’s source code and its corresponding PM2 configuration file. We will name this subdirectory after the domain under which the app will be accessible. For example, if your domain is yourdomain.com, the structure would look like this:

cd /home/nodeuser
mkdir yourdomain.com

Build and Uploading the Nuxt 3 Application

Run the Nuxt 3 production build:

npm run build 
# or 
npx nuxi build

Upload the resulting .output directory to /home/nodeuser/yourdomain.com via SFTP or your CI/CD pipeline. Ensure nodeuser has execute permission for /home/nodeuser/yourdomain.com/.output/server/index.mjs. That is the file to start the Nitro server, which Nuxt 3 builds on.

Installing and Configuring PM2

While it’s technically possible to run Node.js processes without a dedicated process manager, it quickly becomes impractical - in particular when running multiple Node.js processes. PM2 is a popular process manager for Node.js applications. It greatly simplifies things by providing features such as automatic restarts on crashes, system reboots, and monitoring of your applications.

PM2 can be installed globally via npm:

sudo npm install -g pm2

To launch your Nuxt application, you could technically start the server directly using:

pm2 start .output/server/index.mjs

However, it's recommended to use a dedicated configuration file instead, giving your greater flexibility. We'll create a file ecosystem.config.js to hold this configuration:

cd /home/nodeuser/yourdomain.com
nano ecosystem.config.js

Copy the following code to the file and save the changes:

ecosystem.config.js
module.exports = {
  apps: [
    {
      name:yourdomain.com,
      port: '3000',
      script: './.output/server/index.mjs'
    }
  ]
}

The configuration defines the following:

  • name: How the app will be identified in the PM2 process list.
  • port: The port your application will listen on. The incoming requests will be proxied to this port later.
  • script: The entry point for your Nitro server, relative to the config file location.

Now launch your app with the following:

pm2 start ecoystem.config.js

PM2 outputs a short status report. The status column should now be online. If this is not the case, please open the PM2 logs under /home/node/.pm2/logs/yourdomain.com-err.log and check for any errors.

If your application crashes during runtime, PM2 will automatically restart it. To ensure, PM2 also starts your applications after a server reboot, we need to generate a startup script with:

pm2 startup

PM2 detects your system’s init system (systemd) and generates a command for you to execute. This command sets up PM2 to start automatically on system boot.

Output
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u nodeuser --hp /home/nodeuser

Copy the command and execute it:

sudo env PATH=$PATH:/usr/local/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u nodeuser --hp /home/nodeuser

Next, execute the following command to save the list of running PM2 processes (only your Nuxt app at the moment) so they will restart on system boot.

pm2 save

You can now start PM2 as a service and verify its status with the following commands:

sudo systemctl start pm2-nodeuser
sudo systemctl status pm2-nodeuser

The Nuxt 3 app is now running and listening on 127.0.0.1 at port 3000, but it is not yet accessible through your public domain (yourdomain.com). To make your application publicly available, the next step is to set up Apache as a reverse proxy.

Installing Apache HTTP Server

Update the repository of Ubuntu’s package manager apt and install the Apache2 HTTP Server:

sudo apt update
sudo apt install apache2 -y

The -y flag automatically answers yes to the prompts during installation.

Enable and start the web server with the following commands:

sudo systemctl enable apache2
sudo systemctl start apache2

The first command advises the system to start Apache after a system boot.

Apache’s functionality can be extended through modules. To configure a virtual host as a reverse proxy, we need to ensure that the following modules are enabled: proxy, proxy_http, ssl, rewrite, headers:

sudo apache2ctl -M

If any of the required modules are missing from the list, enable them using the following commands:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod ssl
sudo a2enmod rewrite
sudo a2enmod headers

The modules explained:

  • proxy and proxy_http are required for enabling the reverse proxy functionality over HTTP.
  • ssl enables Apache to handle HTTPS connections.
  • rewrite is required to redirect HTTP requests (port 80) to HTTPS (port 443).
  • headers lets you modify the HTTP headers.

To enable the modules, you must restart Apache:

sudo systemctl restart apache2

Configuring Apache Virtual Host

Apache can host multiple websites and applications—referred to as hosts—by setting up so-called virtual hosts (vhosts). Each vhost defines the configuration for a specific domain or subdomain. On Ubuntu systems, Apache vhost configuration files are located in /etc/apache2/sites-available.

To make your Nuxt 3 app accessible via its domain, you’ll need to configure a vhost to act as a reverse proxy. A reverse proxy is a server that sits in front of another server—in this case, your Nitro server, which runs Nuxt. When a client sends a request, Apache forwards the request to the Nitro server. The Nitro server responds, and Apache then returns that response to the client.

Let’s create a new vhost configuration file for our application (we'll call it yourdomain.com.conf):

sudo nano /etc/apache2/sites-availables/yourdomain.com.conf

Add the following configuration:

<VirtualHost *:80>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com

    <IfModule mod_ssl.c>
        <IfModule mod_rewrite.c>
            RewriteEngine on
            RewriteRule ^(.*)$ https://www.yourdomain.com$1 [R=301,L]
        </IfModule>
    </IfModule>
</VirtualHost>

<VirtualHost *:443>
    ServerName yourdomain.com
    ServerAlias www.yourdomain.com

    <IfModule mod_rewrite.c>
        RewriteEngine on
        RewriteCond %{HTTP_HOST} !^www.yourdomain.com$
        RewriteRule ^(.*)$ https://www.docs4.dev$1 [R=307,L]
    </IfModule>

    ProxyPreserveHost on
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    <IfModule mod_headers.c>
        Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    </IfModule>

    ErrorLog /path/to/your/log/error.log
    CustomLog /path/to/your/log/access.log combined


</VirtualHost>

This configuration sets up two virtual hosts:

Port 80 - HTTP Redirect

  • This block handles plain HTTP requests on port 80.
  • ServerNameand ServerAlias define which domains this configuration applies to both the non-www and the www version.
  • If the required ssland rewrite modules are enabled, all incoming requests are redirected to HTTPS using a permanent redirect 301.
  • ^(.*)$ is a regular expression that captures the full path of the URL, and $1 appends it to the destination URL starting with the HTTPS protocol, and www.
  • L-flag advices Apache to stop processing any more rewrite rules if this rule matches.

Port 443 - HTTPS Reverse Proxy

  • This block handles secure HTTPS traffic on port 443.
  • Since we want all requests to use the fully qualified domain (including www), we use the directive RewriteCond %{HTTP_HOST} !^www.yourdomain.com$ to check whether the request was made without the www prefix. If it wasn't, we redirect it to the correct www version—similar to how we handle redirection on port `80.
  • ProxyPreserveHost On preserves the original Host header from the client. This is optional, but useful if your backend app needs to access it.
  • ProxyPass / http://127.0.0.1:3000/ enables the reverse proxy mode and forwards all incoming requests (to any path/) to the Nuxt 3 app running locally on 127.0.0.1:3000
  • ProxyPassReverse / http://127.0.0.1:3000/ ensures that any URLs in the HTTP response headers from the Nuxt app are rewritten appropriately. This is essential for maintaining the illusion that the app is being served directly from your domain, effectively hiding the fact that it's running behind an Apache reverse proxy.
  • Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains“ is a security mechanism. It instructs browsers to always use HTTPS for this domain, including subdomains for one year.
  • At the end of the configuration file, define your access and error logs as needed.

Next, check your config for syntax errors:

sudo apache2ctl configtest

If your configuration looks good, enable the new vhost and reload the Apache:

sudo a2ensite /etc/apache2/sites-available/yourdomain.com.conf
sudo systemctl reload apache2

Setting up SSL Certificate with Certbot

To make the configuration work properly, we still need a valid SSL certificate. We'll use Certbot, a free and widely used tool that automates the process of obtaining and installing SSL certificates from the Let’s Encrypt certificate authority.

Certbot not only requests the certificate but also updates your Apache vhost configuration automatically to enable HTTPS.

Install Certbot and the Apache plugin with:

sudo apt install certbot python3-certbot-apache -y

Since the Nuxt 3 app — or to be more precise, the Nitro server — runs behind a reverse proxy, it’s Apache that handles SSL handshakes with the client. Apache is therefore responsible for presenting the SSL certificate and establishing a secure connection (HTTPS).

By using Certbot, this setup becomes much simpler, as it takes care of certificate generation, installation, and automatic renewal.

Before we request the certificate and test the connection, ensure that the required ports 80 and 443 are open with:

sudo ufw status

If everything is set up correctly, request the SSL certificate with:

sudo certbot —apache -d yourdomain.com -d www.yourdomain.com

Certbot uses the --apache plugin to automatically detect the appropriate virtual host configuration for the domains specified via the -d flags. It then integrates with Apache to perform a domain validation challenge, confirming your ownership of the domains before issuing the certificate. During the process, Certbot will prompt you for a few options (such as whether to redirect HTTP traffic to HTTPS). Since we already configured a redirect, you can answer this question with no. After successful validation, it will automatically update your Apache configuration by appending the following lines to the <VirtualHost *:443> block:

Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem

Let’s Encrypt certificates are valid for 90 days. To avoid expiration, Certbot installs a renewal job that runs automatically by adding a renewal script to the system’s Cron deamon which is executed twice a day. You can verify the timer’s status with:

sudo systemctl status certbot.timer

Your Nuxt 3 app is now securely accessible at https://www.yourdomain.com. When a user visits your domain without HTTPS or without the www. subdomain, Apache will:

  • Redirect HTTP requests from port 80 to HTTPS on port 443.
  • Redirect non-www requests to https://www.yourdomain.com.

At port 443, Apache acts as a reverse proxy, forwarding the request to the Nuxt 3 app application running on 127.0.0.1:3000 which is managed by PM2.

Summary

This guide explains how to securely deploy a Nuxt 3 application using PM2 and Apache on an Ubuntu 22.04 server. Instead of exposing Nitro (Nuxt’s web server) directly to the internet, Apache with a virtual host is configured as a reverse proxy to forward HTTPS requests to Nitro. Additionally, Certbot is used to request and install a free SSL certificate from Let’s Encrypt with auto-renew functionality. PM2 is used to handle the Node.js-based process of Nuxt, ensuring restarts on crashes or server reboots. Further, the process is securely run by a dedicated Linux user with limited permissions. The setup ensures your app runs securely and reliably with minimal maintenance.

Report a problem (E-Mail)