|

Produktions-PHP-Docker-Images bauen — GitHub Actions CI/CD (Teil 3/3)

In Teil 1 haben wir das Base-Image mit all seinen Compile-Flags und Runtime-Tuning gebaut. In Teil 2 haben wir Custom-Module und Variationen aufgeschichtet. Jetzt wird ausgeliefert.

Manuelle Builds brechen auf vorhersehbare Weise: Jemand vergisst ein Flag, baut vom falschen Branch oder pusht ein Image mit Tag latest über eine bekannt-gute Version. CI/CD eliminiert all das. Code pushen, Images bekommen. PR öffnen, Preview-Images bekommen. Base-Version releasen, Button klicken.

Wir verwenden zwei GitHub-Actions-Workflows: einen für Variationen (automatisch bei Push/PR) und einen für Base-Images (manuell über workflow_dispatch). Beide bauen Multi-Architektur-Images und pushen in eine private Registry.

Workflow 1: Variation-Builds

Dieser Workflow läuft bei jedem Push auf main und bei jedem Pull Request gegen main:

name: Build Workflow
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  BASE_VERSION: "1.0.0"
  RELEASE_VERSION: "1.0.0"
  PHP_VERSIONS: "8.4,8.1"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Build images (pull_request)
        if: github.event_name == 'pull_request'
        run: >
          bash ./build.sh
          --base-version "$BASE_VERSION"
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"
          --hash "${{ github.event.pull_request.head.sha }}"          

      - name: Build images (push)
        if: github.event_name != 'pull_request'
        run: >
          bash ./build.sh
          --base-version "$BASE_VERSION"
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"
          --with-dev          

      - name: Push images (pull_request)
        if: github.event_name == 'pull_request'
        run: >
          bash ./push.sh
          --base-version "$BASE_VERSION"
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"
          --hash "${{ github.event.pull_request.head.sha }}"          

      - name: Push images (push)
        if: github.event_name != 'pull_request'
        run: >
          bash ./push.sh
          --base-version "$BASE_VERSION"
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"
          --with-dev          

Der Workflow ist geradlinig — ein Job, sequenzielle Steps. Ein paar Dinge, die erwähnenswert sind:

Versions-Pinning in env. BASE_VERSION und RELEASE_VERSION werden auf Workflow-Ebene definiert. Um eine Version zu bumpen, ändert man eine Zeile — nicht drei Shell-Skripte und ein Dockerfile.

Containerd Image Store für Multi-Arch. Der Docker-Daemon des Runners ist mit dem Containerd Image Store konfiguriert ("features": { "containerd-snapshotter": true }). Das ermöglicht native Multi-Plattform-Builds mit docker buildx build --platform linux/amd64,linux/arm64 — kein QEMU-Emulations-Setup, keine zusätzlichen Builder-Instanzen. Die Build-Skripte handhaben die Plattform-Flags intern.

PR-Builds nutzen --hash. Images werden mit dem Commit-SHA getaggt: amber-8.4-1.0.0-abc123. Reviewer können genau den Build von diesem PR pullen, testen und wissen, dass er mit dem Code übereinstimmt, den sie reviewed haben.

Main-Branch-Builds nutzen --with-dev. Wenn Code auf main landet, bauen wir sowohl Produktions- als auch Dev-Images. Dev-Images beinhalten Xdebug für Entwicklungsumgebungen, die dem neuesten stabilen Release folgen.

cancel-in-progress: false. Docker-Builds sind teuer. Einen laufenden Build abzubrechen, um einen neuen zu starten, verschwendet Rechenleistung und kann halb gepushte Layer in deiner Registry hinterlassen. Lass ihn fertig werden, dann starte den nächsten.

Workflow 2: Base-Image-Releases

Base-Images ändern sich seltener. Neue PHP-Extensions, FPM-Tuning-Updates oder OS-Paket-Upgrades rechtfertigen eine neue Base-Version. Dieser Workflow nutzt workflow_dispatch — er läuft nur, wenn jemand auf „Run workflow" in der GitHub-Actions-UI klickt:

name: Base Release Workflow
on:
  workflow_dispatch:
    inputs:
      release_version:
        description: "Release version to publish, for example 1.0.2"
        required: true
        type: string
      php_versions:
        description: "Comma-separated PHP versions, for example 8.4,8.1"
        required: true
        default: "8.4,8.1"
        type: string

jobs:
  release:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    env:
      RELEASE_VERSION: ${{ github.event.inputs.release_version }}
      PHP_VERSIONS: ${{ github.event.inputs.php_versions }}
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Build base images
        run: >
          bash ./build-base.sh
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"          

      - name: Push base images
        run: >
          bash ./push-base.sh
          --release-version "$RELEASE_VERSION"
          --php-versions "$PHP_VERSIONS"          

Der Operator tippt die Versionsnummer ein. Keine Tags zu erstellen, keine Branches zu mergen, keine Release-Skripte, die auf jemandes Laptop liegen.

if: github.ref == 'refs/heads/main' stellt sicher, dass Base-Releases nur vom Main-Branch kommen. Wenn jemand den Workflow von einem Feature-Branch auslöst — absichtlich oder versehentlich — wird der Job sofort übersprungen.

Der komplette Pipeline-Ablauf

Folgendes passiert, wenn ein Entwickler einen PR öffnet:

  1. checkout — klont das Repo mit LFS
  2. build — führt build.sh mit --hash <commit-sha> für alle PHP-Versionen und Architekturen aus
  3. push — führt push.sh mit den gleichen Flags aus, pusht Preview-Images

Und wenn der PR in main gemergt wird:

1 ist gleich. 2. build — führt build.sh mit --with-dev für Produktions- und Dev-Images aus 3. push — pusht sauber release-getaggte Images

Für Base-Image-Releases klickt der Operator auf „Run workflow", gibt eine Version ein, und die Pipeline übernimmt Build und Push.

Alles zusammen

Über drei Posts hinweg haben wir gebaut:

  • Ein konfigurierbares Base-Image mit PHP-Extensions als Build-Args, OPcache-Tuning über Umgebungsvariablen, FPM auf Unix-Sockets und strukturiertem Log-Routing durch Named Pipes
  • Ein Variation-Layer-System mit Custom-Extension-Installern, Multi-Architektur-Modulinstallation, bedingten Dev-Builds und einem einheitlichen Build-Skript mit CLI-Flags
  • Zwei CI/CD-Pipelines — automatische Variation-Builds bei Push/PR mit Commit-SHA-Tagging und manuelle Base-Releases über workflow_dispatch, beide mit Multi-Arch-Support und Concurrency-Kontrolle

Die wichtigste Erkenntnis: Die Shell-Skripte machen die ganze echte Arbeit. Die GitHub-Actions-Workflows sind nur Trigger und Orchestrierung. Das bedeutet, du kannst bash build.sh --release-version 1.0.2 --with-dev auf deinem Laptop ausführen und genau das gleiche Ergebnis wie CI bekommen. Kein Vendor Lock-in. Kein „es funktioniert in der Pipeline, aber nicht lokal."

Weiter voran und genieße jeden Schritt deiner Coding-Reise.