December 27, 2025 · 8 min

Ultra‑Slim Multi‑Stage Image for SSR: Nuxt 4 + TypeScript + Docker

Build a production‑grade, ultra‑slim Docker image for a Nuxt 4 SSR app using multi‑stage builds, Node 22 LTS, Corepack + pnpm, and Nitro’s node-server preset.

Ultra‑Slim Multi‑Stage Image for SSR: Nuxt 4 + TypeScript + Docker

nuxt-ssr-docker

Create a tiny, production‑ready Docker image for a Nuxt 4 SSR app using modern Dockerfile v1.7 features, Node 22 LTS with Corepack, and Nitro’s node-server preset. We’ll enable fast, reproducible builds with pnpm + cache mounts and run as a non‑root user for better security.

Tags: Nuxt 4, Docker, TypeScript, SSR, BuildKit, pnpm

Time to read: 8 min

What’s new we’ll use

  • Dockerfile 1.7 syntax (2024/2025) with BuildKit cache mounts for blazing‑fast installs
  • Node.js 22 LTS with Corepack enabled by default (so pnpm is first‑class)
  • Nuxt 4 + Nitro node-server preset, which emits a standalone .output perfect for slim containers

Prerequisites

  • Node.js 20+ locally (Node 22 recommended)
  • Docker 24+ with BuildKit enabled (Docker Desktop or Engine)
  • Familiarity with a shell
  • A Nuxt 4 app (we’ll scaffold if you don’t have one)

1) Scaffold a fresh Nuxt 4 app

If you already have a Nuxt 4 app, skip to step 2.

npx nuxi@latest init nuxt4-ssr-docker
cd nuxt4-ssr-docker
pnpm install
pnpm dev

Visit http://localhost:3000 to verify it runs.

Tip: If you’re not using pnpm yet, Node 22 ships Corepack. Enable it once:

corepack enable

2) Lock in pnpm via Corepack for reproducible builds

Add this to your package.json so Docker knows exactly which pnpm to use:

{
  "name": "nuxt4-ssr-docker",
  "private": true,
  "packageManager": "pnpm@9.12.3",
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "start": "node .output/server/index.mjs",
    "preview": "nuxt preview"
  },
  "dependencies": {
    "nuxt": "^4.0.0"
  },
  "devDependencies": {
    "typescript": "^5.6.3"
  }
}

Note: Your generated project may already include most of this; ensure packageManager exists and start points to .output/server/index.mjs.

3) Enable strict TypeScript and SSR friendly output

Edit nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  nitro: {
    preset: 'node-server',
    serveStatic: true
  },
  typescript: {
    typeCheck: true,
    tsConfig: {
      compilerOptions: {
        strict: true
      }
    }
  },
  devtools: { enabled: true }
})

Nuxt 4’s SSR build emits a standalone .output folder (server + public), ideal for slim runtime images.

4) Add a minimal health endpoint for container health checks

Create server/health.get.ts:

// server/health.get.ts
export default eventHandler(() => 'ok')

This exposes GET /health and returns 200 "ok".

5) Optional: a tiny page to confirm SSR

Create pages/index.vue:

<script setup lang="ts">
const now = new Date().toISOString()
</script>

<template>
  <main>
    <h1>Nuxt 4 SSR in a tiny container</h1>
    <p>Rendered at: {{ now }}</p>
    <p>Try the health endpoint at <code>/health</code></p>
  </main>
</template>

<style scoped>
main { font-family: ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji; padding: 2rem; }
h1 { margin-bottom: .5rem; }
code { background: #f6f7f9; padding: .15rem .35rem; border-radius: .25rem; }
</style>

6) Production‑grade multi‑stage Dockerfile (ultra‑slim)

We’ll use:

  • Node 22 bookworm-slim for building (glibc for compatibility)
  • BuildKit cache mounts to speed up installs
  • A slim runtime stage that only contains .output and Node

Create Dockerfile:

# syntax=docker/dockerfile:1.7

ARG NODE_VERSION=22

# Reusable base with Node 22
FROM node:${NODE_VERSION}-bookworm-slim AS base
ENV CI=1
WORKDIR /app

# 1) Dependencies resolution cache (fast, reproducible installs)
FROM base AS deps
# Enable Corepack so pnpm version is managed by package.json "packageManager"
RUN corepack enable
# Only copy files needed for dependency resolution
COPY package.json pnpm-lock.yaml ./
# Warm pnpm store with lockfile (no node_modules yet)
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
    pnpm fetch

# 2) Build stage
FROM base AS build
RUN corepack enable
# Bring in the warmed pnpm store
COPY --from=deps /root/.local/share/pnpm/store /root/.local/share/pnpm/store
# Copy the full source
COPY . .
# Install using offline store + exact lockfile
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile --offline
# Build SSR output (Nitro node-server preset)
RUN pnpm build

# 3) Ultra-slim runtime
FROM node:${NODE_VERSION}-slim AS runner
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
ENV NITRO_PORT=3000
WORKDIR /app/.output

# Copy only the production runtime output
COPY --from=build /app/.output /app/.output

# Drop root
USER node

EXPOSE 3000

# Lightweight healthcheck using Node (no curl needed)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
  CMD ["node","-e","http=require('http');req=http.request({host:'127.0.0.1',port:process.env.PORT||3000,path:'/health'},res=>process.exit(res.statusCode===200?0:1));req.on('error',()=>process.exit(1));req.end();"]

CMD ["node","server/index.mjs"]

Why bookworm-slim in builder and slim in runtime?

  • We build on glibc to avoid musl ABI gotchas for native deps.
  • We run on slim to keep size small without giving up Node’s conveniences (certs, tzdata options, etc.).
  • Distroless is even smaller, but requires more care (no shell, different healthcheck approach). Slim is a great default balance.

7) Build and run

Ensure BuildKit is on:

  • Docker Desktop: it is by default.
  • CLI: export DOCKER_BUILDKIT=1 if needed.

Build:

docker build -t nuxt4-ssr-ultraslim .

Run:

docker run --rm -p 3000:3000 nuxt4-ssr-ultraslim

Open:

Stop with Ctrl+C.

8) Multi‑arch build (arm64 + amd64) with cache

Build and push a multi‑arch image (requires a registry and buildx):

# Create a builder once (if you don’t have one yet)
docker buildx create --use --name nuxtx || docker buildx use nuxtx

# Optional local cache between builds
docker buildx build \
  --platform=linux/amd64,linux/arm64 \
  --tag ghcr.io/<you>/nuxt4-ssr-ultraslim:latest \
  --cache-from=type=registry,ref=ghcr.io/<you>/nuxt4-ssr-ultraslim:buildcache \
  --cache-to=type=registry,mode=max,ref=ghcr.io/<you>/nuxt4-ssr-ultraslim:buildcache \
  --push .

Replace ghcr.io/ with your registry/repo.

9) Tips for even smaller images

  • Copy only what you need: this Dockerfile already copies only .output to runtime.
  • Set NODE_OPTIONS="--max-old-space-size=256" in memory‑constrained environments.
  • If you have no native deps, you can experiment with distroless:
    • Replace the final stage with a distroless Node 22 image
    • Ensure healthchecks are Node‑only (as shown), and expect no shell tools

10) Common pitfalls

  • pnpm not found during build: ensure package.json contains "packageManager": "pnpm@9.x" and Dockerfile runs corepack enable.
  • Alpine vs glibc: if you build on Alpine and run on glibc (or vice versa), native modules may break. This guide uses Debian slim in both builder and runner.
  • Not listening on 0.0.0.0: make sure HOST=0.0.0.0 is set; Nitro respects HOST/PORT/NITRO_PORT.
  • Missing health route: without server/health.get.ts, the HEALTHCHECK will fail.

Conclusion

You’ve built a secure, ultra‑slim, multi‑stage Docker image for a Nuxt 4 SSR app using the latest Dockerfile 1.7 features, Node 22 + Corepack + pnpm, and Nitro’s node-server output. This pattern gives you:

  • Fast, cache‑friendly builds
  • Minimal runtime surface
  • Clean SSR deployment with a single node server/index.mjs entrypoint

Ship it!