Nuxt-3-Docker-Images verkleinern

Nuxt-3-Docker-Images verkleinern

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
5 min

Mein 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:

Dockerfile
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:

Dockerfile
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:

Dockerfile
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

Docker-Image-Größen aus dem ursprünglichen Nuxt-3-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.

Willkommen auf deiner Cloud-Plattform

Sliplane macht es einfach, deine Apps in der Cloud zu deployen und zu skalieren. Starte mit einem Container oder deinem Lieblings-Framework und wachse von da aus. Probier es jetzt aus!