Nuxt is a Vue-based meta-framework that pairs a modern UI stack with a compact server engine called Nitro. In production, Nitro can run as a Node.js server, pre-render to static files, or target serverless and edge platforms, which gives you several deployment shapes on DigitalOcean. In this guide you will set up a production-ready Nuxt 4 app on either DigitalOcean App Platform (managed) or a Droplet (DIY), plus an optional Docker path for portability.
You will need a DigitalOcean account and a Git repository for your app; everything else is covered below. The goal is a repeatable deployment you can maintain, scale, and troubleshoot with confidence.
What you’ll build and the baseline:
You will deploy a server-rendered (SSR) Nuxt 4 application that serves dynamic routes and APIs from a Nitro Node server. We’ll target Node.js 22 LTS on Ubuntu 24.04 where applicable, because Active LTS is stable for production and widely supported. If you prefer containerized workloads, the Docker section shows a minimal, multi-stage image that runs the Nitro server directly.
With those decisions made, choose App Platform for the fastest path, or Droplets if you want full control over networking, Nginx, and process supervision. We will close with CI/CD and common fixes so you can iterate safely.
Option A — DigitalOcean App Platform (managed)
App Platform builds your app from Git and runs it as a managed “Web Service,” which suits Nuxt’s Node server preset. Commit your app to GitHub, GitLab, or Bitbucket, then in the DigitalOcean control panel create an App and select your repository and branch. Choose “Node.js” as the component type; App Platform will inject a PORT
environment variable that Nitro auto-detects, so you only need to specify build and run commands. This route gives you zero-maintenance HTTPS, rollbacks, and horizontal autoscaling without managing Nginx.
Suggested commands (App Spec → Build & Run):
# Build command
npm ci && npm run build
# Run command (Nuxt/Nitro output)
node .output/server/index.mjs
Define your environment variables in the App settings (e.g., NUXT_PUBLIC_*
for client-side, NUXT_*
for server-side). If you use background workers (queues, schedulers), add them as separate components that share the same repo and environment. For scaling, start with 1–2 small containers and enable autoscaling based on CPU to handle traffic bursts, then inspect response times and cache headers before increasing size.
Option B — Droplet on Ubuntu 24.04 (DIY with Nginx + PM2)
A Droplet gives you root control, useful when you need custom networking, long-running jobs, or specific OS packages. The pattern is: install Node.js 22 LTS, clone and build your app, run the Nitro server under PM2, and place Nginx in front for TLS and compression. This provides predictable restarts, logs, and a well-understood reverse proxy. The steps below assume a fresh Ubuntu 24.04 Droplet with a sudo user.
Install Node.js 22 LTS (NodeSource) and basics:
# As a sudo user on Ubuntu 24.04
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs build-essential
node -v && npm -v
Clone, build, and run with PM2:
# Adjust paths and repo
sudo apt-get update && sudo apt-get install -y git
mkdir -p ~/apps && cd ~/apps
git clone https://github.com/your-org/your-nuxt-app.git app && cd app
# Environment
cp .env.example .env # then edit secrets: NUXT_*, NUXT_PUBLIC_*
# Install and build
npm ci
npm run build
# Process manager
sudo npm i -g pm2
pm2 start .output/server/index.mjs --name nuxt --time
pm2 save
pm2 startup systemd -u "$USER" --hp "$HOME"
Install Nginx and set up a reverse proxy:
sudo apt-get install -y nginx
sudo tee /etc/nginx/sites-available/nuxt.conf >/dev/null <<'NGINX'
server {
listen 80;
server_name your-domain.com;
# Big uploads or payloads if needed
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
NGINX
sudo ln -s /etc/nginx/sites-available/nuxt.conf /etc/nginx/sites-enabled/nuxt.conf
sudo nginx -t && sudo systemctl reload nginx
Attach a domain and enable HTTPS with Let’s Encrypt:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com --redirect -m you@dropletdrift.com --agree-tos -n
This DIY stack mirrors DigitalOcean’s general Node.js production guidance while aligning with Nitro’s Node server runtime. Nginx terminates TLS and proxies to Nitro on 127.0.0.1:3000
, PM2 restarts the app if it crashes or the host reboots, and npm run build
produces the optimized .output
directory that Nuxt serves in production. If you change environment variables, restart with pm2 restart nuxt
to reload server config and routes.
Option C — Docker (portable, same image anywhere)
Containerizing Nuxt keeps your build artifacts and runtime consistent across laptops, Droplets, and App Platform. Use a multi-stage build that compiles to .output/
and runs the Node server with only the files it needs. This reduces image size and avoids shipping your dev dependencies to production. You can run it with docker run
, Docker Compose, or push to a registry that App Platform pulls from.
Dockerfile (multi-stage, Node 22 LTS):
# Build stage
FROM node:22 as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage
FROM node:22
WORKDIR /app
ENV NODE_ENV=production
# Nitro will listen on NITRO_PORT or PORT; expose for convenience
ENV NITRO_PORT=3000
EXPOSE 3000
# Copy only the production output
COPY --from=builder /app/.output /app/.output
CMD ["node", ".output/server/index.mjs"]
Run locally or on a Droplet:
docker build -t your-nuxt:prod .
docker run -p 3000:3000 --env-file .env your-nuxt:prod
With Docker, you can place Nginx in a sibling container for TLS and compression, or terminate TLS at a load balancer. On App Platform, choose “Deploy from Dockerfile” and set any secrets as environment variables in the service configuration. This keeps your operational model identical across environments and simplifies rollbacks by tagging images per release.
Production checklists that save you later
First, pin a supported Node runtime in your platform settings or base image to avoid surprise upgrades; Node 22 is the current Active LTS and a safe default in 2025. Next, ensure NUXT_PUBLIC_*
variables contain only values safe for the browser, and keep secrets in server-only NUXT_*
variables. Then confirm that your health checks hit a fast path (e.g., /
or /healthz
) and that responses include cache headers appropriate for your pages and assets. Finally, enable structured logs with PM2 timestamps or platform logs so you can correlate latency with deployments.
Continuous delivery
On App Platform, enable “Autodeploy on commit” for your production branch, and set a manual approval gate if you need change control. On Droplets, add a GitHub Actions workflow that builds on every tag, uploads the build artifacts or Docker image, and restarts the PM2 process remotely. For Docker users, push the image to a registry such as GHCR or DigitalOcean Container Registry and pull by immutable digest during deploy. This keeps deploys reproducible and shortens rollback times to minutes.
Example GitHub Actions (Droplet via SSH):
name: deploy
on:
push:
tags:
- 'v*.*.*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm run build
- name: Upload .output
run: tar -C .output -czf output.tgz .
- name: Ship to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DROPLET_HOST }}
username: ${{ secrets.DROPLET_USER }}
key: ${{ secrets.DROPLET_SSH_KEY }}
source: "output.tgz"
target: "~/apps/app/.output"
- name: Restart PM2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DROPLET_HOST }}
username: ${{ secrets.DROPLET_USER }}
key: ${{ secrets.DROPLET_SSH_KEY }}
script: |
cd ~/apps/app
tar -xzf .output/output.tgz -C .output
pm2 restart nuxt
Troubleshooting and tuning
Start by checking where the app is listening. App Platform sets PORT
automatically; Droplets should bind Nitro to 127.0.0.1:3000
and let Nginx handle public ports. If pages render locally but 502s appear behind Nginx, verify the proxy target matches your PM2 port and that the process is alive (pm2 logs nuxt
). For Node upgrades, keep within LTS lines and test on staging; Nuxt 4 and Nitro support Node 18+ but you gain performance and security improvements on Node 22 in 2025. If cold starts feel slow, pre-warm critical routes, enable HTTP/2 at Nginx, and review dynamic imports and image optimization.