From 1e1eb0c06f04fb630453aa5cd672a3995116521a Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 08:59:58 +0200 Subject: [PATCH 1/7] feat: add v2026.1 image variant (ADR 2026-07-08) Implements the first iteration of the Docker Image Variant v2 ADR alongside the existing v1 images: - Debian-based FrankenPHP production image, base pinned by digest, extension installer and all PECL extensions pinned to exact versions - grpc and opentelemetry shipped but not loaded; enabled at runtime via PHP_EXTENSION_GRPC=1 / PHP_EXTENSION_OPENTELEMETRY=1 (PHP_INI_SCAN_DIR, works on read-only rootfs) - No Shopware application env baked into the image; only PHP_*/COMPOSER_* infrastructure defaults remain - Lifecycle dates baked as env + OCI labels with escalating startup warnings (warn-only), best-effort ONBUILD build-time check - HEALTHCHECK via Caddy admin endpoint, SBOM + provenance attestations - Dev image on top of FrankenPHP (Node 22/24, no supervisord), profilers shipped disabled and enabled via PHP_PROFILER - Per-version layout: v2026.1/ holds contexts and its own docker-bake.hcl; build workflow scoped to that directory - update-php-matrix.mjs refreshes digest pins in all v*/docker-bake.hcl Co-Authored-By: Claude Fable 5 --- .github/action/build-bake-publish/action.yml | 5 + .github/workflows/build-v2026.1.yml | 190 ++++++++++++++++++ .github/workflows/security.yml | 2 + CHANGELOG.md | 9 + README.md | 31 +++ update-php-matrix.mjs | 77 +++++++ v2026.1/dev/Dockerfile | 74 +++++++ v2026.1/dev/config.yaml | 100 +++++++++ v2026.1/dev/rootfs/entrypoint-dev | 27 +++ v2026.1/dev/rootfs/etc/bash.bashrc | 79 ++++++++ .../rootfs/usr/local/bin/new-shopware-setup | 33 +++ v2026.1/docker-bake.hcl | 148 ++++++++++++++ v2026.1/frankenphp/Dockerfile | 95 +++++++++ v2026.1/frankenphp/rootfs/entrypoint | 28 +++ v2026.1/frankenphp/rootfs/etc/caddy/Caddyfile | 40 ++++ v2026.1/frankenphp/rootfs/setup | 20 ++ .../usr/local/bin/shopware-image-lifecycle | 66 ++++++ .../usr/local/etc/php/conf.d/docker.ini | 32 +++ 18 files changed, 1056 insertions(+) create mode 100644 .github/workflows/build-v2026.1.yml create mode 100644 v2026.1/dev/Dockerfile create mode 100644 v2026.1/dev/config.yaml create mode 100755 v2026.1/dev/rootfs/entrypoint-dev create mode 100644 v2026.1/dev/rootfs/etc/bash.bashrc create mode 100755 v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup create mode 100644 v2026.1/docker-bake.hcl create mode 100644 v2026.1/frankenphp/Dockerfile create mode 100755 v2026.1/frankenphp/rootfs/entrypoint create mode 100644 v2026.1/frankenphp/rootfs/etc/caddy/Caddyfile create mode 100755 v2026.1/frankenphp/rootfs/setup create mode 100755 v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle create mode 100644 v2026.1/frankenphp/rootfs/usr/local/etc/php/conf.d/docker.ini diff --git a/.github/action/build-bake-publish/action.yml b/.github/action/build-bake-publish/action.yml index c619e89..14056be 100644 --- a/.github/action/build-bake-publish/action.yml +++ b/.github/action/build-bake-publish/action.yml @@ -5,6 +5,10 @@ inputs: description: 'Targets' required: true default: '' + files: + description: 'Bake definition file' + required: false + default: 'docker-bake.hcl' docker_hub_username: description: 'Docker Hub username' required: true @@ -35,6 +39,7 @@ runs: uses: docker/bake-action@v6 with: push: true + files: ${{ inputs.files }} targets: ${{ inputs.targets }} no-cache: true env: diff --git a/.github/workflows/build-v2026.1.yml b/.github/workflows/build-v2026.1.yml new file mode 100644 index 0000000..849c848 --- /dev/null +++ b/.github/workflows/build-v2026.1.yml @@ -0,0 +1,190 @@ +name: Build Images v2026.1 +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + schedule: + - cron: '30 4 * * *' + +concurrency: + group: "v2026.1-${{ github.ref }}" + cancel-in-progress: true + +env: + DOCKER_BUILDKIT: 1 + # bake variables for the OCI labels + gitSha: ${{ github.sha }} + +permissions: + contents: write + id-token: write + packages: write + pull-requests: write + +jobs: + frankenphp: + name: FrankenPHP v2026.1 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get all changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 + with: + files: | + v2026.1/docker-bake.hcl + v2026.1/frankenphp/** + + - name: Set build date + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: echo "buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + + - name: Build + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: ./.github/action/build-bake-publish + with: + files: v2026.1/docker-bake.hcl + targets: frankenphp + docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + frankenphp-check: + name: Check FrankenPHP v2026.1 + runs-on: ubuntu-latest + needs: [frankenphp] + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get all changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 + with: + files: | + v2026.1/docker-bake.hcl + v2026.1/frankenphp/** + + - name: Determine image tag + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + id: image-tag + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "IMAGE_TAG=ghcr.io/shopware/docker-base-ci-test:${{ github.event.number }}-8.4-frankenphp-v2026.1" >> $GITHUB_OUTPUT + else + echo "IMAGE_TAG=ghcr.io/shopware/docker-base:8.4-frankenphp-v2026.1" >> $GITHUB_OUTPUT + fi + + - name: Pull image + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }} + + - name: grpc and opentelemetry are shipped but not loaded + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} + MODULES=$(docker run --rm $IMAGE php -m) + if echo "$MODULES" | grep -qi '^grpc$'; then echo "grpc must not be loaded by default"; exit 1; fi + if echo "$MODULES" | grep -qi '^opentelemetry$'; then echo "opentelemetry must not be loaded by default"; exit 1; fi + echo "$MODULES" | grep -qi '^redis$' || { echo "redis missing"; exit 1; } + echo "$MODULES" | grep -qi '^amqp$' || { echo "amqp missing"; exit 1; } + + - name: grpc and opentelemetry can be enabled via env + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} + MODULES=$(docker run --rm -e PHP_EXTENSION_GRPC=1 -e PHP_EXTENSION_OPENTELEMETRY=1 $IMAGE php -m) + echo "$MODULES" | grep -qi '^grpc$' || { echo "grpc not loaded with PHP_EXTENSION_GRPC=1"; exit 1; } + echo "$MODULES" | grep -qi '^opentelemetry$' || { echo "opentelemetry not loaded with PHP_EXTENSION_OPENTELEMETRY=1"; exit 1; } + + - name: No Shopware application env baked into the image + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} + ENVS=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $IMAGE) + for var in APP_ENV LOCK_DSN MAILER_DSN SHOPWARE_HTTP_CACHE_ENABLED INSTALL_LOCALE; do + if echo "$ENVS" | grep -q "^$var="; then echo "$var must not be baked into the v2 image"; exit 1; fi + done + echo "$ENVS" | grep -q "^PHP_MEMORY_LIMIT=" || { echo "PHP_MEMORY_LIMIT missing"; exit 1; } + + - name: www-data runs with uid/gid 82 (v1 parity) + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} + UID_GID=$(docker run --rm $IMAGE sh -c 'echo "$(id -u):$(id -g)"') + [ "$UID_GID" = "82:82" ] || { echo "expected uid:gid 82:82, got $UID_GID"; exit 1; } + + dev: + name: Dev v2026.1 + runs-on: ubuntu-latest + needs: [frankenphp] + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get all changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 + with: + files: | + v2026.1/** + + - name: Set build date + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: echo "buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + + - name: Build + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: ./.github/action/build-bake-publish + with: + files: v2026.1/docker-bake.hcl + targets: dev + docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + dev-check: + name: Check Dev v2026.1 Image + runs-on: ubuntu-latest + needs: [dev] + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get all changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 + with: + files: | + v2026.1/** + + - name: Install container-structure-test + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0 + with: + repo: GoogleContainerTools/container-structure-test + extension-matching: disable + rename-to: container-structure-test + chmod: 0755 + + - name: Determine image tag + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + id: image-tag + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "IMAGE_TAG=ghcr.io/shopware/docker-dev-ci-test:${{ github.event.number }}-8.4-node24-v2026.1" >> $GITHUB_OUTPUT + else + echo "IMAGE_TAG=ghcr.io/shopware/docker-dev:8.4-node24-v2026.1" >> $GITHUB_OUTPUT + fi + + - name: Pull image + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }} + + - name: Test Dev Image + if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' + run: container-structure-test test --config v2026.1/dev/config.yaml --image ${{ steps.image-tag.outputs.IMAGE_TAG }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d90c889..97b1dc1 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -37,6 +37,8 @@ jobs: dockerfile: frankenphp/Dockerfile - name: frankenphp-otel dockerfile: frankenphp-otel/Dockerfile + - name: frankenphp-v2026.1 + dockerfile: v2026.1/frankenphp/Dockerfile steps: - name: Checkout uses: actions/checkout@v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index c6fe4b8..f42bde5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2026-08-24 + +Added the first iteration of the v2 images (see `adr/2026-07-08-docker-image-variant-v2.md`), published alongside the existing images: + +- `ghcr.io/shopware/docker-base:-frankenphp-v2026.1` — Debian-based FrankenPHP image with gRPC and OpenTelemetry installed but disabled by default (enable via `PHP_EXTENSION_GRPC=1` / `PHP_EXTENSION_OPENTELEMETRY=1`) +- `ghcr.io/shopware/docker-dev:-node<22|24>-v2026.1` — dev image built on top of the v2 FrankenPHP image + +v2 images no longer bake Shopware application environment variables (`APP_ENV`, `LOCK_DSN`, `MAILER_DSN`, `SHOPWARE_*`, `INSTALL_*`, …) into the image; only infrastructure defaults (`PHP_*`, `COMPOSER_*`) remain. The base image is pinned by digest and all PECL extensions are pinned to exact versions. + ## 2024-08-13 Added zstd php extension to Docker image diff --git a/README.md b/README.md index 4d85b86..fd198ea 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,34 @@ This repository contains a base image with Alpine + PHP + (Caddy or Nginx), which you can use to build your docker image with your code. [Documentation can be found here](https://developer.shopware.com/docs/guides/hosting/installation-updates/docker.html) + +## v2 images (preview) + +The v2 images implement [ADR: Docker Image Variant v2](adr/2026-07-08-docker-image-variant-v2.md) and are published **alongside** the existing (v1) images. In short: + +- **Calendar-versioned tags** with a frozen contract: `ghcr.io/shopware/docker-base:8.3-frankenphp-v2026.1`. A versioned tag keeps being rebuilt for security patches, but its contract (base OS, extension set, env defaults, entrypoint behavior) never changes. Breaking changes only ship in a new calendar version. +- **Debian instead of Alpine** (glibc, matching the `dunglas/frankenphp` base). +- **One production variant**: FrankenPHP. gRPC and OpenTelemetry are installed but **not loaded by default** — enable them with `PHP_EXTENSION_GRPC=1` / `PHP_EXTENSION_OPENTELEMETRY=1`. +- **No Shopware application env baked into the image**: `APP_ENV`, `LOCK_DSN`, `MAILER_DSN`, `SHOPWARE_*`, `INSTALL_*`, … are no longer set as image `ENV`, so your container env and `.env` files stay the single source of truth. Only infrastructure defaults (`PHP_*`, `COMPOSER_*`) remain. +- **Everything pinned**: base image by digest, PECL extensions by exact version, the extension installer by release — updated through reviewed PRs, not silently at build time. +- **Dev images** are built on top of the FrankenPHP variant: `ghcr.io/shopware/docker-dev:8.3-node22-v2026.1`. Profilers (xdebug, tideways, blackfire, spx) are shipped disabled and enabled with `PHP_PROFILER=`. + +### v2 tags + +| Image | Tags | +|---|---| +| Base | `ghcr.io/shopware/docker-base:-frankenphp-v2026.1`, also on Docker Hub as `shopware/docker-base` | +| Dev | `ghcr.io/shopware/docker-dev:-node<22\|24>-v2026.1` | + +`` is either a minor (`8.3`) or a full patch version (`8.3.33`). + +### Support windows + +| Version | Release | Rolling tag flips | Security-only | EOL | +|---|---|---|---|---| +| v1 (legacy, unversioned) | — | not scheduled | not scheduled | not scheduled | +| v2026.1 | preview | — | not scheduled | not scheduled | + +PHP versions that reach their [upstream end of life](https://www.php.net/supported-versions.php) are dropped from all calendar versions on day one. + +Every v2 image carries its lifecycle dates as OCI labels (`com.shopware.image.version`, `com.shopware.image.security-only`, `com.shopware.image.eol`) and warns at container start once a date has passed. Set `SHOPWARE_DOCKER_SUPPRESS_EOL_WARNING=1` to silence the warning. diff --git a/update-php-matrix.mjs b/update-php-matrix.mjs index cd4da0b..2505619 100755 --- a/update-php-matrix.mjs +++ b/update-php-matrix.mjs @@ -75,6 +75,23 @@ function getLatestFrankenphpPatchVersion(apiResponse) { return null; } +// Function to fetch the manifest-list digest of a specific FrankenPHP tag (used for the v2 digest pins) +async function fetchFrankenPhpTagDigest(fullVersion) { + const url = `https://hub.docker.com/v2/repositories/dunglas/frankenphp/tags/php${fullVersion}`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch FrankenPHP tag php${fullVersion}: ${response.status} ${response.statusText}`); + } + + const jsonData = await response.json(); + if (!jsonData.digest) { + throw new Error(`No digest found for FrankenPHP tag php${fullVersion}`); + } + + return jsonData.digest; +} + // Function to update the phpMatrix and frankenphpMatrix in docker-bake.hcl async function updatePhpMatrixInHcl(phpVersions, frankenphpVersions) { const hclPath = 'docker-bake.hcl'; @@ -106,6 +123,48 @@ async function updatePhpMatrixInHcl(phpVersions, frankenphpVersions) { console.log('FrankenPHP versions:', frankenphpVersions); } +// Function to find the bake files of calendar-versioned images (v2026.1/docker-bake.hcl, ...) +async function findVersionedHclFiles() { + const dirents = await fs.readdir('.', { withFileTypes: true }); + const files = []; + + for (const dirent of dirents) { + if (!dirent.isDirectory() || !/^v\d{4}\.\d+$/.test(dirent.name)) { + continue; + } + + const hclPath = `${dirent.name}/docker-bake.hcl`; + try { + await fs.access(hclPath); + files.push(hclPath); + } catch { + // version directory without a bake file — skip + } + } + + return files.sort(); +} + +// Function to update the digest-pinned frankenphpDigestMatrix in a versioned bake file +async function updateDigestMatrixInHcl(hclPath, entries) { + let hclContent = await fs.readFile(hclPath, 'utf8'); + + const digestMatrixRegex = /(variable "frankenphpDigestMatrix" \{[\s\S]*?default = )(\[[^\]]*\])(\s*\})/; + + if (!digestMatrixRegex.test(hclContent)) { + throw new Error(`frankenphpDigestMatrix variable not found in ${hclPath}`); + } + + const newMatrix = '[\n' + entries + .map(e => ` { php = "${e.php}", digest = "${e.digest}" }`) + .join(',\n') + '\n ]'; + + hclContent = hclContent.replace(digestMatrixRegex, `$1${newMatrix}$3`); + + await fs.writeFile(hclPath, hclContent); + console.log(`Successfully updated frankenphpDigestMatrix in ${hclPath}`); +} + const phpVersions = []; const frankenphpVersions = []; @@ -134,3 +193,21 @@ for (const version of supportedVersions) { } await updatePhpMatrixInHcl(phpVersions, frankenphpVersions); + +// Refresh the digest pins for the calendar-versioned images +const versionedHclFiles = await findVersionedHclFiles(); + +if (versionedHclFiles.length > 0) { + const digestEntries = []; + + for (const version of frankenphpVersions) { + console.log(`Fetching digest for FrankenPHP tag php${version}...`); + const digest = await fetchFrankenPhpTagDigest(version); + digestEntries.push({ php: version, digest }); + console.log(`Found digest for php${version}: ${digest}`); + } + + for (const hclPath of versionedHclFiles) { + await updateDigestMatrixInHcl(hclPath, digestEntries); + } +} diff --git a/v2026.1/dev/Dockerfile b/v2026.1/dev/Dockerfile new file mode 100644 index 0000000..317d684 --- /dev/null +++ b/v2026.1/dev/Dockerfile @@ -0,0 +1,74 @@ +#syntax=docker/dockerfile:1.7 +#check=skip=SecretsUsedInArgOrEnv + +# v2 dev image — built on top of the v2 FrankenPHP image +# (see adr/2026-07-08-docker-image-variant-v2.md) +ARG NODE_VERSION=22 + +FROM node:${NODE_VERSION}-bookworm-slim AS node + +FROM base + +ARG NODE_VERSION + +# Versions are injected from docker-bake.hcl (single source of truth). +# tideways/blackfire/spx are distributed without PECL version pins. +ARG PHP_DEV_PINNED_EXTENSIONS="xdebug-3.5.3" +ARG PHP_DEV_EXTENSIONS="tideways blackfire spx" + +USER root + +COPY --link --from=node /usr/local/bin/node /usr/local/bin/node +COPY --link --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules + +RUN < + # by adding their ini directory to PHP_INI_SCAN_DIR (see entrypoint-dev) + for ext in xdebug tideways blackfire spx; do + mkdir -p /usr/local/etc/php/conf.available/$ext + mv /usr/local/etc/php/conf.d/docker-php-ext-$ext.ini /usr/local/etc/php/conf.available/$ext/ + done + + usermod -u 1000 www-data + groupmod -g 1000 www-data + mkdir -p /home/www-data + usermod -d /home/www-data www-data + chown -R 1000:1000 /var/www/html /home/www-data /data/caddy /config/caddy + + rm -rf /var/cache/apt/archives /var/lib/apt/lists/* +EOF + +USER www-data + +COPY --link --from=ghcr.io/shopware/shopware-cli:bin /shopware-cli /usr/local/bin/shopware-cli +COPY --link --from=composer/composer:2-bin /composer /usr/local/bin/composer +COPY --link --from=blackfire/blackfire /usr/local/bin/blackfire /usr/local/bin/blackfire +COPY --link --from=ghcr.io/shopware/shopware-cli/env-bridge /env-bridge /usr/local/bin/env-bridge +COPY --link rootfs / + +ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 \ + PHP_OPCACHE_FILE_OVERRIDE=0 + +ENTRYPOINT ["/entrypoint-dev"] + +CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/v2026.1/dev/config.yaml b/v2026.1/dev/config.yaml new file mode 100644 index 0000000..ab0595a --- /dev/null +++ b/v2026.1/dev/config.yaml @@ -0,0 +1,100 @@ +schemaVersion: 2.0.0 +commandTests: + - name: "PHP installed" + command: "php" + args: ["-v"] + expectedOutput: + - 'PHP 8.4.*' + - name: "PHP extensions there" + command: "php" + args: ["-m"] + expectedOutput: + - 'OPcache' + - 'redis' + - 'amqp' + - 'apcu' + - 'zstd' + - 'xsl' + excludedOutput: + - 'grpc' + - 'opentelemetry' + - 'xdebug' + - 'tideways' + - 'blackfire' + - 'spx' + - name: "Optional extensions shipped but not loaded" + command: "sh" + args: ["-c", "ls /usr/local/etc/php/conf.available/*/"] + expectedOutput: + - 'docker-php-ext-grpc.ini' + - 'docker-php-ext-opentelemetry.ini' + - 'docker-php-ext-xdebug.ini' + - 'docker-php-ext-tideways.ini' + - 'docker-php-ext-blackfire.ini' + - 'docker-php-ext-spx.ini' + - name: "Node installed" + command: 'node' + args: ["-v"] + expectedOutput: + - '^v24' + - name: "NPM installed" + command: 'npm' + args: ["-v"] + - name: 'Git installed' + command: 'git' + args: ["-v"] + - name: 'Bash installed' + command: 'bash' + args: ["--version"] + expectedOutput: + - 'GNU bash' + - name: 'Composer installed' + command: 'composer' + args: ["--version"] + expectedOutput: + - 'Composer version 2' + - name: 'Shopware CLI installed' + command: 'shopware-cli' + args: ["--version"] + - name: 'Patch utility installed' + command: 'patch' + args: ["--version"] + - name: 'User ID is 1000' + command: 'id' + args: ["-u"] + expectedOutput: + - '1000' + - name: 'Debian based' + command: 'sh' + args: ["-c", "grep '^ID=' /etc/os-release"] + expectedOutput: + - 'debian' +fileExistenceTests: + - name: 'Entrypoint script exists' + path: '/entrypoint-dev' + shouldExist: true + permissions: '-rwxr-xr-x' + - name: 'Base entrypoint exists' + path: '/entrypoint' + shouldExist: true + permissions: '-rwxr-xr-x' + - name: 'Lifecycle script exists' + path: '/usr/local/bin/shopware-image-lifecycle' + shouldExist: true + permissions: '-rwxr-xr-x' + - name: 'PHP config directory exists' + path: '/usr/local/etc/php/conf.d' + shouldExist: true + - name: 'Working directory exists' + path: '/var/www/html' + shouldExist: true +metadataTest: + envVars: + - key: PHP_OPCACHE_VALIDATE_TIMESTAMPS + value: '1' + - key: PHP_OPCACHE_FILE_OVERRIDE + value: '0' + entrypoint: ["/entrypoint-dev"] + cmd: ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] + user: 'www-data' + workdir: '/var/www/html' diff --git a/v2026.1/dev/rootfs/entrypoint-dev b/v2026.1/dev/rootfs/entrypoint-dev new file mode 100755 index 0000000..30d1a7e --- /dev/null +++ b/v2026.1/dev/rootfs/entrypoint-dev @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +set -e + +# Profilers (xdebug, tideways, blackfire, spx) ship installed but not loaded. +# PHP_PROFILER= enables one by appending its ini directory to +# PHP_INI_SCAN_DIR — no filesystem writes needed. +if [ -n "${PHP_PROFILER:-}" ]; then + if [ -d "/usr/local/etc/php/conf.available/$PHP_PROFILER" ]; then + if [ -z "${PHP_INI_SCAN_DIR:-}" ]; then + # a leading colon keeps the compiled-in conf.d in the scan path + export PHP_INI_SCAN_DIR=":/usr/local/etc/php/conf.available/$PHP_PROFILER" + else + export PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR}:/usr/local/etc/php/conf.available/$PHP_PROFILER" + fi + else + echo "WARNING: unknown PHP_PROFILER '$PHP_PROFILER' — expected one of: xdebug, tideways, blackfire, spx" >&2 + fi +fi + +# Boot fallback only: a fresh dev container without a project yet needs an +# APP_SECRET to serve anything. Never shadows container env or an .env file. +if [ -z "${APP_SECRET:-}" ] && [ ! -f /var/www/html/.env ] && [ ! -f /var/www/html/.env.local ]; then + export APP_SECRET=def00000bb5acb32b54ff8ee130270586eec0e878f7337dc7a837acc31d3ff00f93a56b595448b4b29664847dd51991b3314ff65aeeeb761a133b0ec0e070433bff08e48 +fi + +exec /entrypoint "$@" diff --git a/v2026.1/dev/rootfs/etc/bash.bashrc b/v2026.1/dev/rootfs/etc/bash.bashrc new file mode 100644 index 0000000..90fee75 --- /dev/null +++ b/v2026.1/dev/rootfs/etc/bash.bashrc @@ -0,0 +1,79 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# don't put duplicate lines or lines starting with space in the history. +# See bash(1) for more options +HISTCONTROL=ignoreboth + +# append to the history file, don't overwrite it +shopt -s histappend + +# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) +HISTFILESIZE=99999999 +HISTSIZE=99999999 + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# If set, the pattern "**" used in a pathname expansion context will +# match all files and zero or more directories and subdirectories. +#shopt -s globstar + +# make less more friendly for non-text input files, see lesspipe(1) +#[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +PS1='${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u\[\033[01;33m\]@\[\033[01;36m\]docker \[\033[01;33m\]\w \[\033[01;35m\]\$ \[\033[00m\]' + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" + alias ls='ls --color=auto' + #alias dir='dir --color=auto' + #alias vdir='vdir --color=auto' + + #alias grep='grep --color=auto' + #alias fgrep='fgrep --color=auto' + #alias egrep='egrep --color=auto' +fi + +# colored GCC warnings and errors +#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' + +# some more ls aliases +alias ll='ls -l' +alias la='ls -A' +#alias l='ls -CF' + +# Alias definitions. +# You may want to put all your additions into a separate file like +# ~/.bash_aliases, instead of adding them here directly. +# See /usr/share/doc/bash-doc/examples in the bash-doc package. + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + +for f in /etc/bash/*.sh; do + [ -r "$f" ] && . "$f" +done +unset f + diff --git a/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup b/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup new file mode 100755 index 0000000..89ac440 --- /dev/null +++ b/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup @@ -0,0 +1,33 @@ +#/usr/bin/env bash + +set -e + +if [ "$(ls -A /var/www/html)" ]; then + echo "Error: /var/www/html is not empty." >&2 + exit 1 +fi + +cd /var/www/html + +SHOPWARE_VERSION=${1:-*} + +echo "Creating Shopware project..." +(composer create-project shopware/production:$SHOPWARE_VERSION . --no-interaction --prefer-dist --no-progress > /tmp/install.log 2>&1) || { echo "Installation failed. Log:"; cat /tmp/install.log; exit 1; } + +echo "Adding developer tools..." +(composer require shopware/docker-dev --no-interaction > /tmp/install.log 2>&1) || { echo "Adding docker-dev failed. Log:"; cat /tmp/install.log; exit 1; } + +if [ -f compose.yaml ]; then + PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + sed -i "s/php[0-9]\.[0-9]/php$PHP_VERSION/g" compose.yaml +fi + +echo -n "Do you want to use Elasticsearch? (y/N): " +read use_elasticsearch + +if [ -z "$use_elasticsearch" ] || [ "$use_elasticsearch" != "y" ] && [ "$use_elasticsearch" != "Y" ]; then + echo "Removing Elasticsearch..." + (composer remove shopware/elasticsearch --no-interaction --no-scripts > /tmp/install.log 2>&1 && rm -fr var/cache/*) || { echo "Removing elasticsearch failed. Log:"; cat /tmp/install.log; exit 1; } +fi + +echo "Project created successfully. Run 'make up' to start the containers and 'make setup' to initialize the project." diff --git a/v2026.1/docker-bake.hcl b/v2026.1/docker-bake.hcl new file mode 100644 index 0000000..10fe619 --- /dev/null +++ b/v2026.1/docker-bake.hcl @@ -0,0 +1,148 @@ +# v2026.1 images — built alongside v1 (see adr/2026-07-08-docker-image-variant-v2.md) +# +# Build from the repository root: docker buildx bake -f v2026.1/docker-bake.hcl +# NOTE: bake resolves relative context paths against the working directory, +# not this file — paths below are therefore relative to the repository root. + +variable "imageSuffix" { + default = "" +} + +variable "tagPrefix" { + default = "" +} + +# Calendar version of this contract (ADR section 1) — matches the directory name +variable "imageVersion" { + default = "v2026.1" +} + +# Lifecycle dates baked into every image (ADR section 7). +# Empty = not scheduled yet. Format: YYYY-MM-DD. +variable "securityOnlyDate" { + default = "" +} + +variable "eolDate" { + default = "" +} + +# Set by CI for OCI labels +variable "gitSha" { + default = "" +} + +variable "buildDate" { + default = "" +} + +# Updated by update-php-matrix.mjs. The digest pins the multi-arch manifest +# list of dunglas/frankenphp:php so rebuilds are reproducible (ADR section 6). +variable "frankenphpDigestMatrix" { + default = [ + { php = "8.2.33", digest = "sha256:ab7284dddea6f9430986918b270d09adf751e8edb2053ab2cac50240a39959bb" }, + { php = "8.3.33", digest = "sha256:b603d870b1b741bac8e509fa865a8e0901d760805a972de640a97b0d1f6fed69" }, + { php = "8.4.24", digest = "sha256:96560b9b3ec5be4f4784ebde460d4c176d7d05d801091b871bad56636bd822ab" }, + { php = "8.5.9", digest = "sha256:e2fb833fac0135f9a070647a8c70eb80ba282de4a035de56b6a3371cb555eca0" } + ] +} + +# Single source of truth for the PHP extension set (ADR section 5). +# Core extensions are versioned implicitly by the pinned PHP base image. +variable "installPhpExtensionsVersion" { + default = "2.11.12" +} + +variable "coreExtensions" { + default = "bcmath gd intl mysqli pdo_mysql pcntl sockets bz2 gmp soap zip ftp ffi opcache xsl" +} + +variable "pinnedExtensions" { + default = "redis-6.3.0 apcu-5.1.28 amqp-2.2.0 zstd-0.18.0" +} + +# Installed but not loaded by default (ADR section 3); +# enabled via PHP_EXTENSION_GRPC=1 / PHP_EXTENSION_OPENTELEMETRY=1 +variable "optionalExtensions" { + default = "grpc-1.83.0 opentelemetry-1.2.1" +} + +variable "devPinnedExtensions" { + default = "xdebug-3.5.3" +} + +target "frankenphp" { + name = "frankenphp-${replace(substr(item.php, 0, 3), ".", "-")}" + context = "./v2026.1/frankenphp" + matrix = { + "item" = frankenphpDigestMatrix + } + args = { + "PHP_BASE_IMAGE" = "dunglas/frankenphp:php${item.php}@${item.digest}" + "INSTALL_PHP_EXTENSIONS_VERSION" = installPhpExtensionsVersion + "PHP_CORE_EXTENSIONS" = coreExtensions + "PHP_PINNED_EXTENSIONS" = pinnedExtensions + "PHP_OPTIONAL_EXTENSIONS" = optionalExtensions + "IMAGE_VERSION" = imageVersion + "IMAGE_SECURITY_ONLY_DATE" = securityOnlyDate + "IMAGE_EOL_DATE" = eolDate + } + labels = { + "org.opencontainers.image.source" = "https://github.com/shopware/docker" + "org.opencontainers.image.revision" = gitSha + "org.opencontainers.image.version" = imageVersion + "org.opencontainers.image.created" = buildDate + "com.shopware.image.version" = imageVersion + "com.shopware.image.security-only" = securityOnlyDate + "com.shopware.image.eol" = eolDate + } + attest = [ + "type=sbom", + "type=provenance,mode=max" + ] + platforms = [ "linux/amd64", "linux/arm64" ] + tags = imageSuffix != "" ? [ + "ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-frankenphp-${imageVersion}", + "ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${item.php}-frankenphp-${imageVersion}" + ] : [ + "shopware/docker-base${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-frankenphp-${imageVersion}", + "shopware/docker-base${imageSuffix}:${tagPrefix}${item.php}-frankenphp-${imageVersion}", + + "ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-frankenphp-${imageVersion}", + "ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${item.php}-frankenphp-${imageVersion}" + ] +} + +target "dev" { + name = "dev-${replace(substr(item.php, 0, 3), ".", "-")}-${node}" + context = "./v2026.1/dev" + matrix = { + "item" = frankenphpDigestMatrix + "node" = [ "22", "24" ] + } + contexts = { + base = "docker-image://ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${item.php}-frankenphp-${imageVersion}" + } + args = { + "NODE_VERSION" = node + "PHP_DEV_PINNED_EXTENSIONS" = devPinnedExtensions + } + labels = { + "org.opencontainers.image.source" = "https://github.com/shopware/docker" + "org.opencontainers.image.revision" = gitSha + "org.opencontainers.image.version" = imageVersion + "org.opencontainers.image.created" = buildDate + "com.shopware.image.version" = imageVersion + "com.shopware.image.security-only" = securityOnlyDate + "com.shopware.image.eol" = eolDate + } + attest = [ + "type=sbom", + "type=provenance,mode=max" + ] + platforms = [ "linux/amd64", "linux/arm64" ] + tags = [ + "ghcr.io/shopware/docker-dev${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-node${node}-${imageVersion}", + "ghcr.io/shopware/docker-dev${imageSuffix}:${tagPrefix}${item.php}-node${node}-${imageVersion}" + ] +} diff --git a/v2026.1/frankenphp/Dockerfile b/v2026.1/frankenphp/Dockerfile new file mode 100644 index 0000000..31d979b --- /dev/null +++ b/v2026.1/frankenphp/Dockerfile @@ -0,0 +1,95 @@ +#syntax=docker/dockerfile:1.7 +#check=skip=SecretsUsedInArgOrEnv + +# v2 image — see adr/2026-07-08-docker-image-variant-v2.md +# The base image is pinned by digest via docker-bake.hcl (single source of truth). +ARG PHP_BASE_IMAGE=dunglas/frankenphp:php8.4 + +FROM ${PHP_BASE_IMAGE} + +# All versions below are injected from docker-bake.hcl so production and dev +# images cannot drift (ADR section 5: single source of truth). +ARG INSTALL_PHP_EXTENSIONS_VERSION=2.11.12 +ARG PHP_CORE_EXTENSIONS="bcmath gd intl mysqli pdo_mysql pcntl sockets bz2 gmp soap zip ftp ffi opcache xsl" +ARG PHP_PINNED_EXTENSIONS="redis-6.3.0 apcu-5.1.28 amqp-2.2.0 zstd-0.18.0" +ARG PHP_OPTIONAL_EXTENSIONS="grpc-1.83.0 opentelemetry-1.2.1" + +# Pinned extension installer instead of the bundled/latest one (ADR section 5) +ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/download/${INSTALL_PHP_EXTENSIONS_VERSION}/install-php-extensions /usr/local/bin/install-php-extensions + +RUN </dev/null || exit 1 + +# Best-effort build-time EOL warning for downstream images (ADR section 7). +# Warn-only by contract: the script always exits 0. +ONBUILD RUN /usr/local/bin/shopware-image-lifecycle build + +ENTRYPOINT ["/entrypoint"] + +CMD ["--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/v2026.1/frankenphp/rootfs/entrypoint b/v2026.1/frankenphp/rootfs/entrypoint new file mode 100755 index 0000000..f2cb1e7 --- /dev/null +++ b/v2026.1/frankenphp/rootfs/entrypoint @@ -0,0 +1,28 @@ +#!/usr/bin/env sh + +set -e + +/usr/local/bin/shopware-image-lifecycle runtime + +# grpc and opentelemetry ship installed but not loaded. Enabling them appends +# their ini directory to PHP_INI_SCAN_DIR instead of writing to the filesystem, +# so this also works on a read-only root filesystem. +append_scan_dir() { + if [ -z "${PHP_INI_SCAN_DIR:-}" ]; then + # a leading colon keeps the compiled-in conf.d in the scan path + PHP_INI_SCAN_DIR=":$1" + else + PHP_INI_SCAN_DIR="${PHP_INI_SCAN_DIR}:$1" + fi + export PHP_INI_SCAN_DIR +} + +if [ "${PHP_EXTENSION_GRPC:-0}" = "1" ]; then + append_scan_dir /usr/local/etc/php/conf.available/grpc +fi + +if [ "${PHP_EXTENSION_OPENTELEMETRY:-0}" = "1" ]; then + append_scan_dir /usr/local/etc/php/conf.available/opentelemetry +fi + +exec docker-php-entrypoint "$@" diff --git a/v2026.1/frankenphp/rootfs/etc/caddy/Caddyfile b/v2026.1/frankenphp/rootfs/etc/caddy/Caddyfile new file mode 100644 index 0000000..2dd75ac --- /dev/null +++ b/v2026.1/frankenphp/rootfs/etc/caddy/Caddyfile @@ -0,0 +1,40 @@ +{ + skip_install_trust + + {$CADDY_GLOBAL_OPTIONS} + + frankenphp { + {$FRANKENPHP_CONFIG} + } +} + +{$CADDY_EXTRA_CONFIG} + +:8000 { + root * /var/www/html/public + encode zstd gzip + + log + + @theme path /theme/* + @phpRoute { + not path /theme/* /media/* /thumbnail/* /bundles/* + not file {path} + } + + rewrite @phpRoute index.php + + @frontController path index.php + php @frontController + + handle @theme { + @theme_file file + handle @theme_file { + file_server + } + respond 410 + } + file_server { + hide *.php + } +} diff --git a/v2026.1/frankenphp/rootfs/setup b/v2026.1/frankenphp/rootfs/setup new file mode 100755 index 0000000..d465b46 --- /dev/null +++ b/v2026.1/frankenphp/rootfs/setup @@ -0,0 +1,20 @@ +#!/usr/bin/env sh + +set -e + +cd /var/www/html + +if [ -x ./vendor/bin/shopware-deployment-helper ]; then + exec ./vendor/bin/shopware-deployment-helper run +fi + +echo "### ERROR ###" +echo "" +echo "### The setup scripts are removed ###" +echo "### Please install the Shopware Deployment Helper using composer require shopware/deployment-helper to continue ###" +echo "### For more information see https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/deployment-helper.html ###" +echo "" +echo "### ERROR ###" +echo "" + +exit 1 diff --git a/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle b/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle new file mode 100755 index 0000000..9dc1a32 --- /dev/null +++ b/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle @@ -0,0 +1,66 @@ +#!/usr/bin/env sh + +# Prints image lifecycle warnings based on the dates baked in at build time +# (see adr/2026-07-08-docker-image-variant-v2.md, section 7). +# +# Usage: shopware-image-lifecycle [runtime|build] +# +# Strictly warn-only by contract: this script ALWAYS exits 0 — an EOL base +# image never fails a downstream build and never refuses to start. + +mode="${1:-runtime}" + +if [ "${SHOPWARE_DOCKER_SUPPRESS_EOL_WARNING:-0}" = "1" ]; then + exit 0 +fi + +version="${SHOPWARE_IMAGE_VERSION:-}" +security_only="${SHOPWARE_IMAGE_SECURITY_ONLY:-}" +eol="${SHOPWARE_IMAGE_EOL:-}" + +now=$(date +%s) + +to_epoch() { + date -d "$1" +%s 2>/dev/null || echo "" +} + +warn() { + echo "$@" >&2 +} + +context="starting a container from" +if [ "$mode" = "build" ]; then + context="building on top of" +fi + +eol_epoch="" +if [ -n "$eol" ]; then + eol_epoch=$(to_epoch "$eol") +fi + +if [ -n "$eol_epoch" ] && [ "$now" -ge "$eol_epoch" ]; then + warn "###################################################################" + warn "# WARNING: This Shopware Docker image ($version) reached its" + warn "# END OF LIFE on $eol. It no longer receives security updates." + warn "# You are $context an unsupported image." + warn "# Migrate to a supported version: https://github.com/shopware/docker" + warn "# Set SHOPWARE_DOCKER_SUPPRESS_EOL_WARNING=1 to silence this warning." + warn "###################################################################" + exit 0 +fi + +if [ -n "$eol_epoch" ] && [ "$now" -ge "$((eol_epoch - 7776000))" ]; then + # within 90 days of EOL + warn "WARNING: This Shopware Docker image ($version) reaches end of life on $eol." + warn "WARNING: Plan your migration now: https://github.com/shopware/docker" + exit 0 +fi + +if [ -n "$security_only" ]; then + security_only_epoch=$(to_epoch "$security_only") + if [ -n "$security_only_epoch" ] && [ "$now" -ge "$security_only_epoch" ]; then + warn "NOTICE: Shopware Docker image $version is in its security-only support phase (EOL: ${eol:-unscheduled}). See https://github.com/shopware/docker" + fi +fi + +exit 0 diff --git a/v2026.1/frankenphp/rootfs/usr/local/etc/php/conf.d/docker.ini b/v2026.1/frankenphp/rootfs/usr/local/etc/php/conf.d/docker.ini new file mode 100644 index 0000000..790c3aa --- /dev/null +++ b/v2026.1/frankenphp/rootfs/usr/local/etc/php/conf.d/docker.ini @@ -0,0 +1,32 @@ +expose_php = Off +error_reporting = E_ALL & ~E_DEPRECATED +display_errors = ${PHP_DISPLAY_ERRORS} +display_startup_errors = ${PHP_DISPLAY_ERRORS} + +upload_max_filesize = ${PHP_MAX_UPLOAD_SIZE} +post_max_size = ${PHP_MAX_UPLOAD_SIZE} +max_execution_time = ${PHP_MAX_EXECUTION_TIME} +memory_limit = ${PHP_MEMORY_LIMIT} + +session.cookie_lifetime = ${PHP_SESSION_COOKIE_LIFETIME} +session.save_handler = ${PHP_SESSION_HANDLER} +session.save_path = ${PHP_SESSION_SAVE_PATH} +session.gc_probability = 0 +session.gc_maxlifetime = ${PHP_SESSION_GC_MAXLIFETIME} + +opcache.enable_cli = ${PHP_OPCACHE_ENABLE_CLI} +opcache.enable_file_override = ${PHP_OPCACHE_FILE_OVERRIDE} +opcache.validate_timestamps = ${PHP_OPCACHE_VALIDATE_TIMESTAMPS} +opcache.interned_strings_buffer = ${PHP_OPCACHE_INTERNED_STRINGS_BUFFER} +opcache.max_accelerated_files= ${PHP_OPCACHE_MAX_ACCELERATED_FILES} +opcache.memory_consumption = ${PHP_OPCACHE_MEMORY_CONSUMPTION} +opcache.file_cache = ${PHP_OPCACHE_FILE_CACHE} +opcache.file_cache_only = ${PHP_OPCACHE_FILE_CACHE_ONLY} + +zend.assertions = -1 +zend.detect_unicode = 0 + +ffi.enable = 1 + +realpath_cache_ttl = ${PHP_REALPATH_CACHE_TTL} +realpath_cache_size = ${PHP_REALPATH_CACHE_SIZE} From a0b706794d83d6e2ada8f5b9110440ee8dba146f Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:11:24 +0200 Subject: [PATCH 2/7] feat: build v2026.1 images with docker/github-builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the custom build-bake-publish composite action (Namespace-powered buildx) with Docker's official reusable bake workflow for the v2026.1 images: - native arm64 runners (ubuntu-24.04-arm) instead of QEMU/remote builders - signed SLSA provenance + SBOM attestations (keyless, GitHub OIDC) — covers the cosign item from the ADR - one reusable-workflow call per bake target: a prepare job derives the PHP/Node matrices from the bake definition via 'bake --print', so the bake file stays the single source of truth - tags move to meta-images/meta-tags inputs in CI (github-builder replaces bake-defined tags); the tags in the bake file now apply to local builds only - v1 workflows keep using the composite action unchanged Pinned to a main commit — docker/github-builder has no release tag yet. Co-Authored-By: Claude Fable 5 --- .github/action/build-bake-publish/action.yml | 5 - .github/workflows/build-v2026.1.yml | 210 ++++++++++++------- v2026.1/docker-bake.hcl | 11 +- 3 files changed, 135 insertions(+), 91 deletions(-) diff --git a/.github/action/build-bake-publish/action.yml b/.github/action/build-bake-publish/action.yml index 14056be..c619e89 100644 --- a/.github/action/build-bake-publish/action.yml +++ b/.github/action/build-bake-publish/action.yml @@ -5,10 +5,6 @@ inputs: description: 'Targets' required: true default: '' - files: - description: 'Bake definition file' - required: false - default: 'docker-bake.hcl' docker_hub_username: description: 'Docker Hub username' required: true @@ -39,7 +35,6 @@ runs: uses: docker/bake-action@v6 with: push: true - files: ${{ inputs.files }} targets: ${{ inputs.targets }} no-cache: true env: diff --git a/.github/workflows/build-v2026.1.yml b/.github/workflows/build-v2026.1.yml index 849c848..4e0840b 100644 --- a/.github/workflows/build-v2026.1.yml +++ b/.github/workflows/build-v2026.1.yml @@ -12,21 +12,22 @@ concurrency: group: "v2026.1-${{ github.ref }}" cancel-in-progress: true -env: - DOCKER_BUILDKIT: 1 - # bake variables for the OCI labels - gitSha: ${{ github.sha }} - permissions: - contents: write - id-token: write - packages: write - pull-requests: write + contents: read jobs: - frankenphp: - name: FrankenPHP v2026.1 + prepare: + name: Prepare runs-on: ubuntu-latest + outputs: + build: ${{ steps.decide.outputs.build }} + build-date: ${{ steps.decide.outputs.build_date }} + image-suffix: ${{ steps.decide.outputs.image_suffix }} + tag-prefix: ${{ steps.decide.outputs.tag_prefix }} + base-images: ${{ steps.decide.outputs.base_images }} + dev-images: ${{ steps.decide.outputs.dev_images }} + frankenphp-matrix: ${{ steps.matrices.outputs.frankenphp }} + dev-matrix: ${{ steps.matrices.outputs.dev }} steps: - name: Checkout uses: actions/checkout@v7 @@ -36,41 +37,99 @@ jobs: uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 with: files: | - v2026.1/docker-bake.hcl - v2026.1/frankenphp/** + v2026.1/** + .github/workflows/build-v2026.1.yml - - name: Set build date - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' - run: echo "buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + - name: Decide build parameters + id: decide + run: | + if [[ "${{ steps.changed-files.outputs.any_changed }}" == "true" || "${{ github.event_name }}" == "schedule" || ( "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ) ]]; then + echo "build=true" >> "$GITHUB_OUTPUT" + else + echo "build=false" >> "$GITHUB_OUTPUT" + fi - - name: Build - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: ./.github/action/build-bake-publish - with: - files: v2026.1/docker-bake.hcl - targets: frankenphp - docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} - docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} - github_token: ${{ secrets.GITHUB_TOKEN }} + echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "image_suffix=-ci-test" >> "$GITHUB_OUTPUT" + echo "tag_prefix=${{ github.event.number }}-" >> "$GITHUB_OUTPUT" + echo "base_images=ghcr.io/shopware/docker-base-ci-test" >> "$GITHUB_OUTPUT" + echo "dev_images=ghcr.io/shopware/docker-dev-ci-test" >> "$GITHUB_OUTPUT" + else + echo "image_suffix=" >> "$GITHUB_OUTPUT" + echo "tag_prefix=" >> "$GITHUB_OUTPUT" + { + echo "base_images<> "$GITHUB_OUTPUT" + echo "dev_images=ghcr.io/shopware/docker-dev" >> "$GITHUB_OUTPUT" + fi + + - name: Compute build matrices from the bake definition + id: matrices + run: | + docker buildx bake -f v2026.1/docker-bake.hcl --print frankenphp > /tmp/frankenphp.json 2>/dev/null + docker buildx bake -f v2026.1/docker-bake.hcl --print dev > /tmp/dev.json 2>/dev/null + { + echo "frankenphp=$(jq -c '[.target | to_entries[] | {target: .key, php: (.value.args.PHP_BASE_IMAGE | capture("php(?[0-9]+[.][0-9]+[.][0-9]+)").v)} | . + {minor: (.php | split(".")[0:2] | join("."))}]' /tmp/frankenphp.json)" + echo "dev=$(jq -c '[.target | to_entries[] | {target: .key, node: .value.args.NODE_VERSION, php: (.value.contexts.base | capture(":(?[0-9]+[.][0-9]+[.][0-9]+)-frankenphp").v)} | . + {minor: (.php | split(".")[0:2] | join("."))}]' /tmp/dev.json)" + } >> "$GITHUB_OUTPUT" + + frankenphp: + name: FrankenPHP ${{ matrix.minor }} + needs: [prepare] + if: needs.prepare.outputs.build == 'true' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.prepare.outputs.frankenphp-matrix) }} + permissions: + contents: read + id-token: write + packages: write + # no release tag published yet — pinned to main + uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a + with: + files: v2026.1/docker-bake.hcl + target: ${{ matrix.target }} + output: image + push: true + sbom: true + runner: | + default=ubuntu-24.04 + linux/arm64=ubuntu-24.04-arm + vars: | + imageSuffix=${{ needs.prepare.outputs.image-suffix }} + tagPrefix=${{ needs.prepare.outputs.tag-prefix }} + gitSha=${{ github.sha }} + buildDate=${{ needs.prepare.outputs.build-date }} + meta-images: ${{ needs.prepare.outputs.base-images }} + meta-flavor: | + latest=false + meta-tags: | + type=raw,value=${{ needs.prepare.outputs.tag-prefix }}${{ matrix.minor }}-frankenphp-v2026.1 + type=raw,value=${{ needs.prepare.outputs.tag-prefix }}${{ matrix.php }}-frankenphp-v2026.1 + secrets: + registry-auths: | + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} frankenphp-check: name: Check FrankenPHP v2026.1 runs-on: ubuntu-latest - needs: [frankenphp] + needs: [prepare, frankenphp] steps: - name: Checkout uses: actions/checkout@v7 - - name: Get all changed files - id: changed-files - uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 - with: - files: | - v2026.1/docker-bake.hcl - v2026.1/frankenphp/** - - name: Determine image tag - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' id: image-tag run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then @@ -80,11 +139,9 @@ jobs: fi - name: Pull image - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }} - name: grpc and opentelemetry are shipped but not loaded - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: | IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} MODULES=$(docker run --rm $IMAGE php -m) @@ -94,7 +151,6 @@ jobs: echo "$MODULES" | grep -qi '^amqp$' || { echo "amqp missing"; exit 1; } - name: grpc and opentelemetry can be enabled via env - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: | IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} MODULES=$(docker run --rm -e PHP_EXTENSION_GRPC=1 -e PHP_EXTENSION_OPENTELEMETRY=1 $IMAGE php -m) @@ -102,7 +158,6 @@ jobs: echo "$MODULES" | grep -qi '^opentelemetry$' || { echo "opentelemetry not loaded with PHP_EXTENSION_OPENTELEMETRY=1"; exit 1; } - name: No Shopware application env baked into the image - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: | IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} ENVS=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $IMAGE) @@ -112,40 +167,50 @@ jobs: echo "$ENVS" | grep -q "^PHP_MEMORY_LIMIT=" || { echo "PHP_MEMORY_LIMIT missing"; exit 1; } - name: www-data runs with uid/gid 82 (v1 parity) - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: | IMAGE=${{ steps.image-tag.outputs.IMAGE_TAG }} UID_GID=$(docker run --rm $IMAGE sh -c 'echo "$(id -u):$(id -g)"') [ "$UID_GID" = "82:82" ] || { echo "expected uid:gid 82:82, got $UID_GID"; exit 1; } dev: - name: Dev v2026.1 - runs-on: ubuntu-latest - needs: [frankenphp] - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Get all changed files - id: changed-files - uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 - with: - files: | - v2026.1/** - - - name: Set build date - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' - run: echo "buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" - - - name: Build - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: ./.github/action/build-bake-publish - with: - files: v2026.1/docker-bake.hcl - targets: dev - docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }} - docker_hub_password: ${{ secrets.DOCKER_HUB_PASSWORD }} - github_token: ${{ secrets.GITHUB_TOKEN }} + name: Dev ${{ matrix.minor }} Node ${{ matrix.node }} + needs: [prepare, frankenphp] + if: needs.prepare.outputs.build == 'true' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.prepare.outputs.dev-matrix) }} + permissions: + contents: read + id-token: write + packages: write + # no release tag published yet — pinned to main + uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a + with: + files: v2026.1/docker-bake.hcl + target: ${{ matrix.target }} + output: image + push: true + sbom: true + runner: | + default=ubuntu-24.04 + linux/arm64=ubuntu-24.04-arm + vars: | + imageSuffix=${{ needs.prepare.outputs.image-suffix }} + tagPrefix=${{ needs.prepare.outputs.tag-prefix }} + gitSha=${{ github.sha }} + buildDate=${{ needs.prepare.outputs.build-date }} + meta-images: ${{ needs.prepare.outputs.dev-images }} + meta-flavor: | + latest=false + meta-tags: | + type=raw,value=${{ needs.prepare.outputs.tag-prefix }}${{ matrix.minor }}-node${{ matrix.node }}-v2026.1 + type=raw,value=${{ needs.prepare.outputs.tag-prefix }}${{ matrix.php }}-node${{ matrix.node }}-v2026.1 + secrets: + registry-auths: | + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} dev-check: name: Check Dev v2026.1 Image @@ -155,15 +220,7 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Get all changed files - id: changed-files - uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46 - with: - files: | - v2026.1/** - - name: Install container-structure-test - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0 with: repo: GoogleContainerTools/container-structure-test @@ -172,7 +229,6 @@ jobs: chmod: 0755 - name: Determine image tag - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' id: image-tag run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then @@ -182,9 +238,7 @@ jobs: fi - name: Pull image - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }} - name: Test Dev Image - if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'push' && github.ref == 'refs/heads/main' run: container-structure-test test --config v2026.1/dev/config.yaml --image ${{ steps.image-tag.outputs.IMAGE_TAG }} diff --git a/v2026.1/docker-bake.hcl b/v2026.1/docker-bake.hcl index 10fe619..ab82bb5 100644 --- a/v2026.1/docker-bake.hcl +++ b/v2026.1/docker-bake.hcl @@ -1,6 +1,9 @@ # v2026.1 images — built alongside v1 (see adr/2026-07-08-docker-image-variant-v2.md) # # Build from the repository root: docker buildx bake -f v2026.1/docker-bake.hcl +# CI builds run through docker/github-builder, which supplies tags (docker/metadata-action) +# and signed SBOM/provenance attestations — the tags below apply to local builds only. +# # NOTE: bake resolves relative context paths against the working directory, # not this file — paths below are therefore relative to the repository root. @@ -96,10 +99,6 @@ target "frankenphp" { "com.shopware.image.security-only" = securityOnlyDate "com.shopware.image.eol" = eolDate } - attest = [ - "type=sbom", - "type=provenance,mode=max" - ] platforms = [ "linux/amd64", "linux/arm64" ] tags = imageSuffix != "" ? [ "ghcr.io/shopware/docker-base${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-frankenphp-${imageVersion}", @@ -136,10 +135,6 @@ target "dev" { "com.shopware.image.security-only" = securityOnlyDate "com.shopware.image.eol" = eolDate } - attest = [ - "type=sbom", - "type=provenance,mode=max" - ] platforms = [ "linux/amd64", "linux/arm64" ] tags = [ "ghcr.io/shopware/docker-dev${imageSuffix}:${tagPrefix}${substr(item.php, 0, 3)}-node${node}-${imageVersion}", From 5886132ed410cf144b17e81c3c501658d6528746 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:17:05 +0200 Subject: [PATCH 3/7] fix: use latest stable dockerfile frontend for v2026.1 images The pinned docker/dockerfile:1.7 frontend (2024) does not know the source.git.checksum capability that docker/github-builder's git-context builds pass, making every CI build fail with: failed to resolve dockerfile: unknown API capability source.git.checksum The floating :1 tag resolves to the latest stable frontend, which supports it. Co-Authored-By: Claude Fable 5 --- v2026.1/dev/Dockerfile | 6 +++++- v2026.1/frankenphp/Dockerfile | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/v2026.1/dev/Dockerfile b/v2026.1/dev/Dockerfile index 317d684..49e28c9 100644 --- a/v2026.1/dev/Dockerfile +++ b/v2026.1/dev/Dockerfile @@ -1,6 +1,10 @@ -#syntax=docker/dockerfile:1.7 +#syntax=docker/dockerfile:1 #check=skip=SecretsUsedInArgOrEnv +# The unpinned syntax directive is deliberate: it resolves to the latest stable +# dockerfile frontend. An older pin (1.7) lacks the source.git.checksum +# capability that docker/github-builder's git-context builds require. + # v2 dev image — built on top of the v2 FrankenPHP image # (see adr/2026-07-08-docker-image-variant-v2.md) ARG NODE_VERSION=22 diff --git a/v2026.1/frankenphp/Dockerfile b/v2026.1/frankenphp/Dockerfile index 31d979b..f8f0958 100644 --- a/v2026.1/frankenphp/Dockerfile +++ b/v2026.1/frankenphp/Dockerfile @@ -1,6 +1,10 @@ -#syntax=docker/dockerfile:1.7 +#syntax=docker/dockerfile:1 #check=skip=SecretsUsedInArgOrEnv +# The unpinned syntax directive is deliberate: it resolves to the latest stable +# dockerfile frontend. An older pin (1.7) lacks the source.git.checksum +# capability that docker/github-builder's git-context builds require. + # v2 image — see adr/2026-07-08-docker-image-variant-v2.md # The base image is pinned by digest via docker-bake.hcl (single source of truth). ARG PHP_BASE_IMAGE=dunglas/frankenphp:php8.4 From 8544038e388c83367c387bef31ebac43ee377d9d Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:18:06 +0200 Subject: [PATCH 4/7] chore: drop new-shopware-setup from the v2026.1 dev image Co-Authored-By: Claude Fable 5 --- .../rootfs/usr/local/bin/new-shopware-setup | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100755 v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup diff --git a/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup b/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup deleted file mode 100755 index 89ac440..0000000 --- a/v2026.1/dev/rootfs/usr/local/bin/new-shopware-setup +++ /dev/null @@ -1,33 +0,0 @@ -#/usr/bin/env bash - -set -e - -if [ "$(ls -A /var/www/html)" ]; then - echo "Error: /var/www/html is not empty." >&2 - exit 1 -fi - -cd /var/www/html - -SHOPWARE_VERSION=${1:-*} - -echo "Creating Shopware project..." -(composer create-project shopware/production:$SHOPWARE_VERSION . --no-interaction --prefer-dist --no-progress > /tmp/install.log 2>&1) || { echo "Installation failed. Log:"; cat /tmp/install.log; exit 1; } - -echo "Adding developer tools..." -(composer require shopware/docker-dev --no-interaction > /tmp/install.log 2>&1) || { echo "Adding docker-dev failed. Log:"; cat /tmp/install.log; exit 1; } - -if [ -f compose.yaml ]; then - PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') - sed -i "s/php[0-9]\.[0-9]/php$PHP_VERSION/g" compose.yaml -fi - -echo -n "Do you want to use Elasticsearch? (y/N): " -read use_elasticsearch - -if [ -z "$use_elasticsearch" ] || [ "$use_elasticsearch" != "y" ] && [ "$use_elasticsearch" != "Y" ]; then - echo "Removing Elasticsearch..." - (composer remove shopware/elasticsearch --no-interaction --no-scripts > /tmp/install.log 2>&1 && rm -fr var/cache/*) || { echo "Removing elasticsearch failed. Log:"; cat /tmp/install.log; exit 1; } -fi - -echo "Project created successfully. Run 'make up' to start the containers and 'make setup' to initialize the project." From 9bf167c2890c01e2a27016dfb37c3743f4410234 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:20:35 +0200 Subject: [PATCH 5/7] fix: pin dockerfile frontend by digest, refreshed by update automation Addresses the supply-chain concern of the floating docker/dockerfile:1 tag: the frontend is now pinned by manifest digest like the base images, and update-php-matrix.mjs refreshes the pin in every versioned directory. Co-Authored-By: Claude Fable 5 --- update-php-matrix.mjs | 53 +++++++++++++++++++++++++++++++++++ v2026.1/dev/Dockerfile | 6 ++-- v2026.1/frankenphp/Dockerfile | 6 ++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/update-php-matrix.mjs b/update-php-matrix.mjs index 2505619..1af27d2 100755 --- a/update-php-matrix.mjs +++ b/update-php-matrix.mjs @@ -123,6 +123,51 @@ async function updatePhpMatrixInHcl(phpVersions, frankenphpVersions) { console.log('FrankenPHP versions:', frankenphpVersions); } +// Function to fetch the manifest-list digest of the docker/dockerfile:1 frontend +async function fetchDockerfileFrontendDigest() { + const url = 'https://hub.docker.com/v2/repositories/docker/dockerfile/tags/1'; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch docker/dockerfile:1 tag: ${response.status} ${response.statusText}`); + } + + const jsonData = await response.json(); + if (!jsonData.digest) { + throw new Error('No digest found for docker/dockerfile:1'); + } + + return jsonData.digest; +} + +// Function to update the digest-pinned #syntax frontend line in the Dockerfiles of a versioned directory +async function updateDockerfileFrontendPin(versionDir, digest) { + const dirents = await fs.readdir(versionDir, { withFileTypes: true }); + + for (const dirent of dirents) { + if (!dirent.isDirectory()) { + continue; + } + + const dockerfilePath = `${versionDir}/${dirent.name}/Dockerfile`; + let content; + try { + content = await fs.readFile(dockerfilePath, 'utf8'); + } catch { + continue; + } + + const syntaxRegex = /^#syntax=docker\/dockerfile:1@sha256:[0-9a-f]{64}$/m; + if (!syntaxRegex.test(content)) { + continue; + } + + content = content.replace(syntaxRegex, `#syntax=docker/dockerfile:1@${digest}`); + await fs.writeFile(dockerfilePath, content); + console.log(`Successfully updated dockerfile frontend pin in ${dockerfilePath}`); + } +} + // Function to find the bake files of calendar-versioned images (v2026.1/docker-bake.hcl, ...) async function findVersionedHclFiles() { const dirents = await fs.readdir('.', { withFileTypes: true }); @@ -210,4 +255,12 @@ if (versionedHclFiles.length > 0) { for (const hclPath of versionedHclFiles) { await updateDigestMatrixInHcl(hclPath, digestEntries); } + + console.log('Fetching digest for docker/dockerfile:1 frontend...'); + const frontendDigest = await fetchDockerfileFrontendDigest(); + console.log(`Found digest for docker/dockerfile:1: ${frontendDigest}`); + + for (const hclPath of versionedHclFiles) { + await updateDockerfileFrontendPin(hclPath.replace('/docker-bake.hcl', ''), frontendDigest); + } } diff --git a/v2026.1/dev/Dockerfile b/v2026.1/dev/Dockerfile index 49e28c9..5f5af0a 100644 --- a/v2026.1/dev/Dockerfile +++ b/v2026.1/dev/Dockerfile @@ -1,8 +1,8 @@ -#syntax=docker/dockerfile:1 +#syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 #check=skip=SecretsUsedInArgOrEnv -# The unpinned syntax directive is deliberate: it resolves to the latest stable -# dockerfile frontend. An older pin (1.7) lacks the source.git.checksum +# The dockerfile frontend is pinned by digest (refreshed by update-php-matrix.mjs). +# It must stay recent: frontends before 1.10 lack the source.git.checksum # capability that docker/github-builder's git-context builds require. # v2 dev image — built on top of the v2 FrankenPHP image diff --git a/v2026.1/frankenphp/Dockerfile b/v2026.1/frankenphp/Dockerfile index f8f0958..c15980f 100644 --- a/v2026.1/frankenphp/Dockerfile +++ b/v2026.1/frankenphp/Dockerfile @@ -1,8 +1,8 @@ -#syntax=docker/dockerfile:1 +#syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 #check=skip=SecretsUsedInArgOrEnv -# The unpinned syntax directive is deliberate: it resolves to the latest stable -# dockerfile frontend. An older pin (1.7) lacks the source.git.checksum +# The dockerfile frontend is pinned by digest (refreshed by update-php-matrix.mjs). +# It must stay recent: frontends before 1.10 lack the source.git.checksum # capability that docker/github-builder's git-context builds require. # v2 image — see adr/2026-07-08-docker-image-variant-v2.md From a9ad1d13322d16c07301436feeb9d45ee4d35e77 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:24:16 +0200 Subject: [PATCH 6/7] chore: trim comments in v2026.1 build files Co-Authored-By: Claude Fable 5 --- v2026.1/dev/Dockerfile | 14 +++------ v2026.1/dev/rootfs/entrypoint-dev | 6 +--- v2026.1/docker-bake.hcl | 26 +++++++--------- v2026.1/frankenphp/Dockerfile | 30 +++++++------------ v2026.1/frankenphp/rootfs/entrypoint | 3 -- .../usr/local/bin/shopware-image-lifecycle | 8 ++--- 6 files changed, 28 insertions(+), 59 deletions(-) diff --git a/v2026.1/dev/Dockerfile b/v2026.1/dev/Dockerfile index 5f5af0a..6307d1d 100644 --- a/v2026.1/dev/Dockerfile +++ b/v2026.1/dev/Dockerfile @@ -1,12 +1,8 @@ #syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 #check=skip=SecretsUsedInArgOrEnv -# The dockerfile frontend is pinned by digest (refreshed by update-php-matrix.mjs). -# It must stay recent: frontends before 1.10 lack the source.git.checksum -# capability that docker/github-builder's git-context builds require. - -# v2 dev image — built on top of the v2 FrankenPHP image -# (see adr/2026-07-08-docker-image-variant-v2.md) +# frontend digest refreshed by update-php-matrix.mjs; must stay >= 1.10 for +# the source.git.checksum capability used by docker/github-builder ARG NODE_VERSION=22 FROM node:${NODE_VERSION}-bookworm-slim AS node @@ -15,8 +11,7 @@ FROM base ARG NODE_VERSION -# Versions are injected from docker-bake.hcl (single source of truth). -# tideways/blackfire/spx are distributed without PECL version pins. +# tideways/blackfire/spx have no PECL version pins ARG PHP_DEV_PINNED_EXTENSIONS="xdebug-3.5.3" ARG PHP_DEV_EXTENSIONS="tideways blackfire spx" @@ -46,8 +41,7 @@ RUN < - # by adding their ini directory to PHP_INI_SCAN_DIR (see entrypoint-dev) + # installed but not loaded; the entrypoint enables one via PHP_PROFILER= for ext in xdebug tideways blackfire spx; do mkdir -p /usr/local/etc/php/conf.available/$ext mv /usr/local/etc/php/conf.d/docker-php-ext-$ext.ini /usr/local/etc/php/conf.available/$ext/ diff --git a/v2026.1/dev/rootfs/entrypoint-dev b/v2026.1/dev/rootfs/entrypoint-dev index 30d1a7e..e7e9411 100755 --- a/v2026.1/dev/rootfs/entrypoint-dev +++ b/v2026.1/dev/rootfs/entrypoint-dev @@ -2,9 +2,6 @@ set -e -# Profilers (xdebug, tideways, blackfire, spx) ship installed but not loaded. -# PHP_PROFILER= enables one by appending its ini directory to -# PHP_INI_SCAN_DIR — no filesystem writes needed. if [ -n "${PHP_PROFILER:-}" ]; then if [ -d "/usr/local/etc/php/conf.available/$PHP_PROFILER" ]; then if [ -z "${PHP_INI_SCAN_DIR:-}" ]; then @@ -18,8 +15,7 @@ if [ -n "${PHP_PROFILER:-}" ]; then fi fi -# Boot fallback only: a fresh dev container without a project yet needs an -# APP_SECRET to serve anything. Never shadows container env or an .env file. +# boot fallback for a container without a project; never shadows container env or an .env file if [ -z "${APP_SECRET:-}" ] && [ ! -f /var/www/html/.env ] && [ ! -f /var/www/html/.env.local ]; then export APP_SECRET=def00000bb5acb32b54ff8ee130270586eec0e878f7337dc7a837acc31d3ff00f93a56b595448b4b29664847dd51991b3314ff65aeeeb761a133b0ec0e070433bff08e48 fi diff --git a/v2026.1/docker-bake.hcl b/v2026.1/docker-bake.hcl index ab82bb5..da7505a 100644 --- a/v2026.1/docker-bake.hcl +++ b/v2026.1/docker-bake.hcl @@ -1,11 +1,10 @@ -# v2026.1 images — built alongside v1 (see adr/2026-07-08-docker-image-variant-v2.md) +# v2026.1 images, built alongside v1 (see adr/2026-07-08-docker-image-variant-v2.md) # -# Build from the repository root: docker buildx bake -f v2026.1/docker-bake.hcl -# CI builds run through docker/github-builder, which supplies tags (docker/metadata-action) -# and signed SBOM/provenance attestations — the tags below apply to local builds only. +# Run from the repository root: docker buildx bake -f v2026.1/docker-bake.hcl +# (bake resolves relative context paths against the working directory, not this file) # -# NOTE: bake resolves relative context paths against the working directory, -# not this file — paths below are therefore relative to the repository root. +# CI builds through docker/github-builder, which supplies the tags and signed +# SBOM/provenance attestations — the tags below apply to local builds only. variable "imageSuffix" { default = "" @@ -15,13 +14,11 @@ variable "tagPrefix" { default = "" } -# Calendar version of this contract (ADR section 1) — matches the directory name variable "imageVersion" { default = "v2026.1" } -# Lifecycle dates baked into every image (ADR section 7). -# Empty = not scheduled yet. Format: YYYY-MM-DD. +# lifecycle dates (YYYY-MM-DD), empty = not scheduled yet variable "securityOnlyDate" { default = "" } @@ -30,7 +27,7 @@ variable "eolDate" { default = "" } -# Set by CI for OCI labels +# set by CI for the OCI labels variable "gitSha" { default = "" } @@ -39,8 +36,7 @@ variable "buildDate" { default = "" } -# Updated by update-php-matrix.mjs. The digest pins the multi-arch manifest -# list of dunglas/frankenphp:php so rebuilds are reproducible (ADR section 6). +# updated by update-php-matrix.mjs variable "frankenphpDigestMatrix" { default = [ { php = "8.2.33", digest = "sha256:ab7284dddea6f9430986918b270d09adf751e8edb2053ab2cac50240a39959bb" }, @@ -50,8 +46,7 @@ variable "frankenphpDigestMatrix" { ] } -# Single source of truth for the PHP extension set (ADR section 5). -# Core extensions are versioned implicitly by the pinned PHP base image. +# core extensions are versioned implicitly by the pinned PHP base image variable "installPhpExtensionsVersion" { default = "2.11.12" } @@ -64,8 +59,7 @@ variable "pinnedExtensions" { default = "redis-6.3.0 apcu-5.1.28 amqp-2.2.0 zstd-0.18.0" } -# Installed but not loaded by default (ADR section 3); -# enabled via PHP_EXTENSION_GRPC=1 / PHP_EXTENSION_OPENTELEMETRY=1 +# installed but not loaded by default, enabled via PHP_EXTENSION_GRPC=1 / PHP_EXTENSION_OPENTELEMETRY=1 variable "optionalExtensions" { default = "grpc-1.83.0 opentelemetry-1.2.1" } diff --git a/v2026.1/frankenphp/Dockerfile b/v2026.1/frankenphp/Dockerfile index c15980f..2d3b44a 100644 --- a/v2026.1/frankenphp/Dockerfile +++ b/v2026.1/frankenphp/Dockerfile @@ -1,24 +1,20 @@ #syntax=docker/dockerfile:1@sha256:ecfaec9ed6d810b56388c508f4121597bfbba70d41a6dfeee4d8cad5f295fc32 #check=skip=SecretsUsedInArgOrEnv -# The dockerfile frontend is pinned by digest (refreshed by update-php-matrix.mjs). -# It must stay recent: frontends before 1.10 lack the source.git.checksum -# capability that docker/github-builder's git-context builds require. +# frontend digest refreshed by update-php-matrix.mjs; must stay >= 1.10 for +# the source.git.checksum capability used by docker/github-builder -# v2 image — see adr/2026-07-08-docker-image-variant-v2.md -# The base image is pinned by digest via docker-bake.hcl (single source of truth). +# see adr/2026-07-08-docker-image-variant-v2.md ARG PHP_BASE_IMAGE=dunglas/frankenphp:php8.4 FROM ${PHP_BASE_IMAGE} -# All versions below are injected from docker-bake.hcl so production and dev -# images cannot drift (ADR section 5: single source of truth). +# defaults overridden from docker-bake.hcl ARG INSTALL_PHP_EXTENSIONS_VERSION=2.11.12 ARG PHP_CORE_EXTENSIONS="bcmath gd intl mysqli pdo_mysql pcntl sockets bz2 gmp soap zip ftp ffi opcache xsl" ARG PHP_PINNED_EXTENSIONS="redis-6.3.0 apcu-5.1.28 amqp-2.2.0 zstd-0.18.0" ARG PHP_OPTIONAL_EXTENSIONS="grpc-1.83.0 opentelemetry-1.2.1" -# Pinned extension installer instead of the bundled/latest one (ADR section 5) ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/download/${INSTALL_PHP_EXTENSIONS_VERSION}/install-php-extensions /usr/local/bin/install-php-extensions RUN </dev/null || exit 1 -# Best-effort build-time EOL warning for downstream images (ADR section 7). -# Warn-only by contract: the script always exits 0. +# best-effort EOL warning for downstream builds; always exits 0 ONBUILD RUN /usr/local/bin/shopware-image-lifecycle build ENTRYPOINT ["/entrypoint"] diff --git a/v2026.1/frankenphp/rootfs/entrypoint b/v2026.1/frankenphp/rootfs/entrypoint index f2cb1e7..55850a9 100755 --- a/v2026.1/frankenphp/rootfs/entrypoint +++ b/v2026.1/frankenphp/rootfs/entrypoint @@ -4,9 +4,6 @@ set -e /usr/local/bin/shopware-image-lifecycle runtime -# grpc and opentelemetry ship installed but not loaded. Enabling them appends -# their ini directory to PHP_INI_SCAN_DIR instead of writing to the filesystem, -# so this also works on a read-only root filesystem. append_scan_dir() { if [ -z "${PHP_INI_SCAN_DIR:-}" ]; then # a leading colon keeps the compiled-in conf.d in the scan path diff --git a/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle b/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle index 9dc1a32..96c9692 100755 --- a/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle +++ b/v2026.1/frankenphp/rootfs/usr/local/bin/shopware-image-lifecycle @@ -1,12 +1,10 @@ #!/usr/bin/env sh -# Prints image lifecycle warnings based on the dates baked in at build time -# (see adr/2026-07-08-docker-image-variant-v2.md, section 7). -# # Usage: shopware-image-lifecycle [runtime|build] # -# Strictly warn-only by contract: this script ALWAYS exits 0 — an EOL base -# image never fails a downstream build and never refuses to start. +# Prints image lifecycle warnings based on the dates baked in at build time. +# Warn-only by contract: always exits 0, an EOL image must never fail a +# downstream build or refuse to start. mode="${1:-runtime}" From 6810372b3dd951ee098a36dd111d15502bd72172 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 24 Aug 2026 09:28:37 +0200 Subject: [PATCH 7/7] chore: drop the setup script from the v2026.1 image Co-Authored-By: Claude Fable 5 --- v2026.1/frankenphp/rootfs/setup | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100755 v2026.1/frankenphp/rootfs/setup diff --git a/v2026.1/frankenphp/rootfs/setup b/v2026.1/frankenphp/rootfs/setup deleted file mode 100755 index d465b46..0000000 --- a/v2026.1/frankenphp/rootfs/setup +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env sh - -set -e - -cd /var/www/html - -if [ -x ./vendor/bin/shopware-deployment-helper ]; then - exec ./vendor/bin/shopware-deployment-helper run -fi - -echo "### ERROR ###" -echo "" -echo "### The setup scripts are removed ###" -echo "### Please install the Shopware Deployment Helper using composer require shopware/deployment-helper to continue ###" -echo "### For more information see https://developer.shopware.com/docs/guides/hosting/installation-updates/deployments/deployment-helper.html ###" -echo "" -echo "### ERROR ###" -echo "" - -exit 1