# How to deploy a Nuxt app to a VPS

Source: https://tenkai.cloud/help/deploy-nuxt

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

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](https://tenkai.cloud/help/projects-and-clusters).

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

```ts
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`.

```html
<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`.

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

```dockerfile
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.

```text
.git
node_modules
.nuxt
.output
.data
.env
.env.*
```

## Test the image locally

```sh
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](https://tenkai.cloud/help/publish-image-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

1. **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](https://tenkai.cloud/help/pull-private-ghcr-image) explains.
2. **Add the variables.** In the **Variables** tab, add your `NUXT_` variables, and choose **Sensitive** for a secret such as `NUXT_API_SECRET`. To use a PostgreSQL [managed service](https://tenkai.cloud/help/managed-services), bind it and rename its variable to the one your config reads, such as `NUXT_DATABASE_URL`.
3. **Add the health check.** In the **Configuration** tab, add a [health check](https://tenkai.cloud/help/blue-green-deployments#the-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](https://tenkai.cloud/help/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](https://tenkai.cloud/help/custom-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](https://tenkai.cloud/help/deploy-single-page-application) shows.
