Ubuntu 24.04 LTS Server with Nuxt 4 + NGINX + PM2
Tutorial for Windows (WSL2) / Mac / Linux

The Perfect Ubuntu 24.04 LTS Server

NGINX reverse proxy + Node 22 LTS (NVM) + PM2 + Docker

This guide shows you how to deploy a production-ready Nuxt 4 application with Prisma + SQLite on Ubuntu 24.04 LTS. Perfect for developers who want a secure, fast, and scalable web server with modern tooling.

💡 Recommended Hosting Providers:

  • 🇪🇺 Hetzner Cloud - Best value in Europe (from €4.15/month, EU-based, GDPR-compliant, basic DDoS protection)
  • 🛡️ HostUp.se - Swedish VPS with advanced L7 DDoS protection, geo-blocking dashboard, and real-time threat controls (75 SEK/month ≈ €6.50)
  • 🌍 DigitalOcean - Global reach with $200 free credit for 60 days (basic DDoS protection)
  • Linode - High-performance VPS with excellent support (basic DDoS protection)

DDoS Protection Note: Hetzner, DigitalOcean, and Linode offer basic network-level (L3/L4) DDoS protection for volumetric attacks. HostUp.se provides advanced application-layer (L7) filtering with a real-time dashboard to block sophisticated attacks, geo-block countries, and adjust thresholds instantly — similar to what you'd get with Cloudflare but built into the VPS.

Recommended VPS Specs for Nuxt 4 Apps: 2 vCPU, 4-8 GB RAM, 50+ GB SSD, 3+ TB bandwidth — handles moderate traffic well. The HostUp.se VPS SM (2 vCPU, 8 GB RAM, 100 GB NVMe, 5 TB bandwidth, 75 SEK/month) handles most sites to a very good price.

⭐ What You'll Learn:

  • Install and configure Ubuntu 24.04 LTS for production
  • Setup NGINX as a reverse proxy with SSL/TLS (Let's Encrypt)
  • Install Node.js 22 with NVM for multi-version support
  • Deploy Nuxt 4 applications with PM2 process manager
  • Optional: Docker setup for containerized deployments
  • Automated backups, monitoring, and maintenance
  • Upgrade path to Ubuntu 26.04 LTS (April 2026)

Quick checklist

  1. Initial server setup: Update system, configure firewall (UFW), set timezone.
  2. Install NGINX; install Node 22 with NVM (allows multiple Node versions); enable pnpm with corepack; install PM2 globally.
  3. Optional: Install Docker Engine for containerized deployments and production parity testing.
  4. Point DNS to your server; create an NGINX server block that proxies to 127.0.0.1:3000 .
  5. Upload your deploy folder ( .output/ , package.json , pnpm-lock.yaml , ecosystem.config.cjs , prisma/ , scripts/ , .env ).
  6. Install prod deps with lockfile; generate Prisma client; run preflight and preflight:db.
  7. Start with PM2 from the deploy folder; save and tail logs.
  8. Run Certbot for SSL and reload NGINX.
  9. Future: Upgrade to Ubuntu 26.04 LTS (April 2026) when ready.

🎯 Personalize This Tutorial

Enter your information below to customize all commands throughout this tutorial. Commands will update automatically with your values shown in green.

Enter application name without spaces for PM2 process
Enter your primary domain name for NGINX configuration
Optional secondary domain for www redirect
Enter IPv4 address of your Ubuntu server
Enter username for SSH access
Enter deployment folder name
Enter port number for Node.js application

Why Ubuntu 24.04 LTS?

Ubuntu 24.04 LTS is recommended over 25.10:
  • 5 years support until 2029 (vs 9 months for 25.10)
  • Stable and battle-tested for production
  • Direct upgrade path to Ubuntu 26.04 LTS (April 2026)
  • Industry standard for production servers

Ubuntu 25.10 ends support in July 2026, forcing an upgrade at an inconvenient time. With 24.04 LTS, you can upgrade to 26.04 LTS at your convenience after April 2026.

1) Initial Server Setup

# Update system packages
sudo apt update && sudo apt full-upgrade -y

# Install essential build tools
sudo apt install -y curl git wget build-essential ca-certificates gnupg

# Set timezone (adjust for your location)
sudo timedatectl set-timezone Europe/Stockholm

# Configure firewall
sudo ufw allow 22/tcp   # SSH
sudo ufw allow 80/tcp   # HTTP
sudo ufw allow 443/tcp  # HTTPS
sudo ufw enable
sudo ufw status

Security Tip: Consider changing SSH port, disabling root login, and setting up fail2ban. See my previous tutorial.

2) Install NGINX

# Option A: Ubuntu repository (NGINX 1.24+)
sudo apt install -y nginx

# Option B: Official NGINX repository (latest stable 1.27+)
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update
sudo apt install -y nginx

# Start and enable NGINX
sudo systemctl start nginx
sudo systemctl enable nginx

# Verify installation
nginx -v
curl http://localhost  # Should show NGINX welcome page

3) Install Node.js 22 with NVM (Multi-Version Support)

Why NVM? Node Version Manager allows you to install and switch between multiple Node.js versions. Perfect for servers hosting multiple applications with different Node requirements.

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Load NVM (add to ~/.bashrc for persistence)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# Install Node.js 22 LTS
nvm install 22
nvm use 22
nvm alias default 22

# Verify installation
node --version  # Should show v22.x.x
npm --version

# Install multiple versions if needed (optional)
# nvm install 20    # For apps requiring Node 20
# nvm install 18    # For apps requiring Node 18
# nvm list          # Show all installed versions

4) Install pnpm and PM2

# Enable corepack for pnpm
corepack enable
corepack prepare pnpm@latest --activate

# Verify pnpm
pnpm --version

# Install PM2 globally
sudo npm install -g pm2

# Verify PM2
pm2 --version

# Setup PM2 to start on system boot
pm2 startup systemd
# Copy and run the command that PM2 outputs

# Optional: Install PM2 log rotation
pm2 install pm2-logrotate

Per-App Node Versions: Use nvm use 20 before starting apps that need Node 20. PM2 will remember the Node version used when you save the process list.

5) Install Docker Engine (Optional)

Docker Benefits: Test production builds locally in WSL2 with identical setup. Not required for basic deployment, but valuable for containerized apps and testing.

# Install Docker using official script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Add your user to docker group (no sudo needed)
sudo usermod -aG docker $USER

# Logout and login for group changes to take effect
# Or run: newgrp docker

# Start and enable Docker
sudo systemctl start docker
sudo systemctl enable docker

# Verify installation
docker --version
docker compose version
docker run hello-world

# Install Portainer for Docker web UI (optional)
docker volume create portainer_data
docker run -d -p 9000:9000 --name portainer --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:latest

# Access Portainer at http://your-server-ip:9000

6) Configure NGINX Reverse Proxy

Create a server block for your domain and proxy to your Nuxt app:

sudo nano /etc/nginx/sites-available/{{tutorial.domain}}
upstream nuxt_app {
  server 127.0.0.1:{{tutorial.appport}};
  keepalive 64;
}

server {
  listen 80;
  server_name {{tutorial.domain}} {{tutorial.domain2}};

  # Redirect www -> non-www
  if ($host = {{tutorial.domain2 || 'www.' + tutorial.domain}}) {
    return 301 http://{{tutorial.domain}}$request_uri;
  }

  # Root directory for static files (Nuxt .output/public)
  root /var/www/{{tutorial.domain}}/.output/public;

  # Security headers
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-XSS-Protection "1; mode=block" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;

  # Static files from Nuxt build
  location /_nuxt/ {
    alias /var/www/{{tutorial.domain}}/.output/public/_nuxt/;
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
  }

  # Other static assets
  location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
  }

  # Proxy to Node.js application
  location / {
    proxy_pass http://nuxt_app;
    proxy_http_version 1.1;
    
    # WebSocket support (for real-time features)
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
    # Standard proxy headers
    proxy_set_header Host $host;
    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;
    
    # Timeouts
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
  }

  # Health check endpoint
  location /health {
    access_log off;
    return 200 "healthy\n";
    add_header Content-Type text/plain;
  }
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/{{tutorial.domain}} /etc/nginx/sites-enabled/
# Remove default site (optional) sudo rm /etc/nginx/sites-enabled/default # Test configuration sudo nginx -t # Reload NGINX sudo systemctl reload nginx

Note: Adjust paths and domain names to match your setup. After SSL setup, NGINX will automatically update to use HTTPS.

7) Deploy Nuxt Application

A) Create Application Directory

# Create directory and set ownership
sudo mkdir -p /var/www/{{tutorial.domain}}
sudo chown -R $USER:$USER /var/www/{{tutorial.domain}}
cd /var/www/{{tutorial.domain}}

B) Upload or Clone Your Project

# Option 1: Clone from Git
git clone https://github.com/yourusername/{{tutorial.appname}}.git .

# Option 2: Upload deploy folder via SCP/SFTP
# From your local machine (WSL2):
# scp -r {{tutorial.deploydir}}/* {{tutorial.sudouser}}@{{tutorial.ipaddress}}:/var/www/{{tutorial.domain}}/

C) Required Files

Ensure these files/folders are present:

D) Configure Environment

# Create or edit .env
nano .env

Example production .env :

# Database
DATABASE_URL="file:./prisma/db.sqlite"

# Session (generate with: openssl rand -hex 32)
NUXT_SESSION_PASSWORD="your-64-character-hex-password-here"

# Production URL
NUXT_PUBLIC_SITE_URL="https://{{tutorial.domain}}"

# Node environment
NODE_ENV="production"

# Optional: OAuth providers
# NUXT_OAUTH_GITHUB_CLIENT_ID="..."
# NUXT_OAUTH_GITHUB_CLIENT_SECRET="..."

8) Install Dependencies and Setup Database

A) Install Production Dependencies

cd /var/www/{{tutorial.domain}}

# Ensure correct Node version is active
nvm use 22

# Install dependencies (frozen lockfile for reproducibility)
pnpm install --frozen-lockfile

# Or for production-only dependencies:
# pnpm install --prod --frozen-lockfile

B) Setup Prisma and Database

# Generate Prisma Client
pnpm dlx prisma generate

# Run database migrations (if you have them)
pnpm dlx prisma migrate deploy

# Optional: Seed database with initial data
pnpm db:seed

# Verify database connectivity
pnpm run preflight:db

C) Build Application (if not pre-built)

# If you cloned from Git and need to build:
pnpm build

# Verify build succeeded
ls -lh .output/server/index.mjs

D) Run Preflight Checks

# Verify runtime dependencies
pnpm run preflight

# Check database connectivity
pnpm run preflight:db

# Both should complete without errors

9) Start Application with PM2

A) Initial Start

cd /var/www/{{tutorial.domain}}

# Ensure correct Node version
nvm use 22

# Delete existing PM2 process if it exists
pm2 delete {{tutorial.appname}} || true

# Start application using ecosystem config
pm2 start ecosystem.config.cjs --env production

# Save PM2 process list (survives reboots)
pm2 save

# View logs
pm2 logs {{tutorial.appname}} --lines 200

B) Verify Application is Running

# Check PM2 status
pm2 list

# Test application locally
curl -i http://127.0.0.1:{{tutorial.appport}}/

# Should return 200 OK with HTML content

# Monitor application
pm2 monit

C) PM2 Management Commands

# Reload app after code changes (zero-downtime)
pm2 reload ecosystem.config.cjs --env production --update-env

# Restart app (brief downtime)
pm2 restart {{tutorial.appname}}

# Stop app
pm2 stop {{tutorial.appname}}

# View real-time logs
pm2 logs {{tutorial.appname}} --lines 100

# Clear logs
pm2 flush

# Application info
pm2 info {{tutorial.appname}}

Multiple Node Versions: If running multiple apps with different Node versions: nvm use 20 && pm2 start app1.js , then nvm use 22 && pm2 start app2.js . PM2 remembers which Node binary to use for each app.

10) Setup SSL with Let's Encrypt (Certbot)

A) Install Certbot

# Install Certbot with NGINX plugin
sudo apt install -y certbot python3-certbot-nginx

B) Obtain SSL Certificate

# Get certificate for your domain(s)
sudo certbot --nginx -d {{tutorial.domain}} -d {{tutorial.domain2}}


# Certbot will:
# 1. Verify domain ownership
# 2. Obtain SSL certificate
# 3. Automatically configure NGINX for HTTPS
# 4. Set up auto-renewal

# Follow the prompts:
# - Enter email address
# - Agree to terms
# - Choose redirect option (recommended: Yes)

C) Verify SSL Setup

# Test NGINX configuration
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

# Test auto-renewal
sudo certbot renew --dry-run

# Check certificate status
sudo certbot certificates

D) Auto-Renewal

Certbot automatically sets up a systemd timer for renewal. Verify it's active:

# Check renewal timer status
sudo systemctl status certbot.timer

# Manually renew (if needed)
sudo certbot renew

# Renewal happens automatically before expiration

E) Security Headers (Optional but Recommended)

After SSL is set up, enhance security with additional headers. Add to your NGINX config:

# Add to server block (port 443)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Test and reload
sudo nginx -t && sudo systemctl reload nginx

11) Monitoring and Maintenance

A) Install Monitoring Tools

# System monitoring
sudo apt install -y htop ncdu

# Log viewing
sudo apt install -y lnav

# PM2 log rotation
pm2 install pm2-logrotate

B) Regular Maintenance Tasks

# Weekly: Update packages
sudo apt update && sudo apt upgrade -y

# Weekly: Check disk space
df -h
ncdu /var/www

# Weekly: Review application logs
pm2 logs --lines 100

# Monthly: Check for security updates
sudo apt list --upgradable

# Monthly: Verify SSL certificate
sudo certbot certificates

# Monthly: Review PM2 status
pm2 status

C) Automated Backups

# Create backup script
sudo nano /usr/local/bin/backup-{{tutorial.appname}}.sh
#!/bin/bash
# Backup script for {{tutorial.appname}} application

BACKUP_DIR="/backup/{{tutorial.appname}}"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# Backup application files
tar -czf $BACKUP_DIR/app_$DATE.tar.gz /var/www/{{tutorial.domain}}/.output /var/www/{{tutorial.domain}}/prisma

# Backup database
cp /var/www/{{tutorial.domain}}/prisma/db.sqlite $BACKUP_DIR/db_$DATE.sqlite

# Backup environment file
cp /var/www/{{tutorial.domain}}/.env $BACKUP_DIR/env_$DATE

# Keep only last 7 days of backups
find $BACKUP_DIR -name "app_*.tar.gz" -mtime +7 -delete
find $BACKUP_DIR -name "db_*.sqlite" -mtime +7 -delete
find $BACKUP_DIR -name "env_*" -mtime +7 -delete

echo "Backup completed: $DATE"
# Make executable
sudo chmod +x /usr/local/bin/backup-{{tutorial.appname}}.sh
# Add to crontab (daily at 2 AM)
sudo crontab -e
# Add line: 0 2 * * * /usr/local/bin/backup-{{tutorial.appname}}.sh >> /var/log/backup-{{tutorial.appname}}.log 2>&1

12) Upgrade to Ubuntu 26.04 LTS (April 2026)

Timeline: Ubuntu 26.04 LTS releases in April 2026. Wait 1 month for initial bugs to be fixed, then plan your upgrade.

A) Pre-Upgrade Preparation

# 1. Full backup of everything
sudo rsync -av /var/www/ /backup/www/
cp /var/www/{{tutorial.domain}}/prisma/db.sqlite /backup/db-$(date +%F).sqlite
tar -czf /backup/env-$(date +%F).tar.gz /var/www/{{tutorial.domain}}/.env

# 2. Update current system fully
sudo apt update && sudo apt full-upgrade -y

# 3. Remove unnecessary packages
sudo apt autoremove -y
sudo apt autoclean

# 4. Stop application
pm2 stop all

# 5. Test backup restoration (recommended)
# Restore to test server first if possible

B) Perform Upgrade

# Run release upgrade
sudo do-release-upgrade

# Follow prompts (takes 30-60 minutes)
# Say Yes to most questions
# Review config file changes carefully

# System will reboot automatically

C) Post-Upgrade Verification

# After reboot, SSH back in

# Verify Ubuntu version
lsb_release -a  # Should show 26.04

# Check NGINX
sudo systemctl status nginx
nginx -v

# Check Node.js (NVM should preserve versions)
nvm list
node --version

# Check PM2
pm2 list

# Start application
pm2 resurrect  # Restore saved PM2 processes
pm2 start all

# Verify application
curl https://{{tutorial.domain}}
pm2 logs

# Test all features thoroughly

D) If Something Goes Wrong

# If upgrade fails, you can restore from backup
# Boot from rescue mode or previous kernel
# Restore files from /backup/

# If application doesn't start:
# 1. Check PM2 logs: pm2 logs
# 2. Regenerate dependencies: rm -rf node_modules && pnpm install
# 3. Regenerate Prisma: pnpm dlx prisma generate
# 4. Check NGINX config: sudo nginx -t

13) Docker Alternative Deployment (Optional)

Docker Deployment: Instead of PM2, you can run your Nuxt app in Docker containers. Useful for isolation, easier scaling, and consistent environments.

A) Basic Docker Deployment

# Create docker-compose.yml in /var/www/{{tutorial.domain}}
version: '3.8'

services:
  nuxt-app:
    image: node:22-alpine
    container_name: {{tutorial.appname}}-production
    working_dir: /app
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=production
      - DATABASE_URL=file:./prisma/db.sqlite
    command: sh -c "pnpm install --frozen-lockfile && pnpm dlx prisma generate && node .output/server/index.mjs"
    ports:
      - "{{tutorial.appport}}:{{tutorial.appport}}"
    restart: unless-stopped

# Start with Docker
docker compose up -d

# View logs
docker compose logs -f

# Stop
docker compose down

B) When to Use Docker vs PM2

Scenario Recommended Reason
Single Nuxt app PM2 Simpler, lighter, faster restarts
Multiple apps, different Node versions NVM + PM2 Easy version switching
Microservices architecture Docker Isolation, orchestration
Need PostgreSQL/Redis/etc Docker All services in one compose file
Kubernetes deployment Docker Container images required

Troubleshooting

Common Issues and Solutions

502 Bad Gateway

# Check PM2 logs
pm2 logs {{tutorial.appname}} --lines 100
# Verify app is running
pm2 list

# Test app directly
curl -i http://127.0.0.1:{{tutorial.appport}}/

# Check NGINX configuration
sudo nginx -t

# Restart application
pm2 restart {{tutorial.appname}}

Missing Node Modules

# Ensure lockfile exists
ls -lh pnpm-lock.yaml

# Reinstall dependencies
rm -rf node_modules
nvm use 22
pnpm install --frozen-lockfile

# Restart app
pm2 restart {{tutorial.appname}}

Database Connection Errors

# Verify DATABASE_URL in .env
cat .env | grep DATABASE_URL

# Check database file exists
ls -lh prisma/db.sqlite

# Regenerate Prisma client
pnpm dlx prisma generate

# Run preflight check
pnpm run preflight:db

Prisma Client Not Generated

# Generate Prisma client
pnpm dlx prisma generate

# If using migrations
pnpm dlx prisma migrate deploy

# Restart application
pm2 restart {{tutorial.appname}}

Wrong Node Version

# Check current Node version
node --version

# Switch to Node 22
nvm use 22

# Set as default
nvm alias default 22

# Restart PM2 with correct Node
pm2 delete {{tutorial.appname}}
nvm use 22
pm2 start ecosystem.config.cjs --env production
pm2 save

SSL Certificate Issues

# Check certificate status
sudo certbot certificates

# Renew if needed
sudo certbot renew

# Test NGINX config
sudo nginx -t

# Reload NGINX
sudo systemctl reload nginx

High Memory Usage

# Check PM2 memory
pm2 list

# Monitor in real-time
pm2 monit

# Restart app to free memory
pm2 restart {{tutorial.appname}}

# Check system memory
free -h
htop

Port Already in Use

# Find process using port {{tutorial.appport}}
sudo lsof -i :{{tutorial.appport}}

# Kill process (if needed)
sudo kill -9 PID

# Or change port in ecosystem.config.cjs
# Update NGINX proxy_pass accordingly

Additional Resources