Logo von Miran Arnaut Logo von Miran Arnaut
Development 5 Min. Lesezeit

Docker für Webentwickler — Mein Deployment-Workflow als Freelancer

Wie ich Docker für meine Webentwicklung-Projekte einsetze: Container-Entwicklung, CI/CD-Pipeline und automatisierte Deployments auf eigener Infrastruktur.

Docker ist für mich kein Nice-to-have mehr — es ist das Fundament meiner gesamten Entwicklungs- und Deployment-Infrastruktur. Jedes Projekt, das ich ausliefere, läuft in Docker-Containern, wird über eine CI/CD-Pipeline gebaut und automatisch auf meiner Server-Infrastruktur deployed.

In diesem Beitrag zeige ich dir meinen genauen Workflow — von der lokalen Entwicklung bis zum Production-Deployment.

Warum Docker?

Bevor ich Docker genutzt habe, sah mein Deployment so aus:

  1. Per FTP Dateien auf den Server kopieren
  2. Hoffen, dass die PHP/Konfiguration auf dem Server mit der lokalen Umgebung übereinstimmt
  3. Bei Fehlern: stundenlanges Debuggen, warum es lokal funktioniert, aber auf dem Server nicht

Docker löst dieses Problem radikal: Die Umgebung, in der du entwickelst, ist identisch mit der Umgebung, in der die Website läuft. Kein „Es funktioniert aber auf meinem Computer!“ mehr.

Die Vorteile für mich als Freelancer:

  • Reproduzierbarkeit — Jedes Projekt hat eine exakte Definition seiner Laufzeitumgebung
  • Isolation — Projekte beeinflussen sich nicht gegenseitig
  • Deployment-Sicherheit — Was lokal läuft, läuft auch auf dem Server
  • Skalierbarkeit — Ein Docker-Container lässt sich horizontal skalieren, falls nötig

Mein Docker-Setup pro Projekt

Jedes Projekt bekommt ein docker-compose.yml im Root-Verzeichnis. Hier ein Beispiel für ein typisches Angular + Astro + Vue.js Projekt:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development

  caddy:
    image: caddy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    depends_on:
      - app

volumes:
  caddy_data:

Warum Caddy? Weil Caddy automatische SSL-Zertifikate über Let’s Encrypt ausstellt. Kein manuelles Zertifikatsmanagement mehr.

Der Dockerfile-Ansatz

Ich verwende zwei Dockerfiles: eins für die Entwicklung und eins für die Produktion.

Dockerfile.dev — Für die lokale Entwicklung:

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Dockerfile.prod — Für den Production-Build mit Multi-Stage-Build:

# Stage 1: Build
FROM node:22-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: Production
FROM nginx:stable-alpine

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

EXPOSE 80

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

Der Multi-Stage-Build sorgt dafür, dass das finale Image nur das Nötigste enthält — kein Node.js, keine Entwicklungs-Abhängigkeiten. Das spart Speicherplatz und reduziert die Angriffsfläche.

CI/CD mit GitHub Actions

Mein Deployment läuft voll automatisiert über GitHub Actions. Sobald ich Code in den main-Branch pushe, passiert Folgendes:

  1. Tests laufen (TypeScript-Check, Linting, Build-Test)
  2. Docker-Image wird gebaut mit dem Production-Dockerfile
  3. Image wird in die GitHub Container Registry gepusht
  4. SSH in den Server und Deployment-Skript ausführen

Hier ist meine .github/workflows/deploy.yml:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/miran-arnaut/projektname:latest

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /opt/projekte/projektname
            docker compose pull
            docker compose up -d --force-recreate
            docker system prune -f

Deployment-Infrastruktur

Mein Server-Setup sieht so aus:

  • Ein Virtual Private Server (VPS) mit Ubuntu 24.04, 4 GB RAM, 2 CPUs
  • Docker und Docker Compose installiert
  • Caddy als Reverse Proxy mit automatischen SSL-Zertifikaten
  • Cloudflare Tunnel für DDoS-Schutz und schnelleres Routing
  • Automatische Backups via Cron-Job (tägliches Backup der Docker-Volumes)

Auf dem Server läuft ein docker-compose.yml, das alle laufenden Projekte verwaltet:

version: '3.8'

services:
  projekt-a:
    image: ghcr.io/miran-arnaut/projekt-a:latest
    restart: always
    networks:
      - web

  projekt-b:
    image: ghcr.io/miran-arnaut/projekt-b:latest
    restart: always
    networks:
      - web

  caddy:
    image: caddy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    networks:
      - web

networks:
  web:
    external: true

volumes:
  caddy_data:

Jedes Projekt läuft isoliert in seinem eigenen Container. Caddy routet die Domain zum richtigen Container.

Monitoring

Deployment ist eine Sache — zu wissen, dass alles läuft, eine andere. Ich verwende:

  • Uptime Kuma (selbst gehostet) — pingt alle 5 Minuten jede Website an
  • Docker Healthchecks — jeder Container hat einen Healthcheck, der automatisch Neustarts auslöst
  • Caddy Logs — werden automatisch rotiert und für 30 Tage aufbewahrt
  • E-Mail-Benachrichtigungen — wenn ein Container crasht oder eine Website nicht erreichbar ist

Was ich gelernt habe

Docker ist am Anfang eine Lernkurve. Hier sind die Dinge, die ich gerne früher gewusst hätte:

  1. Verwende Named Volumes, nicht Bind Mounts für Datenbanken. Sonst verlierst du Daten beim Neustart.
  2. Setze immer Ressourcen-Limits. --memory="512m" verhindert, dass ein fehlerhafter Container den ganzen Server lahmlegt.
  3. Dokumentiere deine Docker-Befehle. Wenn du ein Projekt sechs Monate nicht anfasst, weißt du nicht mehr, wie man es startet.
  4. Nutze Docker Networks. Isolierte Netzwerke zwischen Containern sind sicherer und sauberer.
  5. Prune regelmäßig. docker system prune -f in der CI/CD-Pipeline hält den Server sauber.

Lohnt sich der Aufwand?

Absolut. Ja, Docker bedeutet zusätzliche Komplexität am Anfang. Aber die Zeit, die ich früher mit manuellen Deployments, Konfigurationsproblemen und „Es funktioniert bei mir”-Debugging verbracht habe, ist jetzt praktisch Null.

Jedes neue Projekt bekommt in 15 Minuten sein Docker-Setup. Jedes Deployment läuft automatisch. Jeder Server ist identisch zur Entwicklungsumgebung.

Für mich als Freelancer, der mehrere Projekte gleichzeitig betreut, ist Docker der Unterschied zwischen chaotischem und kontrolliertem Arbeiten.


Du willst dein nächstes Projekt mit Docker und CI/CD aufsetzen? Ich helfe dir dabei. Schreib mir an arnaut@miran.at.

(05) Kontakt aufnehmen

Let’s Talk

Schreib mir eine Nachricht oder verbinde dich über Social Media.