
Nuxt-3-Docker-Images verkleinern
Jonas ScholzMein erstes Nuxt-3-Docker-Image in diesem Versuch war etwa 1,4 GB groß. Am Ende waren es 164 MB. Den größten Unterschied machte ein einfacher Schritt: Build-Tools und Quellcode aus dem Runtime-Image entfernen.
Die Zahlen stammen aus meinem Projekt von 2023. Das Image wurde damals rund 88 Prozent kleiner. Wie viel du sparst, hängt von deiner App und deinem Basis-Image ab.
Mit einem einfachen Image anfangen
Dieses Dockerfile installiert die Pakete, baut Nuxt und startet den Nitro-Server:
FROM node:24
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", ".output/server/index.mjs"]
Das funktioniert für eine normale Nuxt-3-App mit npm und einer Lockdatei im Repository. Im fertigen Image bleiben aber auch der Quellcode, die Entwicklungsabhängigkeiten und die Build-Tools liegen.
1. Ein kleineres Basis-Image nehmen
Mit node:24-alpine sparst du Platz gegenüber dem vollständigen Node-Image. Alpine nutzt musl statt glibc. Test deshalb deine nativen Pakete. Der kleinere Download bringt dir wenig, wenn die App danach nicht startet.
Nimm für Build und Runtime dieselbe Basis. Brauchen deine Pakete Debian, nutze in beiden Stufen Debian slim.
2. Lokale Dateien ausschließen
Leg diese .dockerignore an:
node_modules
.output
.nuxt
.git
.env
.env.*
Docker soll die Pakete selbst installieren und eine neue .output erzeugen. Lokale Build-Dateien machen den Kontext größer und können von einem anderen Betriebssystem stammen. Ergänze weitere Verzeichnisse, die du nicht brauchst. Dateien für den Build müssen natürlich drinbleiben.
3. Nur die Server-Ausgabe kopieren
Beim Node-Server-Build legt Nuxt alles für die Runtime unter .output ab. Dieses Dockerfile baut die App in einer Stufe. Danach kopiert es .output in ein frisches Image:
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV NITRO_HOST=0.0.0.0
ENV NITRO_PORT=3000
COPY --from=build --chown=node:node /app/.output ./.output
USER node
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
Im fertigen Image liegen Node und .output. Der Rest des Repositories bleibt in der Build-Stufe. USER node startet den Server ohne Root-Rechte. NITRO_HOST und NITRO_PORT setzen Host und Port.
Beim Node-Server-Preset startet die App über .output/server/index.mjs. Nutzt dein Projekt ein anderes Deployment-Preset, pass die Config zuerst an. Die Nuxt-Dokumentation zum Deployment zeigt die Optionen.
Bau das Image und starte den Container:
docker build -t nuxt-app .
docker run --rm --name nuxt-app -p 3000:3000 nuxt-app
Schau danach nach, wie groß das Image ist:
docker image ls nuxt-app
Test unter http://localhost:3000 eine serverseitig gerenderte Route. Ruf auch ein Bild oder Stylesheet auf.
4. Lohnt sich Distroless noch?
Ein Distroless-Image sparte in meinem Versuch weitere 13 MB. Build-Tools und Quellcode zu entfernen hatte deutlich mehr gebracht.
Distroless-Images haben weder Shell noch Paketmanager. Die Node-Datei startet über den Entrypoint des Images. Tools wie curl stehen dir zum Debuggen nicht einfach zur Verfügung. Die verfügbaren Node-Tags findest du im Distroless-Repository.
Achte auch hier auf native Module. Kopier sie nicht ungeprüft aus einem Alpine-Build in ein Debian-basiertes Distroless-Image. Bau sie dafür in einer passenden Debian-Umgebung. Wenn du dir diese zusätzliche Arbeit sparen willst, bleib beim mehrstufigen Alpine-Image von oben.
5. Statische Dateien über ein CDN ausliefern
Nehmen statische Dateien viel Platz ein, kannst du sie über ein CDN ausliefern. Setz dafür Nuxts app.cdnURL, lade die erzeugten öffentlichen Dateien hoch und test ihre URLs. Erst wenn das klappt, entfernst du sie aus dem Container.
Ersetze dann das COPY für .output durch diese Zeile für die Serverdateien:
COPY --from=build --chown=node:node /app/.output/server ./.output/server
Das lohnt sich nicht für jede App. Du brauchst dann bei jedem Release auch einen Upload und musst den CDN-Cache verwalten. Soll ein einzelnes Image deine App samt statischen Dateien enthalten, behalte das vollständige .output-Verzeichnis.
Das Ergebnis aus meinem Versuch

Im Beispiel-Repository findest du das damalige Setup. Miss deinen eigenen Build. Die 164 MB sind kein Ziel, das jede App erreichen muss. Sobald Quellcode und Build-Tools draußen sind, lohnt sich mehr Aufwand für ein paar MB oft nicht.
Das fertige Dockerfile kannst du auf Sliplane deployen. Setz dafür den Service-Port auf 3000.