How to deploy a Nuxt app to a VPS
Build a Nuxt 4 app into its own Node.js server, override its runtime config with NUXT_ variables when it starts and run it in Docker.
Updated
This guide deploys a Nuxt 4 app with server-side rendering to your own server. nuxt build turns it into a Node.js server that needs no node_modules, so the image stays small, and the same image runs in every environment because its settings come from NUXT_ variables when it starts. The last sections deploy it with Tenkai.
We tested it on 2 November 2026 with a new app from npm create nuxt@latest and its minimal template, on Nuxt 4.6.4 and node:24-trixie-slim, so adapt the paths and packages to your own app.
Prerequisites
You need a Nuxt 4 app in a Git repository with a CI such as GitHub Actions, and Node.js 24 with npm. Docker is useful to test the image on your machine, and the Tenkai part needs a project on a ready cluster.
Declare the runtime config
Nuxt reads its settings from runtimeConfig in nuxt.config.ts, and a variable overrides a key there when the server starts, as long as the key is declared with a default.
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
runtimeConfig: {
apiSecret: '',
public: {
siteName: 'Hello',
},
},
});
The variable takes the key's path in capitals, so NUXT_API_SECRET sets apiSecret, which only the server sees, and NUXT_PUBLIC_SITE_NAME sets public.siteName, which the browser sees too. Read them with useRuntimeConfig().
Show one on a page to check it later, for example in app/app.vue.
<script setup lang="ts">
const config = useRuntimeConfig();
</script>
<template>
<h1>{{ config.public.siteName }}</h1>
</template>
Two habits keep a value at what the build saw. A key you did not declare in runtimeConfig ignores its variable, and so does a variable without the NUXT_ prefix, such as SITE_NAME. A process.env read in nuxt.config.ts itself runs during nuxt build and never again.
Add a health check
Create server/routes/health.ts.
export default defineEventHandler(() => ({ status: 'ok' }));
/health answers from the server itself. Add a query to your database here when you have one, so it fails when the app cannot work.
Add a Dockerfile
Create Dockerfile at the repository root.
FROM node:24-trixie-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-trixie-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=8080
COPY --from=build /app/.output ./.output
USER node
EXPOSE 8080
CMD ["node", ".output/server/index.mjs"]
.output holds the server, its dependencies and the public files, so the runtime stage copies that folder alone. The server listens on every address at PORT, and it stops at once on SIGTERM because the CMD starts node directly.
Exclude local files from the image
Create .dockerignore beside the Dockerfile.
.git
node_modules
.nuxt
.output
.data
.env
.env.*
Test the image locally
docker build -t hello-nuxt .
docker run --rm --name hello-nuxt -p 8080:8080 -e NUXT_PUBLIC_SITE_NAME="From Docker" hello-nuxt
Visit http://localhost:8080, where the page now reads From Docker, then http://localhost:8080/health.
Publish the image from your CI
Build the image in your CI on each push and publish it to a container registry, so your servers spend nothing on builds and your code can stay private. Build an image in GitHub Actions and publish it to GHCR gives a complete workflow for this Dockerfile, built for your cluster's CPU architecture, which publishes an image such as ghcr.io/acme/hello-nuxt:main on each push to main.
Deploy it with Tenkai
- Create the application. Choose Add resource, then Application, Web application and Container image. Enter the image reference with its moving tag, such as
ghcr.io/acme/hello-nuxt:main, and for a private package choose a GHCR credential that Can pull it, as pull a private GHCR image explains. - Add the variables. In the Variables tab, add your
NUXT_variables, and choose Sensitive for a secret such asNUXT_API_SECRET. To use a PostgreSQL managed service, bind it and rename its variable to the one your config reads, such asNUXT_DATABASE_URL. - Add the health check. In the Configuration tab, add a health check on
/health.
Each push then publishes a new image, and a last step of your workflow can deploy it by calling the application's deploy hook.
Deploy and verify
Choose Deploy and wait for an active release, then open its default route over HTTPS. A setting that keeps its default value has no matching key in runtimeConfig or a variable without the NUXT_ prefix. A change of variable takes effect with Save and apply, without a new build. Once it works, connect your own domain.
A site without a server
When nuxt generate renders every page to files, no Node.js server is needed. Deploy the .output/public folder as a static site from your CI instead, as deploy a single-page application shows.