Nuxt is the full-stack meta-framework for Vue: server-side rendering, file-based routing, and server API routes (Nitro) in one project. Because it builds on Nitro, deploying Nuxt to a self-hosted Node server is clean and portable. This guide deploys a Nuxt app with a managed PostgreSQL database.
How Nuxt builds for production
nuxt build produces a Nitro server in .output/. The default node-server preset emits a standalone server you run with Node:
npm run build # nuxt build
node .output/server/index.mjsNitro bundles your server dependencies into .output/server, so the runtime image doesn't need node_modules — a meaningful size reduction.
Binding to the injected port
The Nitro server reads HOST and PORT. In a container set HOST=0.0.0.0 and let PORT come from the platform:
HOST=0.0.0.0 PORT=3000 node .output/server/index.mjsThis is built into Nitro — no extra config to listen on the right interface and port.
Server API routes with a database
Nuxt's server routes live in server/api/. Link a managed PostgreSQL so DATABASE_URL is injected, then query it from a server route:
// server/api/users.get.ts
import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL!);
export default defineEventHandler(async () => {
const users = await sql`SELECT id, name FROM users LIMIT 20`;
return { users };
});Initialize the client at module scope so the connection pool persists across requests. Server routes run only on the server, so DATABASE_URL never leaks to the browser.
Runtime config and secrets
Nuxt's runtimeConfig is the right place for server secrets. Anything under the top level of runtimeConfig is server-only; only runtimeConfig.public is exposed to the client:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
databaseUrl: '', // server-only, from NUXT_DATABASE_URL
public: {
apiBase: '/api', // safe to expose
},
},
});Nitro maps env vars to runtimeConfig using the NUXT_ prefix, so NUXT_DATABASE_URL populates runtimeConfig.databaseUrl. If you prefer, just read process.env.DATABASE_URL directly in server routes — both work.
The Dockerfile
# ---- Build ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Runtime ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]Because Nitro bundles dependencies into .output, we copy only that directory. No node_modules in the runtime stage — the image stays small and the cold start is fast.
Migrations
If you manage schema with a tool like Drizzle or Prisma, run migrations as a release step, not on server boot:
npx drizzle-kit migrate # or: npx prisma migrate deployWith multiple replicas, running migrations on boot causes races. On PandaStack, wire this as a once-per-release job before traffic shifts.
Environment variables
| Variable | Purpose |
|---|---|
NODE_ENV | production |
HOST | 0.0.0.0 |
PORT | injected listen port |
DATABASE_URL / NUXT_DATABASE_URL | injected by managed DB link |
Health checks
Add a server route for readiness:
// server/api/health.get.ts
export default defineEventHandler(() => 'ok');Point the platform's probe at /api/health. Keep it free of DB calls for the high-frequency liveness check.
Static vs SSR
If your Nuxt app doesn't need per-request rendering, you can prerender it with nuxt generate and deploy as a static site — cheaper and CDN-friendly. But database-backed dynamic pages and API routes require the Node server deployment above.
Deploying
Push, connect the repo, link a managed PostgreSQL so DATABASE_URL is injected, set HOST and NODE_ENV, add the migration release step, and deploy. The platform builds the Dockerfile (or auto-detects Nuxt) and runs the Nitro server. Live build and app logs make build failures and connection issues obvious.
git push origin mainConclusion
Nuxt deploys as a self-contained Nitro Node server: build to .output, run index.mjs binding to 0.0.0.0 and the injected port, query DATABASE_URL from server routes, and run migrations as a release step. Nitro's bundled output keeps the runtime image tiny.
Try a Nuxt app plus a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database auto-wires via DATABASE_URL.
References
- [Nuxt: Deployment](https://nuxt.com/docs/getting-started/deployment)
- [Nuxt: Server Routes (Nitro)](https://nuxt.com/docs/guide/directory-structure/server)
- [Nuxt: Runtime Config](https://nuxt.com/docs/guide/going-further/runtime-config)
- [Nitro: Node Server Preset](https://nitro.build/deploy/runtimes/node)
- [postgres.js Client](https://github.com/porsager/postgres)