Multi-stage Docker builds — Alpine, hardening i minimalne obrazy produkcyjne
Obraz z naiwnego Dockerfile może ważyć 1 GB. Pokazuję jak zredukowałem obrazy produkcyjne przez multi-stage builds, Alpine Linux i usunięcie zbędnych narzędzi.
Problem z prostymi Dockerfile
Naiwny Dockerfile dla aplikacji Laravel:
FROM php:8.5-fpm
RUN apt-get install -y git zip unzip nodejs npm
COPY . /var/www
RUN composer install
RUN npm install && npm run build
Wynikowy obraz: ~1.2 GB. Zawiera narzędzia build-time (git, npm, composer) których produkcja nie potrzebuje.
Multi-stage build — podział na etapy
# Etap 1: build assets (Node.js)
FROM node:22-alpine AS assets
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY resources/ resources/
COPY vite.config.js ./
RUN npm run build
# Etap 2: install PHP dependencies
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Etap 3: obraz produkcyjny
FROM php:8.5-fpm-alpine AS production
WORKDIR /var/www
# Kopiujemy tylko artefakty z poprzednich etapów
COPY --from=composer /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
RUN chown -R www-data:www-data /var/www
USER www-data
Wynikowy obraz: ~180 MB — 85% mniej.
Hardening kontenera
Kilka zasad bezpieczeństwa, które stosuję:
Non-root user:
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser
Read-only filesystem (tam gdzie możliwe):
# docker-compose.prod.yml
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /var/run
Minimalny obraz — Alpine zamiast Debian: Alpine Linux ma ~5 MB w porównaniu do ~80 MB Debiana. Mniej pakietów = mniejsza powierzchnia ataku.
STOPSIGNAL SIGQUIT dla PHP-FPM:
STOPSIGNAL SIGQUIT
PHP-FPM na SIGQUIT robi graceful shutdown — dokańcza aktywne requesty zanim się wyłączy. Bez tego K8s mógłby zabić kontener w trakcie obsługi requestu.
OCI labels dla trackowalności
LABEL org.opencontainers.image.version="0.0.4"
LABEL org.opencontainers.image.revision="${GIT_COMMIT}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
LABEL org.opencontainers.image.source="https://github.com/szymonborowski/portfolio"
Dzięki temu patrząc na działający kontener wiem dokładnie z jakiego commitu pochodzi.