Warum Docker-Images auf Gigabytes anschwellen
Ein typisches Dockerfile für eine Rails-Anwendung: ruby:3.2 nehmen, Node.js für Asset-Kompilierung installieren, build-essential für native Gems, Code kopieren, bundle install und assets:precompile ausführen. Ergebnis — ein 1,5-2 GB großes Image mit GCC-Compiler, Header-Dateien, Node.js mit npm, Bundler-Cache — alles, was zur Laufzeit nicht benötigt wird.
Jedes zusätzliche Megabyte bedeutet langsamere Pulls, längere Deploys, mehr Speicherplatz, größere Angriffsfläche. Bei CI mit 50 Deploys pro Tag ist der Unterschied zwischen 2 GB und 150 MB stundenlange Wartezeit und Terabytes an Traffic pro Monat.
Multi-Stage Builds lösen das Problem: ein Dockerfile, mehrere Stufen (FROM). Jede Stufe ist eine separate Umgebung. Das finale Image enthält nur das, was zum Ausführen nötig ist.
Das Multi-Stage-Prinzip
Einfachstes Beispiel
# Stufe 1: Build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server
# Stufe 2: Runtime
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /server /server
EXPOSE 8080
CMD ["/server"]
Der erste FROM erstellt eine Stufe mit dem Go-Compiler (~800 MB). Die Binary wird kompiliert. Der zweite FROM beginnt mit einem sauberen Alpine (~7 MB). COPY --from=builder kopiert nur die Binary. Finales Image: ~15 MB statt 800 MB.
Rails: Von 1,8 GB auf 120 MB
# Stufe 1: Basis mit Runtime-Abhängigkeiten
FROM ruby:3.2-slim AS base
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y libpq5 curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ENV RAILS_ENV=production \
BUNDLE_DEPLOYMENT=1 \
BUNDLE_WITHOUT="development:test"
# Stufe 2: Gem-Kompilierung
FROM base AS gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle install && rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache
# Stufe 3: Asset-Kompilierung
FROM base AS assets
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install --no-install-recommends -y nodejs && \
npm install -g yarn && rm -rf /var/lib/apt/lists/*
COPY --from=gems /usr/local/bundle /usr/local/bundle
COPY Gemfile Gemfile.lock package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production
COPY . .
RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile && \
rm -rf node_modules tmp/cache
# Stufe 4: Finales Production-Image
FROM base AS production
COPY --from=gems /usr/local/bundle /usr/local/bundle
COPY --from=assets /app /app
RUN useradd -m rails && chown -R rails:rails /app
USER rails
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
# Größe: ~120 MB
Von 1,8 GB auf 120 MB — 15x kleiner.
Node.js / TypeScript
Express + TypeScript
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && \
cp -R node_modules /prod_modules && \
npm ci
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Größe: ~80 MB
Python / Django / FastAPI
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
FROM python:3.12-slim AS production
RUN apt-get update && apt-get install --no-install-recommends -y libpq5 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN python manage.py collectstatic --noinput
RUN useradd -m django
USER django
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
# Größe: ~180 MB
PHP / Laravel
FROM composer:2 AS vendor
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --prefer-dist --ignore-platform-reqs
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json vite.config.js ./
COPY resources/ ./resources/
RUN npm ci && npm run build
FROM php:8.4-fpm-alpine AS production
RUN apk add --no-cache libpq postgresql-dev \
&& docker-php-ext-install pdo_pgsql opcache \
&& apk del postgresql-dev
WORKDIR /app
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
RUN php artisan config:cache && php artisan route:cache
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["php-fpm"]
# Größe: ~95 MB
Go: Minimale Images
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /server ./cmd/server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /server /server
ENTRYPOINT ["/server"]
# Größe: ~8 MB
scratch ist ein leeres Image, 0 Bytes. Kein Shell, kein libc, nichts. Nur die Binary und SSL-Zertifikate. Perfekt für Go mit CGO_ENABLED=0.
Layer-Caching-Optimierung
COPY-Reihenfolge ist wichtig
# Schlecht: Änderung JEDER Datei invalidiert bundle install Cache
COPY . .
RUN bundle install
# Gut: Abhängigkeitsdateien zuerst, dann Code
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
.dockerignore ist Pflicht
.git
node_modules
tmp
log
.env*
spec
test
*.md
BuildKit Cache Mounts
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.bundle/cache \
bundle install
RUN --mount=type=cache,target=/root/.npm \
npm ci
Cache bleibt zwischen Builds erhalten, landet aber NICHT im Image.
Sicherheit
Non-Root-Benutzer
RUN useradd -m appuser
USER appuser
Niemals Anwendungen als Root in Containern ausführen.
Basis-Image-Vergleich
| Basis-Image | Größe | CVEs (typisch) | Shell | Paketmanager |
|---|---|---|---|---|
| ubuntu:24.04 | 78 MB | 50-100 | Ja | apt |
| python:3.12-slim | 45 MB | 20-40 | Ja | apt |
| python:3.12-alpine | 17 MB | 5-15 | Ja | apk |
| distroless | 3-15 MB | 0-5 | Nein | Nein |
| scratch | 0 MB | 0 | Nein | Nein |
Schwachstellen-Scanning
# Trivy
docker run --rm aquasec/trivy image myapp:latest
# Docker Scout
docker scout cves myapp:latest
Größenvergleich: Vorher und Nachher
| Stack | Monolithisches Dockerfile | Multi-Stage | Einsparung |
|---|---|---|---|
| Rails 8 | 1,8 GB | 120 MB | 93% |
| Next.js 15 | 1,0 GB | 120 MB | 88% |
| Laravel 11 | 800 MB | 95 MB | 88% |
| Django 5 | 700 MB | 150 MB | 79% |
| FastAPI | 500 MB | 80 MB | 84% |
| Go (gin) | 800 MB | 8 MB | 99% |
| Express + TS | 400 MB | 80 MB | 80% |
Abschließende Checkliste
Dockerfile-Struktur
- [ ] Mindestens 2 Stufen: Builder + Production
- [ ] Für Rails/Laravel/Next.js: 3-4 Stufen (Base, Deps, Assets, Production)
- [ ] Finale Stufe auf slim/alpine/distroless
- [ ]
.dockerignore— .git, node_modules, tmp, log ausschließen
Caching
- [ ] Zuerst Abhängigkeitsdateien COPY, dann Code
- [ ]
--mount=type=cachefür Bundler/npm/pip-Cache - [ ] BuildKit aktiviert
- [ ] CI:
cache-from: type=ghafür GitHub Actions
Sicherheit
- [ ] Non-Root USER in finaler Stufe
- [ ] Keine Secrets im Image
- [ ] Scanning: Trivy/Scout in CI-Pipeline
- [ ] Basis-Image: regelmäßig aktualisieren
Produktion
- [ ]
HEALTHCHECK-Instruktion - [ ] Image-Größe < 200 MB für interpretierte Sprachen, < 20 MB für Go/Rust
- [ ] Multi-Platform: linux/amd64 + linux/arm64
- [ ] Tags:
:latest+:sha(für Rollback)
Kommentare (0)