Last updated on 7th March 2026

Deploy Nuxt 3 Apps with DeployHQ

Prerequisites:

  • Node.js 18 or later (Node.js 20 LTS recommended)
  • A server with SSH access (for SSR) or SFTP access (for static)
  • A DeployHQ account

What is Nuxt?

Nuxt is the official meta-framework for Vue.js. It provides server-side rendering (SSR), static site generation (SSG), and hybrid rendering out of the box, built on top of the Nitro server engine and Vite.

Nuxt 3 handles routing, data fetching, state management, and build configuration automatically — letting you focus on building features rather than configuring tooling. It supports auto-imported components, composables, and a powerful module ecosystem.

Step 1: Create a Nuxt Project

npx nuxi init my-app
cd my-app
npm install

Start the development server: bash npm run dev

The app runs at http://localhost:3000 with hot module replacement.

Step 2: Project Structure

my-app/
├── app.vue                 # Root component
├── pages/                  # File-based routing
│   ├── index.vue           # → /
│   ├── about.vue           # → /about
│   └── blog/
│       ├── index.vue       # → /blog
│       └── [slug].vue      # → /blog/:slug
├── components/             # Auto-imported components
│   ├── AppHeader.vue
│   └── AppFooter.vue
├── composables/            # Auto-imported composables
│   └── useAuth.ts
├── layouts/                # Layout components
│   └── default.vue
├── middleware/              # Route middleware
├── plugins/                # Vue plugins
├── server/                 # Server-side code (Nitro)
│   ├── api/                # API routes
│   │   └── posts.get.ts    # → GET /api/posts
│   └── middleware/
├── assets/                 # Processed by Vite (SCSS, images)
├── public/                 # Static files (favicon, robots.txt)
├── nuxt.config.ts          # Nuxt configuration
└── package.json

File-Based Routing

Pages in the pages/ directory automatically become routes:

File Route
pages/index.vue /
pages/about.vue /about
pages/blog/index.vue /blog
pages/blog/[slug].vue /blog/:slug
pages/users/[id]/posts.vue /users/:id/posts

Auto-Imports

Components in components/, composables in composables/, and utilities in utils/ are auto-imported. No import statements needed:

<script setup>
// useAuth is auto-imported from composables/useAuth.ts
const { user, logout } = useAuth()

// useFetch is a built-in Nuxt composable
const { data: posts } = await useFetch('/api/posts')
</script>

<template>
  <!-- AppHeader is auto-imported from components/ -->
  <AppHeader />
  <main>
    <div v-for="post in posts" :key="post.id">
      {{ post.title }}
    </div>
  </main>
</template>

Step 3: Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: [
    '@nuxt/content',     // Markdown-based CMS
    '@nuxt/image',       // Image optimisation
    '@pinia/nuxt',       // State management
  ],

  css: ['~/assets/css/main.css'],

  runtimeConfig: {
    // Server-only (not exposed to client)
    databaseUrl: process.env.DATABASE_URL,
    apiSecret: process.env.API_SECRET,

    // Public (exposed to client)
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
      siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://example.com',
    }
  },

  routeRules: {
    // Hybrid rendering: per-route configuration
    '/': { prerender: true },
    '/blog/**': { swr: 3600 },           // Stale-while-revalidate, 1 hour
    '/admin/**': { ssr: false },          // Client-side only
    '/api/**': { cors: true },
  },

  nitro: {
    preset: 'node-server'  // For VPS deployment
  }
})

Step 4: Core Features

Data Fetching

Nuxt provides useFetch and useAsyncData for SSR-safe data fetching:

<script setup>
const { data: post, error } = await useFetch(`/api/posts/${route.params.slug}`)

if (error.value) {
  throw createError({ statusCode: 404, message: 'Post not found' })
}
</script>

Server Routes (Nitro)

Create API endpoints in server/api/:

// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const posts = await db.posts.findMany({
    take: Number(query.limit) || 10,
    orderBy: { createdAt: 'desc' }
  })
  return posts
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const post = await db.posts.create({ data: body })
  return post
})

Middleware

Route middleware runs before navigation:

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { user } = useAuth()
  if (!user.value) {
    return navigateTo('/login')
  }
})

Apply to pages: vue <script setup> definePageMeta({ middleware: 'auth' }) </script>

SEO

<script setup>
useHead({
  title: 'My Page Title',
  meta: [
    { name: 'description', content: 'Page description for SERP' }
  ]
})

useSeoMeta({
  ogTitle: 'My Page Title',
  ogDescription: 'Page description',
  ogImage: '/og-image.png'
})
</script>

Step 5: Rendering Modes

SSR (Server-Side Rendering) — Default

Every request renders on the server. Best for dynamic, personalised content.

npm run build

Output: .output/ directory with Nitro server.

SSG (Static Site Generation)

Pre-render all pages at build time:

npx nuxt generate

Output: .output/public/ with static HTML files.

Hybrid Rendering

Mix rendering strategies per route in nuxt.config.ts:

routeRules: {
  '/': { prerender: true },              // Static at build time
  '/blog/**': { swr: 3600 },            // SSR with caching
  '/dashboard/**': { ssr: false },       // Client-side only
  '/api/**': { cache: { maxAge: 60 } },  // Cached API
}

Step 6: Deploy with DeployHQ

SSR Deployment (Node.js Server)

Build Command: bash cd %path% && npm ci && npm run build

Output is .output/ containing the Nitro server. The entry point is .output/server/index.mjs.

Do NOT set a deployment subdirectory — deploy the entire .output/ directory.

PM2 Ecosystem File:

Create ecosystem.config.cjs in your project root:

module.exports = {
  apps: [{
    name: 'nuxt-app',
    script: './.output/server/index.mjs',
    instances: 'max',
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
      NITRO_PORT: 3000
    },
    max_memory_restart: '512M'
  }]
}

Post-Deployment SSH Command: bash cd /var/www/nuxt-app && npm ci --omit=dev && pm2 reload ecosystem.config.cjs --env production

First deployment: bash cd /var/www/nuxt-app && npm ci --omit=dev && pm2 start ecosystem.config.cjs --env production && pm2 save && pm2 startup

Nginx Reverse Proxy: ```nginx server { listen 443 ssl http2; server_name example.com;

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

# Static assets from Nuxt build
location /_nuxt/ {
    alias /var/www/nuxt-app/.output/public/_nuxt/;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# All other requests → Nitro server
location / {
    proxy_pass http://localhost:3000;
    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;
}

} ```

Environment Variables:

Set in .env on the server (not committed to Git): bash DATABASE_URL=postgresql://user:pass@localhost:5432/mydb API_SECRET=your-secret NUXT_PUBLIC_API_BASE=https://api.example.com NUXT_PUBLIC_SITE_URL=https://example.com

SSG Deployment (Static Files)

Build Command: bash cd %path% && npm ci && npx nuxt generate

Deployment Subdirectory: .output/public

No PM2 needed — just static files served by Nginx.

Nginx Configuration: ```nginx server { listen 443 ssl http2; server_name example.com;

root /var/www/nuxt-site;
index index.html;

location /_nuxt/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location / {
    try_files $uri $uri/ $uri/index.html /200.html;
}

error_page 404 /404.html;

} ```

Step 7: Troubleshooting

Hydration Mismatch Errors

Symptom: Console warnings about hydration mismatches after deployment.

Fix: Ensure server and client render the same content. Common causes: - Browser extensions injecting HTML - Date/time rendering that differs between server and client timezones - Using Math.random() or Date.now() in components

Wrap client-only content: vue <ClientOnly> <DynamicComponent /> </ClientOnly>

Environment Variables Undefined in Production

Symptom: runtimeConfig values are undefined on the server.

Fix: Nuxt reads environment variables at build time for public config and at runtime for server config. Ensure: - Public variables are prefixed with NUXT_PUBLIC_ - Server variables match the config key path (e.g., NUXT_DATABASE_URL for runtimeConfig.databaseUrl) - The .env file exists on the server

Build Fails with "Cannot find module"

Fix: Run npm ci (not npm install) to ensure exact lockfile versions. Delete node_modules/ and .nuxt/ if stale: bash rm -rf node_modules .nuxt .output && npm ci && npm run build

Port Already in Use

ss -tulnp | grep :3000
pm2 delete nuxt-app
pm2 start ecosystem.config.cjs --env production

Conclusion

Nuxt 3 gives you SSR, SSG, and hybrid rendering with a single framework. DeployHQ handles the build and deployment pipeline — whether you're deploying a static marketing site or a full SSR application with API routes.

For more deployment guides, browse the DeployHQ guides library.

Sign up for DeployHQ — free for one project.

Questions? Contact support@deployhq.com or on Twitter/X.