Back to Dev Notes
10 min read

Deploying Nuxt 3 to a VPS with Docker & Nginx

Deploying Nuxt 3 to a VPS with Docker & Nginx

Shared hosting won't cut it for a Nuxt 3 SSR app. A VPS gives you full control — but with great power comes a somewhat longer deployment checklist. This guide walks you through every step, from writing the Dockerfile to getting that green padlock in the browser.

Prerequisites

  • A VPS running Ubuntu 22.04 (DigitalOcean, Hetzner, or similar)
  • Docker and Docker Compose installed on the server
  • A domain name pointed at your server's IP

1. Dockerise Your App

Add a Dockerfile in your project root:

# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: production
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.output ./  .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

2. docker-compose.yml

version: '3.9'
services:
  nuxt:
    build: .
    container_name: nuxt_app
    restart: unless-stopped
    environment:
      - NUXT_PUBLIC_SITE_URL=https://yourdomain.com
    networks:
      - web

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/www:/var/www/certbot
      - ./certbot/conf:/etc/letsencrypt
    depends_on:
      - nuxt
    networks:
      - web

networks:
  web:
    driver: bridge

3. Nginx Configuration

Create nginx/conf.d/default.conf:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    gzip on;
    gzip_types text/plain application/json text/css application/javascript;

    location / {
        proxy_pass         http://nuxt:3000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

4. Get an SSL Certificate

# Start Nginx on port 80 first
docker compose up -d nginx

# Issue a certificate via Certbot
docker run --rm \
  -v ./certbot/conf:/etc/letsencrypt \
  -v ./certbot/www:/var/www/certbot \
  certbot/certbot certonly --webroot \
  --webroot-path=/var/www/certbot \
  -d yourdomain.com -d www.yourdomain.com \
  --email your@email.com --agree-tos --no-eff-email

5. Deploy Everything

# On your local machine — push the image or just SSH in and pull
git pull origin main
docker compose up -d --build

Your app is now live at https://yourdomain.com with automatic HTTP→HTTPS redirects and gzip compression enabled.

Auto-Renew SSL

Add this to your server's crontab (crontab -e):

0 3 * * * docker run --rm \
  -v /home/deploy/certbot/conf:/etc/letsencrypt \
  -v /home/deploy/certbot/www:/var/www/certbot \
  certbot/certbot renew --quiet && \
  docker exec nginx nginx -s reload

Wrapping Up

You now have a fully containerised, HTTPS-enabled Nuxt 3 application running behind an Nginx reverse proxy. This setup is portable, reproducible, and easy to scale — just add more services to your docker-compose.yml as your project grows.

Tags
Back to Dev Notes

Are You Ready to kickstart your project with a touch of magic?

Reach out and let's make it happen ✨. I'm also available for full-time or part-time opportunities to push the boundaries of design and deliver exceptional work.

LaravelNuxt.jsVue.jsTypeScriptNext.jsPrismaTailwindBootstrapSuSupabase
PostgreSQLERPNextExpress.jsLinuxMySQLMongoDBRedisDockerGitHub