uptime · 1414 days · 26 posts published · last deploy 1 hour, 22 minutes ago build:passing rss
~ / software-web / gitlab-ci-wagtail-django-build-migrationen-deploy.md
Software & Web · 14. Juni 2026 · ~10min · a8d381c

GitLab CI für Wagtail/Django: Build, Migrationen, Deploy

Vom Push zur Produktion: Lint & Test, Image-Build, sichere Migrationen und Rollout per Docker

>
devmaker.net
author · a8d381c · 2026-06-14
GitLab CI Wagtail Django Header.jpg 1024×1024
GitLab CI Wagtail Django Header
Django-Apps „von Hand” zu deployen ist fehleranfällig – eine vergessene Migration oder ein übersprungenes collectstatic reichen. Dieser Artikel zeigt eine schlanke, wiederholbare GitLab-CI-Pipeline für eine Wagtail/Django-App: Lint & Tests, ein unveränderliches Docker-Image (Code ins Image gebacken), sichere Migrationen beim Container-Start und ein ehrlicher Blick aufs Thema Zero-Downtime – alles aus echtem Betrieb, Secrets maskiert.

Warum überhaupt eine Pipeline?

Ein Django-Projekt „mal eben” auf den Server zu deployen, geht eine Weile gut – bis man eine Migration vergisst, collectstatic überspringt oder die halbe Konfiguration nur lokal existiert. Eine CI/CD-Pipeline macht den Weg vom git push zur Produktion wiederholbar und überprüfbar: testen, Image bauen, ausrollen – immer gleich, ohne Handarbeit.

Dieser Artikel zeigt mein Setup mit GitLab CI für eine Wagtail/Django-App, die als Docker-Image deployt wird. Secrets, Hostnamen und Registry sind maskiert – die Struktur ist 1:1 übertragbar.

Der Branch-Flow

Ich fahre einen einfachen, aber disziplinierten Flow:

  • develop – Arbeitsbranch, hier läuft die Test-Stage bei jedem Push.
  • main – Merge nach main löst den kompletten Build + Deploy auf Produktion aus.

Kein Direkt-Push auf main, kein Deploy ohne grüne Tests. Das allein verhindert die meisten „Freitagabend-Unfälle”.

Die Pipeline im Überblick

Drei Stages: testbuilddeploy. Nur main baut und deployt; develop testet nur.

stages:
  - test
  - build
  - deploy

variables:
  IMAGE: $CI_REGISTRY_IMAGE:production
  DOCKER_BUILDKIT: "1"

Stage 1: Lint & Test

Bevor irgendetwas gebaut wird, muss der Code testbar sauber sein. Linting (ruff) und die Test-Suite (pytest) laufen bei jedem Push – auf develop wie auf main.

test:
  stage: test
  image: python:3.11-slim
  services:
    - postgres:16
  variables:
    DATABASE_URL: "postgres://postgres:postgres@postgres:5432/test"
  before_script:
    - pip install -r requirements.txt -r requirements-dev.txt
  script:
    - ruff check .
    - python manage.py makemigrations --check --dry-run   # vergessene Migration = Fehler
    - pytest -q
makemigrations --check als Schutz

Der Schritt makemigrations --check --dry-run bricht die Pipeline ab, wenn Modelländerungen ohne passende Migration committet wurden. Das hat mir schon mehr als einen kaputten Deploy erspart.

Stage 2: Image bauen

Der Kern meiner Strategie: Der Code wird ins Image gebacken. Das Image ist damit ein unveränderliches Artefakt – exakt das, was getestet wurde, läuft später in Produktion. Kein git pull auf dem Server, keine Überraschungen.

build:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build -t "$IMAGE" .
    - docker push "$IMAGE"

Im Dockerfile wird neben dem Code auch collectstatic ausgeführt – die fertigen Static-Files liegen also schon im Image:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
CMD ["./entrypoint.sh"]

Stage 3: Deploy

Der Deploy-Job verbindet sich per SSH mit dem Produktiv-Host, zieht das frische Image und startet die Container neu. Die eigentliche Logik (Migrationen!) steckt im Container-Start, nicht im CI-Job – dazu gleich mehr.

deploy:
  stage: deploy
  image: alpine:3
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s); echo "$DEPLOY_SSH_KEY" | ssh-add -
  script:
    - ssh $DEPLOY_USER@$DEPLOY_HOST "cd /srv/app && docker compose pull && docker compose up -d"

Migrationen sicher fahren

Die wichtigste Frage bei jedem Django-Deploy: Wer führt die Migrationen aus – und wann? Ich mache es im Entrypoint des Containers, beim Start, bevor der Webserver hochfährt:

#!/bin/sh
set -e
python manage.py migrate --noinput
exec gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3
Zerstörerische Migrationen brauchen zwei Deploys

Migrationen müssen abwärtskompatibel sein: Während des Rollouts laufen kurz alte und neue Container parallel. Eine Spalte zu löschen, die der alte Code noch liest, crasht ihn. Regel: erst additive Migration + neuer Code, im nächsten Deploy das Aufräumen. Spalten umbenennen = niemals in einem Schritt.

Secrets & CI-Variablen

Keine Passwörter im Repo. Alles Sensible kommt aus den CI/CD-Variablen in GitLab (Settings → CI/CD → Variables), als protected + masked markiert:

# nur als Referenz – Werte stehen in GitLab, nicht im Repo
DEPLOY_SSH_KEY   # privater Key fuer den Deploy-User (protected)
DEPLOY_HOST      # Produktiv-Host
CI_REGISTRY_*    # von GitLab automatisch bereitgestellt
DJANGO_SECRET_KEY, DATABASE_URL  # via .env.production auf dem Host

Zero-Downtime – die ehrliche Variante

Ein einzelner Host mit docker compose up -d ist nicht echtes Zero-Downtime – es gibt ein kurzes Fenster beim Container-Tausch. Für meinen Anwendungsfall (eine Hobby-/Nebenprojekt-Site) ist das völlig okay: ein paar Sekunden, abgefedert durch einen Healthcheck und restart: unless-stopped.

Wer echtes Zero-Downtime braucht, kommt um Rolling- oder Blue-Green-Deployments nicht herum (zwei parallele Stacks, Umschalten per Reverse Proxy). Das ist ein eigenes Thema – und für die meisten Maker-Projekte Overkill.

Was ich weggelassen habe

Ehrliche Abgrenzung

Bewusst nicht behandelt: Caching der pip-Layer (spart Build-Zeit, lenkt hier aber ab), Review-Apps pro Merge Request, Container-Registry-Cleanup (alte Images stapeln sich), und echte Blue-Green-Deployments. Alles sinnvoll – aber nicht nötig, um sauber und wiederholbar zu deployen.

Fazit

Eine schlanke test → build → deploy-Pipeline nimmt dir den Stress aus dem Deployen: getesteter Code wird zum unveränderlichen Image, Migrationen laufen deterministisch beim Start, Secrets bleiben außerhalb des Repos. Bewusst pragmatisch, nicht überengineered – genau das richtige Maß für ein selbst gehostetes Django/Wagtail-Projekt.

// Weitere Empfehlungen

Anzeige · Affiliate-Link – kaufst du darüber, erhalte ich ggf. eine Provision. Für dich ändert sich am Preis nichts.

// responses (0)
> echo "your thoughts" >> gitlab-ci-wagtail-django-build-migrationen-deploy.responses

Schreibe einen Kommentar

Wird für die Bestätigung benötigt