Running Nuxt 3 in production with Server-Side Rendering (SSR) needs more than just tossing static files into an S3 bucket. You need a persistent Node process running the Nitro engine, a reverse proxy handling SSL and static asset caching, clean DNS routing, and an edge CDN layer so your server doesn’t fall over during traffic spikes.
Here is my end-to-end setup for deploying a full-stack Nuxt 3 app on an Ubuntu Hostinger VPS—from Node.js 20 and PM2 cluster mode to Nginx tuning and Namecheap DNS configuration.

1. Server Baseline and Node.js Environment Setup
Spin up a fresh Ubuntu 22.04 or 24.04 LTS instance in your Hostinger dashboard. SSH in as root to get baseline packages updated before touching your application code.
First, update the package index and grab basic utilities like curl, git, and build essentials:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw fail2banNuxt 3 and its underlying Nitro engine run best on an Active LTS version of Node. We’ll pull Node.js 20 LTS directly from the official NodeSource repository.
Add the repository and install Node along with npm:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -vRun node -v to verify you’re on v20.x.x. To avoid running your app as root (a massive security risk in production), create a dedicated deploy user with sudo access:
adduser deploy
usermod -aG sudo deploy
su - deploy2. Clone, Build, and Configure the Nuxt 3 App
Now switch over to your web directory and grab your application code. If you’ve already read our guide on how to deploy Next.js on Hostinger VPS, the project layout will feel familiar, though Nuxt compiles its output differently through Nitro.
Create a directory under /var/www/ and hand ownership over to your deploy user:
sudo mkdir -p /var/www/my-nuxt-app
sudo chown -R deploy:deploy /var/www/my-nuxt-app
cd /var/www/my-nuxt-appClone your repository or upload your files here. Once they’re in place, install your dependencies:
npm installCreate your production .env file to hold database credentials, API keys, and runtime environment variables:
NITRO_PORT=3000
NITRO_HOST=127.0.0.1
NODE_ENV=production
API_BASE_URL=https://api.yourdomain.comNuxt 3 uses Nitro as its server engine. Running the build command generates a completely self-contained production bundle inside the .output folder, so you don’t even need the bulky node_modules directory at runtime.
Trigger the production build on the server:
npm run buildOnce the build finishes, you’ll see .output/server/index.mjs. That single entry file runs your entire SSR application.
3. Process Management with PM2
If your VPS reboots or your app hits an unhandled error, you can’t have your site sitting dead. PM2 manages the process lifecycle, handles automatic restarts, and captures logs cleanly.
Install PM2 globally:
sudo npm install -g pm2Instead of passing messy inline CLI flags every time, create an ecosystem configuration file at /var/www/my-nuxt-app/ecosystem.config.cjs:
module.exports = { apps: [ { name: 'nuxt-production', port: '3000', host: '127.0.0.1', exec_mode: 'cluster', instances: 'max', script: './.output/server/index.mjs', env: { NODE_ENV: 'production', PORT: 3000, NITRO_PORT: 3000, NITRO_HOST: '127.0.0.1' } } ]
};Pay attention to exec_mode: 'cluster'. Because Nitro’s server bundle is stateless, PM2 can spawn worker instances across all available CPU cores on your VPS, giving you instant concurrency gains without touching cluster code.
Start the app and configure PM2 to boot on system startup:
pm2 start ecosystem.config.cjs
pm2 save
pm2 startupCopy and run the sudo env PATH=... command that PM2 spits out in your terminal. Then verify the server is responding locally:
curl -I http://127.0.0.1:3000You should see an HTTP/1.1 200 OK response directly from Nitro.
4. Point Namecheap DNS to Hostinger VPS
Before issuing SSL certificates, your domain needs to point directly to your Hostinger VPS public IP. Head over to your Namecheap Dashboard.
Here is the setup in Namecheap Advanced DNS:
- Go to your Domain List and click Manage next to your domain.
- Open the Advanced DNS tab.
- Make sure Nameservers is set to Namecheap BasicDNS.
- Under Host Records, add or update these two records:
- A Record: Host
@| ValueYOUR_VPS_PUBLIC_IP| TTLAutomatic(or1 minduring migration) - CNAME Record: Host
www| Valueyourdomain.com.| TTLAutomatic
If you aren’t sure how apex routing behaves, check our guide on configuring root domains in Namecheap DNS to avoid common apex record conflicts.
Give it a few minutes, then verify DNS propagation from your local machine:
dig +short yourdomain.com AOnce it resolves to your VPS IP, you’re ready to wire up Nginx.
5. Nginx Reverse Proxy and Static Asset Tuning
Nginx acts as your frontend traffic cop. It listens on ports 80 and 443, terminates SSL, serves static assets (JS chunks, CSS, images) straight off the disk without touching Node, and proxies dynamic SSR requests to PM2 on port 3000.
Install Nginx:
sudo apt install -y nginxCreate a server block configuration at /etc/nginx/sites-available/yourdomain.com:
server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com; # Static assets built by Nuxt/Nitro location /_nuxt/ { alias /var/www/my-nuxt-app/.output/public/_nuxt/; expires 365d; access_log off; add_header Cache-Control "public, max-age=31536000, immutable"; } # Public folder files (robots.txt, favicon.ico, images) location / { try_files $uri $uri/ @nitro; root /var/www/my-nuxt-app/.output/public; } # Dynamic SSR proxy location @nitro { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; 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; proxy_cache_bypass $http_upgrade; proxy_read_timeout 60s; proxy_connect_timeout 60s; }
}The biggest performance win here is the /_nuxt/ block. Nginx serves those hashed bundles directly from disk with immutable 1-year cache headers, freeing up Node to focus entirely on SSR execution.
Enable the site and test your configuration syntax:
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx6. Secure with Free Let’s Encrypt SSL
Now lock down your domain with a free TLS certificate from Let’s Encrypt using Certbot.
Install Certbot and run the Nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comCertbot will ask for an email address and automatically update your Nginx config with HTTPS redirection blocks. Test that the renewal timer works properly:
sudo certbot renew --dry-runIf you manage subdomains or need automated DNS challenges, see our tutorial on Let’s Encrypt wildcard SSL with Certbot DNS-01.
7. Layer an Edge CDN for Asset Offloading
While Nginx is fast, putting an edge CDN in front of your server ensures static bundles and cached HTML load with single-digit millisecond latency worldwide while protecting your origin server from traffic spikes.
Here are the key CDN settings to apply:
- Origin Server: Point your CDN origin to your VPS IP or origin hostname.
- Origin Protocol: Set to
HTTPS Only (Port 443)to prevent redirect loops. If you get stuck in a loop, read our fix for SSL ERR_TOO_MANY_REDIRECTS on Nginx behind a CDN. - Cache Rule for
/_nuxt/*: Set Edge Cache TTL to 1 Year (31536000s). These files include unique content hashes on every build and are completely immutable. - Dynamic SSR Routes: Set default Edge Cache TTL to Bypass or a short micro-cache (10–30s) depending on how dynamic your content is.
For heavier traffic, you can also configure CDN origin shield and tiered caching to prevent cache stampedes after fresh deployments.
8. Zero-Downtime Deployment Script
You don’t want to SSH in and run five manual build commands every time you push a hotfix. Create a deploy script at /var/www/my-nuxt-app/deploy.sh:
#!/bin/bash
set -e echo "Fetching latest code from Git..."
git pull origin main echo "Installing dependencies..."
npm ci --prefer-offline echo "Building Nuxt 3 project..."
npm run build echo "Reloading PM2 cluster with zero downtime..."
pm2 reload ecosystem.config.cjs --update-env echo "Deployment complete!"Make it executable:
chmod +x /var/www/my-nuxt-app/deploy.shThe magic here is pm2 reload. Unlike a standard restart, it performs a rolling reload across your worker cluster—spawning new workers with the updated build before killing off the old ones so your users never see a 502 Bad Gateway.
Gotchas and Production Troubleshooting
These are two exact bugs I ran into while deploying Nuxt 3 on entry-level Hostinger VPS plans:
1. Out of Memory (OOM) During Build
On 1 GB or 2 GB RAM plans, npm run build will frequently crash with a JavaScript heap out of memory error while Vite and Rollup compile chunks.
The fastest fix is adding a 2 GB swap file:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab2. Missing Client IP in SSR Logs
Behind both a CDN and Nginx, useRequestEvent()?.node.req.socket.remoteAddress will just log 127.0.0.1. Make sure Nginx passes X-Forwarded-For and tell Nitro to trust upstream proxies inside your nuxt.config.ts:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ nitro: { routeRules: { '/api/**': { cors: true } } }, runtimeConfig: { public: { apiBase: process.env.API_BASE_URL || '/api' } }
})For deeper runtime settings, check the official Nuxt Deployment documentation and the Nitro Engine guides.
Frequently Asked Questions
Can I run Nuxt 3 on Hostinger Shared Web Hosting instead of a VPS?
Only if you’re using npx nuxt generate for Static Site Generation (SSG). Shared hosting cannot keep a long-running Node.js process alive. If you need dynamic SSR, server routes, or middleware, you must use a VPS.
Why should I use PM2 instead of systemd?
systemd is great, but PM2 gives you out-of-the-box cluster mode, simple log tailing via pm2 logs, live resource monitoring with pm2 monit, and zero-downtime rolling reloads without building complex service templates.
What is the difference between pm2 restart and pm2 reload?
pm2 restart kills all running worker processes at once and reboots them, resulting in a couple of seconds of downtime. pm2 reload rotates through the cluster sequentially, keeping at least one worker active to handle requests during code refreshes.
Do I need to run npm install on the server for every deploy?
Use npm ci instead—it strictly respects your package-lock.json and installs faster. If you want even faster deploys, build your .output artifacts inside a GitHub Actions CI pipeline and rsync only the build output to your VPS.
Next Steps
If you’re testing other full-stack frameworks on your infrastructure, take a look at our guide on how to deploy SvelteKit on Hostinger VPS with Nginx and Namecheap DNS to compare SSR resource footprints under load.

