Zum Inhalt

8. Docker & Deployment

Dockerfile-Erklärung

Backend Dockerfile

Datei: backend/Dockerfile

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --ignore-scripts

COPY prisma ./prisma
COPY prisma.config.ts ./

RUN npm ci

COPY . .

RUN npx prisma generate

RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

Erklärung

  1. Base Image: node:20-alpine
  2. Minimaler Linux-Container (Alpine)
  3. Reduzierte Angriffsfläche vs. vollständige Node.js-Images

  4. Working Directory: /app

  5. Konsistenter Pfad für alle Container-Befehle

  6. Dependency Installation:

    COPY package*.json ./
    RUN npm ci --ignore-scripts
    

  7. Zuerst nur package.json kopieren (Docker Layer Caching)
  8. npm ci: Installiert exakte Versionen aus lock-file
  9. --ignore-scripts: Verhindert Ausführung von Scripts (Security-Maßnahme)

  10. Prisma Setup:

    COPY prisma ./prisma
    COPY prisma.config.ts ./
    RUN npm ci
    

  11. Prisma-Schema und Konfiguration kopieren
  12. Zweites npm ci zur Installation von Prisma-Dependencies

  13. Application Copy:

    COPY . .
    

  14. Kopiert restlichen Anwendungscode
  15. Ausgenutzt Docker Layer Caching (unveränderte Schichten werden gecacht)

  16. Prisma Client Generation:

    RUN npx prisma generate
    

  17. Generiert TypeScript-Client für Datenbank-Zugriff

  18. Build:

    RUN npm run build
    

  19. Kompiliert TypeScript zu JavaScript (dist/-Ordner)

  20. Runtime-Port:

    EXPOSE 3000
    

  21. Dokumentiert, dass Container Port 3000 nutzt
  22. (Nicht automatisches Publish)

  23. Start-Command:

    CMD ["npm", "start"]
    

  24. Startet Produktions-Server (node dist/src/index.js)

Frontend Dockerfile (Multi-Stage)

Datei: frontend/Dockerfile

# Stage 1: Build
FROM node:20-alpine as builder

WORKDIR /app

COPY package*.json ./

RUN npm install --prefer-offline --no-audit

RUN npm install @rollup/rollup-linux-x64-musl --no-save --no-audit

COPY . .

RUN npm run build

# Stage 2: Serve (Nginx)
FROM nginx:alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf

COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Stage 1: Builder

  1. Base Image: node:20-alpine as builder
  2. Temporärer Container für Build-Prozess
  3. Wird nicht im finalen Image enthalten

  4. Dependency Installation:

    RUN npm install --prefer-offline --no-audit
    

  5. --prefer-offline: Nutzt lokalen Cache
  6. --no-audit: Keine Audit-Berichte (Build-Speed)

  7. Rollup Platform-Specific Dependency:

    RUN npm install @rollup/rollup-linux-x64-musl --no-save --no-audit --prefer-offline
    

  8. Speziell für Alpine Linux (musl libc vs. glibc)
  9. Erforderlich für korrekte Vite-Builds

  10. Build:

    RUN npm run build
    

  11. Vite generiert statische Assets in dist/

Stage 2: Nginx

  1. Base Image: nginx:alpine
  2. Minimaler Webserver-Container
  3. Keine Node.js-Runtime erforderlich

  4. Nginx Configuration:

    COPY nginx.conf /etc/nginx/conf.d/default.conf
    

  5. Kopiert Custom Nginx-Konfiguration

  6. Static Files:

    COPY --from=builder /app/dist /usr/share/nginx/html
    

  7. Kopiert dist/ aus Builder-Stage
  8. --from=builder: Nutzt Dateien aus vorheriger Stage

  9. Runtime-Port:

    EXPOSE 80
    

  10. HTTP-Standard-Port

  11. Start-Command:

    CMD ["nginx", "-g", "daemon off;"]
    

  12. Startet Nginx im Vordergrund (Docker-konform)

Sicherheitsaspekte der Images

Base Images

Backend: - node:20-alpine - Vorteile: Kleines Image (~50 MB), reduzierte Angriffsfläche - Nachteil: musl libc vs. glibc (glibc-kompatibler)

Frontend: - node:20-alpine (Builder-Stage) - nginx:alpine (Runtime-Stage) - Vorteile: Minimaler Runtime-Container (~10 MB)

Best Practices

1. Multi-Stage Builds

Vorteil: Runtime-Image enthält keine Build-Tools

# Builder-Stage hat Compiler, Dependencies, etc.
FROM node:20-alpine as builder
RUN npm install
RUN npm run build

# Runtime-Stage hat nur statische Files und Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

2. Minimal Layers

Docker Layer Caching:

# Unverändertes: Wird gecacht
COPY package*.json ./
RUN npm ci

# Verändert bei Code-Change: Wird neu gebaut
COPY . .
RUN npm run build

3. No Scripts

RUN npm ci --ignore-scripts

Security: Verhindert Ausführung von bösartigen Scripts in node_modules

4. No Secrets in Images

# ❌ Schlecht
COPY .env ./

# ✅ Gut
ENV DATABASE_URL=${DATABASE_URL}

Security: Secrets nur via Environment-Variablen

Offene Sicherheitsverbesserungen

1. Non-Root User

Aktuell: Container läuft als root

Empfehlung:

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

USER nodejs

CMD ["npm", "start"]

2. Read-only Filesystem

Aktuell: Filesystem ist beschreibbar

Empfehlung:

# docker-compose.yml
services:
  backend:
    read_only: true
    tmpfs:
      - /tmp

3. Image Scanning

Aktuell: Kein Image-Scanning

Empfehlung:

# Lokal
trivy image securenotes-backend:latest

# CI/CD Integration
- name: Trivy Scan
  uses: aquasecurity/trivy-action@master

4. Image Signing

Aktuell: Keine Image-Signatur-Verifikation

Empfehlung:

# Image signieren
docker trust sign securenotes-backend:latest

# Image verifizieren
docker trust verify securenotes-backend:latest

Docker Compose Struktur

Vollständige Konfiguration

Datei: docker-compose.yml

services:
  postgresql:
    image: postgres:alpine
    container_name: securenotes-db
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  mailpit:
    image: axllent/mailpit:latest
    container_name: securenotes-mailpit
    ports:
      - "${SMTP_PORT:-1025}:1025"
      - "${WEB_UI_PORT:-8025}:8025"

  backend:
    build: ./backend
    container_name: securenotes-backend
    ports:
      - "${PORT}:${PORT}"
    depends_on:
      - postgresql
      - mailpit
    env_file:
      - .env
      - ./backend/.env
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgresql:5432/${POSTGRES_DB}?schema=public
      PORT: ${PORT}
      FRONTEND_URL: ${FRONTEND_URL}
      FRONTEND_BASE_URL: ${FRONTEND_BASE_URL}
      SMTP_HOST: mailpit
      SMTP_PORT: 1025
      SMTP_SECURE: "false"
      MAIL_FROM: ${MAIL_FROM}
      MAIL_ENABLED: "true"
    command: sh -c "npx prisma migrate deploy && node dist/src/index.js"

  frontend:
    build: ./frontend
    container_name: securenotes-frontend
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  postgres_data:

Service-Beschreibung

PostgreSQL Service

postgresql:
  image: postgres:alpine
  container_name: securenotes-db
  environment:
    POSTGRES_USER: ${POSTGRES_USER}
    POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    POSTGRES_DB: ${POSTGRES_DB}
  ports:
    - "5432:5432"
  volumes:
    - postgres_data:/var/lib/postgresql/data
  • Image: postgres:alpine (Minimal)
  • Environment: DB-Credentials via .env
  • Ports: 5432 → Host (nur für Entwicklung!)
  • Volumes: Persistente Daten (postgres_data)

Mailpit Service

mailpit:
  image: axllent/mailpit:latest
  container_name: securenotes-mailpit
  ports:
    - "${SMTP_PORT:-1025}:1025"
    - "${WEB_UI_PORT:-8025}:8025"
  • Image: axllent/mailpit:latest
  • Ports:
  • 1025: SMTP-Server
  • 8025: Web UI

Web UI Access: http://localhost:8025

Backend Service

backend:
  build: ./backend
  container_name: securenotes-backend
  ports:
    - "${PORT}:${PORT}"
  depends_on:
    - postgresql
    - mailpit
  env_file:
    - .env
    - ./backend/.env
  environment:
    DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgresql:5432/${POSTGRES_DB}?schema=public
    PORT: ${PORT}
    FRONTEND_URL: ${FRONTEND_URL}
    FRONTEND_BASE_URL: ${FRONTEND_BASE_URL}
    SMTP_HOST: mailpit
    SMTP_PORT: 1025
    SMTP_SECURE: "false"
    MAIL_FROM: ${MAIL_FROM}
    MAIL_ENABLED: "true"
  command: sh -c "npx prisma migrate deploy && node dist/src/index.js"
  • Build: Aus ./backend-Verzeichnis
  • Ports: ${PORT} → Host (Default: 3000)
  • Dependencies: Wartet auf postgresql und mailpit
  • Env Files: Lädt .env und ./backend/.env
  • Command: Migrationen + Start

Frontend Service

frontend:
  build: ./frontend
  container_name: securenotes-frontend
  ports:
    - "80:80"
  depends_on:
    - backend
  • Build: Multi-Stage Build aus ./frontend
  • Ports: 80 → Host
  • Dependencies: Backend muss laufen

Volumes

volumes:
  postgres_data:
  • Purpose: Persistente Datenbank-Daten
  • Lifecycle: Überlebt Container-Neustarts
  • Backup: Kann extern gesichert werden

Netzwerk- und Port-Konfiguration

Standard-Ports

Service Container-Port Host-Port Beschreibung
Frontend (Nginx) 80 80 HTTP-Access
Backend (Express) 3000 3000 API-Access
PostgreSQL 5432 5432 DB-Access (Entwicklung)
Mailpit SMTP 1025 1025 SMTP-Server
Mailpit Web UI 8025 8025 Web-Interface

Docker-Netzwerk

# Implizites Standard-Netzwerk
docker-compose up

# Erstellt: securenotes_default network

Container-Kommunikation

Backend → PostgreSQL:

DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgresql:5432/${POSTGRES_DB}
#                                                            ^^^^^^^^^^^^
#                                                            Container-Name

Backend → Mailpit:

SMTP_HOST=mailpit
#          ^^^^^^^^^^^
#          Container-Name

Frontend → Backend:

# Via Host-Port-Mapping
http://localhost:3000

# Oder via Docker-Netzwerk (empfohlen)
http://backend:3000

Sicherheitsverbesserungen

1. PostgreSQL-Port nicht exponieren

Aktuell: Port 5432 ist offen

Empfehlung:

postgresql:
  image: postgres:alpine
  # ports:
  #   - "5432:5432"  # Entferne oder kommentiere aus

Alternative für lokale Entwicklung:

postgresql:
  ports:
    - "127.0.0.1:5432:5432"  # Nur localhost

2. User-defined Network

Aktuell: Standard-Bridge-Netzwerk

Empfehlung:

networks:
  securenotes-net:
    driver: bridge

services:
  postgresql:
    networks:
      - securenotes-net

  backend:
    networks:
      - securenotes-net

  frontend:
    networks:
      - securenotes-net

3. Health Checks

Aktuell: Keine Health Checks

Empfehlung:

services:
  backend:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Nächste Schritte