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.