Back to Blog
Tutorial11 min read2026-07-02

How to Deploy a Nuxt App with PostgreSQL

Nuxt is the full-stack Vue meta-framework with SSR and server API routes powered by Nitro. Learn how to deploy a Nuxt app to production with a managed PostgreSQL database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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.mjs

Nitro 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.mjs

This 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 deploy

With multiple replicas, running migrations on boot causes races. On PandaStack, wire this as a once-per-release job before traffic shifts.

Environment variables

VariablePurpose
NODE_ENVproduction
HOST0.0.0.0
PORTinjected listen port
DATABASE_URL / NUXT_DATABASE_URLinjected 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 main

Conclusion

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)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also