Zum Inhalt springen
myvpsguide

Eigene Docker-Images bauen: klein, schnell neu gebaut, ohne root

Ein Dockerfile, das den Build-Cache nutzt, in mehreren Stufen baut, ohne root läuft, feste Basisversionen hat und keine Geheimnisse in einer Schicht vergräbt.

veröffentlicht 19.09.2026
3 min lesezeit
fortgeschritten

Ein Dockerfile ist schnell geschrieben, und das erste funktioniert meist auch. Die Probleme zeigen sich später: Jede kleine Code-Änderung löst einen kompletten Neubau aus, das Image enthält Compiler und Quellcode, die niemand zur Laufzeit braucht, und der Prozess darin läuft als root. Fünf Regeln beheben das, und sie gelten für fast jede Sprache.

Regel 1: Reihenfolge für den Cache#

Docker baut ein Image Schicht für Schicht und merkt sich jede. Ändert sich eine Schicht, werden alle folgenden neu gebaut. Also: was sich selten ändert, nach oben.

# schlecht: jede Code-Änderung installiert alle Abhängigkeiten neu
COPY . .
RUN npm ci

# gut: Abhängigkeiten nur neu, wenn sich package*.json ändert
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Dasselbe Muster gilt für requirements.txt bei Python, go.mod bei Go, Gemfile bei Ruby.

Regel 2: in Stufen bauen#

Zum Bauen brauchst du Compiler, Entwicklungswerkzeuge und Quellcode. Zum Ausführen nur das Ergebnis. Ein mehrstufiger Build trennt das:

# Stufe 1: bauen
FROM node:22-bookworm-slim AS bau
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

# Stufe 2: ausführen
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=bau /app/node_modules ./node_modules
COPY --from=bau /app/dist ./dist
COPY package.json ./
USER node
CMD ["node", "dist/server.js"]

Nur die letzte Stufe landet im fertigen Image. Build-Werkzeuge, Quellcode und Entwicklungsabhängigkeiten bleiben in Stufe 1 zurück.

Bei kompilierten Sprachen wird der Unterschied besonders groß. Ein Go-Programm braucht zur Laufzeit oft nur die eine Datei:

FROM golang:1.23 AS bau
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=bau /app /app
USER nonroot
ENTRYPOINT ["/app"]

Die Versionsnummern in den Beispielen sind Platzhalter: Nimm die, die dein Projekt tatsächlich nutzt.

Regel 3: nicht als root#

Läuft der Prozess im Container als root und findet ein Angreifer eine Lücke in der Anwendung, ist er im Container root, und jede Fehlkonfiguration am Host wird damit gefährlicher. Viele offizielle Images bringen einen Benutzer mit (node, nonroot, www-data). Sonst legst du einen an:

RUN useradd --system --uid 10001 --no-create-home app
USER app

Die feste UID hilft bei Volumes: Die Dateirechte auf dem Host lassen sich dann gezielt setzen. Mehr zu den Grenzen von Containern steht in Podman ohne Root.

Regel 4: feste Basisversionen#

FROM node:latest baut heute ein anderes Image als in drei Monaten. Ein Tag mit Hauptversion und Distribution (node:22-bookworm-slim) ist berechenbar. Wer es ganz genau will, hängt den Digest an:

FROM node:22-bookworm-slim@sha256:<digest>

Den Digest zeigt docker buildx imagetools inspect node:22-bookworm-slim. Er ändert sich mit jedem Update des Basisimages, und genau deshalb musst du ihn bewusst nachziehen, am besten automatisch über Renovate oder Dependabot. Wie du Images danach aktuell hältst, steht in Container aktuell halten.

Welche Basis? -slim-Varianten auf Debian sind ein guter Standard. Alpine ist kleiner, nutzt aber musl statt glibc; manche Programme verhalten sich dort anders oder brauchen eigene Builds. Distroless-Images enthalten nicht einmal eine Shell, ideal für statische Programme, unpraktisch zum Fehlersuchen.

Regel 5: keine Geheimnisse in Schichten#

# Falsch: das Token steckt für immer in einer Schicht des Images
COPY .npmrc .
RUN npm ci
RUN rm .npmrc

Das rm löscht die Datei nur in der nächsten Schicht. Wer das Image hat, holt sie aus der vorherigen. Richtig ist ein Build-Secret, das nur während eines Schritts eingehängt wird:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc -t meine-app .

Laufzeit-Geheimnisse wie Datenbankpasswörter gehören gar nicht ins Image, sondern werden beim Start übergeben, siehe Passwörter in Containern.

Die .dockerignore#

Ohne sie schickt docker build alles im Verzeichnis an den Build, auch .git, node_modules und die .env mit den Zugangsdaten.

# .dockerignore
.git
node_modules
dist
.env
*.log
Dockerfile

Prüfen, was drin ist#

docker image ls meine-app              # Größe
docker history meine-app               # welche Schicht wie groß
docker run --rm meine-app id           # läuft wirklich nicht als root?

Einen Sicherheitsscan des fertigen Images erledigen Werkzeuge wie Trivy oder Docker Scout. Sie melden bekannte Lücken in den Paketen des Basisimages, also genau das, was eine feste, aber regelmäßig aktualisierte Basis behebt.

Häufige Fragen#

Mein Image ist trotz mehrstufigem Build groß.#

docker history zeigt, welche Schicht zählt. Häufig: ein apt-get install ohne --no-install-recommends und ohne anschließendes rm -rf /var/lib/apt/lists/* in derselben RUN-Zeile.

Muss ich HEALTHCHECK ins Dockerfile schreiben?#

Kann man, muss man nicht. Oft ist es praktischer, ihn in der Compose-Datei festzulegen, weil er dort je Umgebung angepasst werden kann. Was er leistet und was nicht, steht in Healthchecks und Restart-Regeln.

Warum CMD in eckigen Klammern?#

Die Schreibweise mit [...] startet den Prozess direkt als PID 1. Ohne Klammern läuft er unter einer Shell, die Signale wie SIGTERM nicht weiterreicht. Die Folge: docker stop wartet zehn Sekunden und beendet den Prozess dann hart.

Baue ich auf dem Server oder woanders?#

Am besten woanders, in einer CI oder auf dem eigenen Rechner, und der Server zieht nur fertige Images. Das hält Build-Werkzeuge und Quellcode vom Produktivsystem fern.

Kurz gesagt#

  • Was sich selten ändert, nach oben: Abhängigkeiten vor dem Code kopieren.
  • Mehrstufig bauen, nur das Ergebnis ins letzte Image.
  • USER setzen; nicht als root laufen.
  • Feste Basisversionen, bewusst und automatisiert nachziehen.
  • Geheimnisse per --mount=type=secret, nie per COPY und rm.
MRMedia

Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.