Ultra‑Slim Multi‑Stage Image for SSR: Nuxt 4 + TypeScript + 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
.outputand 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:
- App: http://localhost:3000
- Health: http://localhost:3000/health
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/
9) Tips for even smaller images
- Copy only what you need: this Dockerfile already copies only
.outputto 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 runscorepack 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.0is 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.mjsentrypoint
Ship it!





