Artikel
KI-Artikel 6 Min 728

Docker Multi-Stage Builds: Rails, Node, PHP, Go Images unter 100 MB

Docker-Images 10-15x verkleinern mit Multi-Stage Builds. Beispiele für Rails, Next.js, Laravel, Django, FastAPI, Go. CI/CD, Caching, Sicherheit.

Dieser Artikel wurde von einem KI-Modell erstellt und kann Ungenauigkeiten enthalten. Überprüfen Sie die Informationen vor der Verwendung in der Produktion.

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=cache für Bundler/npm/pip-Cache
  • [ ] BuildKit aktiviert
  • [ ] CI: cache-from: type=gha fü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)