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

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
.
nuxt generate
, which are intended for deployment on static hosting platforms and do not require a server runtime.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.
nuxt generate
, which are for static site hosting platforms and do not require a server runtime for Nuxt.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:
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.
[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
PM2
process is already running with ps aux | grep pm2
, terminate them and try restarting again.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
andproxy_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.
ServerName
andServerAlias
define which domains this configuration applies to both the non-www and the www version.- If the required
ssl
andrewrite
modules are enabled, all incoming requests are redirected to HTTPS using a permanent redirect301
. ^(.*)$
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, andwww.
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 directiveRewriteCond %{HTTP_HOST} !^www.yourdomain.com$
to check whether the request was made without thewww
prefix. If it wasn't, we redirect it to the correctwww
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 on127.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.
.htaccess
users: In .htaccess
or per-directory contexts, Apache may strip the leading slash from the URL path. If you're working in such a context, you might need to update the redirect rule to:RewriteRule ^/?(.*) https://www.yourdomain.com/$1 [R=301,L]`.
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 tohttps://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.