-
Notifications
You must be signed in to change notification settings - Fork 19
docs: add ADR for Docker Image Variant v2 #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| # Docker Image Variant v2 | ||
|
|
||
| ## Status | ||
|
|
||
| Proposed — discussed in [shopware/docker#150](https://github.com/shopware/docker/issues/150) | ||
|
|
||
| ## Context | ||
|
|
||
| The images built from this repository are consumed by an unknown number of production deployments that we cannot reach reliably. All published tags (e.g. `ghcr.io/shopware/docker-base:8.3-frankenphp`) are **rolling**: every rebuild overwrites the tag in place. This has several consequences: | ||
|
|
||
| 1. **We cannot ship breaking changes safely.** Any change to the base OS, bundled extensions, or default environment variables is silently rolled out to everyone on the next pull. | ||
| 2. **Users cannot roll back.** When a rebuild introduces a regression (e.g. a broken extension update), the previous image is gone unless the user happened to pin a digest. | ||
| 3. **Builds are not reproducible.** Several inputs are unpinned: | ||
| - `install-php-extensions` is fetched from the `latest` GitHub release (`fpm/Dockerfile`), so the installer itself changes between builds. | ||
| - Most extensions (`gd`, `intl`, `redis`, `soap`, …) are installed without a version constraint and resolve to whatever PECL/upstream serves at build time. Only a few are pinned today (`apcu-5.1.27`, `zstd-0.15.2`, `xdebug@3.5.0`, `amqp` via git commit), and those pins were mostly added reactively after upstream breakage. | ||
| - `apk upgrade` / `apt-get upgrade` pulls in whatever the distribution mirrors serve. | ||
|
|
||
| Additionally, the current image matrix has grown large and expensive to maintain: 8 production variants (`fpm`, `fpm-otel`, `caddy`, `caddy-otel`, `nginx`, `nginx-otel`, `frankenphp`, `frankenphp-otel`) plus dev images (`caddy`/`nginx` × Node 22/24), each across 4 PHP versions and 2 CPU architectures. | ||
|
|
||
| Finally, the production images are Alpine-based (except FrankenPHP, which is already Debian-based). musl brings recurring problems: | ||
|
|
||
| - **Performance**: musl's default allocator degrades significantly under memory-intensive and multi-threaded workloads compared to glibc (reports range from 2× up to 6× slowdowns for allocation-heavy code paths). | ||
| - **Ecosystem friction**: no official Node.js binaries for musl (the dev image copies Node out of the `node:alpine` image), and extensions such as `amqp` and `grpc` need source builds with custom patches on Alpine. | ||
|
|
||
| ## Decision | ||
|
|
||
| ### 1. Calendar-versioned, immutable image tags | ||
|
|
||
| We adopt a calendar-based versioning scheme, following the model of [pimcore/docker](https://github.com/pimcore/docker#versioning): | ||
|
|
||
| - `ghcr.io/shopware/docker-base:8.3-frankenphp` — rolling tag (points to the latest supported version) | ||
| - `ghcr.io/shopware/docker-base:8.3-frankenphp-v2026.1` — versioned tag | ||
|
|
||
| Rules: | ||
|
|
||
| - A **versioned tag** (`-vYYYY.N`) never receives breaking changes. It continues to be rebuilt on schedule so it picks up OS security patches, PHP patch releases and extension bugfix releases, but the contract (base OS, extension list, environment variable defaults, entrypoint behavior) is frozen for its lifetime. | ||
| - **Breaking changes only ship in a new calendar version.** Users opt in by moving the suffix. | ||
| - A new calendar version deprecates the previous one. During the deprecation window the old version keeps being rebuilt (security patches only) and its image logs a deprecation warning at container start. Rollover of the rolling tag and end of rebuilds are governed by the support policy below (section 7). | ||
| - Users who need bit-exact reproducibility should additionally pin by digest; versioned tags trade strict immutability for continued security patching, which is the right default for a base image. | ||
|
|
||
| ### 2. Debian as the only base OS | ||
|
|
||
| v2 images are built on Debian slim (matching the base already used by `dunglas/frankenphp`) instead of Alpine: | ||
|
|
||
| - glibc avoids the musl allocator/threading performance issues. | ||
| - Official upstream packages (Node.js, grpc) become usable without source builds or musl patches. | ||
| - The size difference between `debian:*-slim` and Alpine is acceptable for a PHP application image where the PHP layer dominates anyway. | ||
|
|
||
| ### 3. One production variant: FrankenPHP, batteries included | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for only using and endorsing FrankenPHP
renaudhager marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://frankenphp.dev/docs/known-issues/#buggy-php-extensions How do you think about the issue with support for blackfire and datadog?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same thought about ensuring support for:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I pinged Blackfire, Datadog and OTEL. any other does not low level things |
||
|
|
||
| v2 reduces the production matrix to a single variant: | ||
|
|
||
| - `ghcr.io/shopware/docker-base:8.3-frankenphp-v2026.1` | ||
|
|
||
| with **gRPC and OpenTelemetry always included** (extensions installed but loadable/configurable via environment, so the cost for non-users is disk size only, not runtime overhead). The `fpm`, `caddy`, `nginx` and all `-otel` variants are not continued in v2. | ||
|
|
||
| Rationale: the 8-variant matrix multiplies build time, security scanning, and support surface, while FrankenPHP covers the same use cases with a single process model (and worker mode as an upside). Users who require a plain FPM pool behind their own web server can stay on v1 during the deprecation window; if there is significant demand, an `fpm` variant can be re-added to v2 in a later calendar version — the versioning scheme makes that a non-breaking addition. (Raised in the issue discussion: nginx is the familiar entry point for newcomers. We accept this trade-off in favor of a drastically smaller matrix and will address it with documentation and a migration guide rather than by keeping the variant.) | ||
|
|
||
| Dev images follow the same consolidation: | ||
|
|
||
| - `ghcr.io/shopware/docker-dev:8.3-node22-v2026.1` | ||
| - `ghcr.io/shopware/docker-dev:8.3-node24-v2026.1` | ||
|
|
||
| ### 4. Shopware application defaults move out of the image | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like a great change (an probably overdue) |
||
|
|
||
| The image no longer bakes Shopware application-level environment variables (`APP_ENV`, `LOCK_DSN`, `MAILER_DSN`, `SHOPWARE_*`, `INSTALL_*`, …) into `ENV` layers. `ENV` values defined in the image always win over an `.env` file loaded by the application, which surprises users and makes the image dictate application config. | ||
|
|
||
| - **Kept in the image**: infrastructure-level defaults that configure the runtime itself (`PHP_*` ini tuning, FrankenPHP/Caddy settings, `COMPOSER_*`). | ||
| - **Removed from the image**: everything the application reads as business/deployment config. Defaults that are genuinely required for the container to boot are documented and set in the entrypoint only if unset, so both container `env` and `.env` files can override them. | ||
|
|
||
| (Raised in the issue discussion: container-provided env vars should stay the single source of truth. This decision preserves that — explicitly set container env always wins; the change only removes *image-baked* defaults that currently shadow both `.env` files and sane application defaults.) | ||
|
|
||
| ### 5. PHP extension update policy | ||
|
|
||
| v2 makes extension management explicit and automated instead of implicit and reactive: | ||
|
|
||
| - **Pin everything.** Every PECL/third-party extension is installed with an exact version (`redis-6.2.0`, `apcu-5.1.27`, …) using `install-php-extensions`' version syntax. The installer itself is pinned to a release tag instead of `latest`. Core extensions (`intl`, `gd`, …) are versioned implicitly by the pinned PHP base image. | ||
| - **Automate updates.** A scheduled workflow (extending the existing `update-php-matrix` mechanism, or Renovate with a custom regex manager over a central extension manifest) opens a PR when a new extension release is available. Updates land through review + CI, not silently at build time. | ||
| - **Single source of truth.** Extension names and versions live in one place (e.g. bake variables or a manifest file consumed by the Dockerfiles) rather than being repeated per Dockerfile, so the dev image can no longer drift from the production image. | ||
| - **Update semantics per tag class**: | ||
| - *Versioned tags* receive extension **patch/bugfix** updates via the scheduled rebuild; **major/minor** extension bumps only ship with a new calendar version, since ABI or behavior changes in extensions are breaking from the consumer's perspective. | ||
| - *Rolling tags* follow whatever the current calendar version ships. | ||
| - **Fewer source builds.** On Debian, `grpc` and `amqp` install from upstream packages/PECL without custom patches, removing the git-commit pins and patch files that currently make those two extensions the most fragile part of the build. | ||
|
|
||
| ### 6. Supply-chain and Dockerfile best practices | ||
|
|
||
| Together with v2 we adopt the following (largely orthogonal, but cheapest to introduce at a version boundary): | ||
|
|
||
| - **Pin base images by digest** (`FROM dunglas/frankenphp:1.x-php8.3@sha256:…`), with automated digest bumps via the update workflow, so a rebuild of an unchanged commit produces a predictable result. | ||
| - **Publish SBOM and SLSA provenance attestations** from `docker buildx` (`--sbom=true --provenance=mode=max`) and sign images with cosign, so consumers can verify what an image contains and where it was built. The existing daily vulnerability scan workflow continues to cover published images. | ||
| - **Standard OCI labels** (`org.opencontainers.image.source`, `.revision`, `.version`, `.created`) on every image so tooling can map an image back to the exact commit. | ||
| - **`HEALTHCHECK`** built into the image (FrankenPHP/Caddy exposes an endpoint), so orchestrators get container health without per-deployment configuration. | ||
| - **Keep the non-root default** (`USER www-data`, already in place) and continue to run scheduled rebuilds so OS packages stay patched between releases. | ||
|
|
||
| ### 7. Support policy | ||
|
|
||
| Support windows are an explicit, dated promise — an image that silently stops being rebuilt accumulates CVEs without anyone noticing, so every lifecycle event below is announced with a date rather than derived from a formula. | ||
|
|
||
| **Calendar version lifecycle** | ||
|
|
||
| - **At most one calendar version per year**, and only when a breaking change actually requires one. There is no version bump for cadence's sake. | ||
| - **At most two calendar versions are supported concurrently**: the current one (full support) and the previous one (security-only rebuilds: OS packages, PHP patch releases, extension bugfix releases — no new features, no new PHP minor versions). This caps the build and scan matrix at 2×. | ||
| - **The previous version reaches end of life 12 months after its successor is released.** This matches the extended-support year of a Shopware major release and covers one full merchant/agency upgrade cycle. After EOL, tags remain pullable but are frozen and no longer rebuilt. | ||
| - **Rolling tags switch to the new calendar version 3 months after its release**, announced at release and preceded by a startup warning in the old image. Rolling tags are documented as "latest, may introduce breaking changes"; anyone who needs stability pins a calendar version. | ||
|
|
||
| **PHP version lifecycle** | ||
|
|
||
| - **PHP versions that are end of life upstream are not supported — dropped on day one.** The moment a PHP minor version reaches its upstream end of security support ([php.net supported versions](https://www.php.net/supported-versions.php)), its image tags stop being rebuilt immediately, in *every* calendar version, regardless of how much support time the calendar version itself has left. There is no grace period: without upstream patches a "security rebuild" of that PHP version is impossible, and continuing to publish it would only feign safety. New calendar versions launch without EOL PHP versions from day one. | ||
| - PHP versions in their upstream *security-only* phase remain part of the matrix and receive patch rebuilds as usual. | ||
| - A **new** PHP minor version is only added to the *current* calendar version — adding one is a feature, not a security fix. | ||
| - The versioned-tag contract is therefore: same base OS, extension set, and defaults **for the PHP versions upstream still supports**. A PHP tag disappearing due to upstream EOL is not a breaking change to the calendar version. | ||
|
|
||
| **Propagating EOL to users** | ||
|
|
||
| A base image has no channel back to its consumers, so the images carry their own lifecycle information: | ||
|
|
||
| - **Every image knows its own dates.** The lifecycle dates are baked into every image as OCI labels (`com.shopware.image.version`, `com.shopware.image.security-only`, `com.shopware.image.eol`) for registries, scanners and admission controllers to read. In addition, the entrypoint compares the baked EOL date against the current clock at container start and prints a warning once the date has passed. Because the date is known and baked in at build time, this works even after rebuilds have stopped — a frozen image starts warning on EOL day without any rebuild, registry support, or network call. The warning escalates with the lifecycle stage: a single startup line during the security-only phase, a prominent warning in the last 3 months, a loud permanent warning past EOL. Setting `SHOPWARE_DOCKER_SUPPRESS_EOL_WARNING=1` silences it for users who consciously accept the risk. | ||
| - **Developers are warned at build time, not only at runtime.** Consumers do not run `docker-base` directly — they build their own image `FROM` it, so a runtime log line reaches the ops team while the people who can bump the tag live in CI. An `ONBUILD RUN` hook performs the same EOL date check during every downstream build, and `shopware-cli` / the deployment helper read the EOL label and warn during project builds. Both are strictly warn-only: an EOL base image never fails a downstream build or refuses to start — warning without blocking is the contract. | ||
|
|
||
| **Communication** | ||
|
|
||
| - The README carries an EOL table (version, release date, security-only date, EOL date), updated at every release; PHP-EOL-driven removals are announced ahead of the known upstream dates. | ||
| - Example, assuming v2026.1 releases 2026-07 and v2027.1 releases 2027-07: | ||
|
|
||
| | Version | Release | Rolling tag flips | Security-only | EOL | | ||
| |---|---|---|---|---| | ||
| | v1 (legacy, unversioned) | — | 2026-10 (3 months after v2026.1) | 2026-07 (v2026.1 GA) | 2027-07 | | ||
| | v2026.1 | 2026-07 | — | 2027-07 (v2027.1 GA) | 2028-07 | | ||
| | PHP 8.2 tags (all versions) | — | — | — | 2026-12-31 (upstream EOL, dropped day one) | | ||
|
|
||
| - **v1 special case**: current users never opted into a versioning contract, so the existing unversioned images get the same treatment as a "previous version" — 12 months of security-only rebuilds starting at v2026.1 GA, with a deprecation notice at container start. This doubles as the migration grace period for users of the discontinued `fpm`/`caddy`/`nginx` variants. | ||
|
|
||
| ## Consequences | ||
|
|
||
| ### Positive | ||
|
|
||
| - Breaking changes become shippable: users opt in per calendar version, with a documented migration path and deprecation window instead of surprise breakage. | ||
| - Rollback becomes possible: previous calendar versions keep working while a regression in the new one is fixed. | ||
| - Reproducibility and auditability improve significantly (pins + digests + SBOM/provenance). | ||
| - The build/scan/support matrix shrinks from 8 production variants to 1, cutting CI cost and the security-scan surface roughly proportionally. | ||
| - Extension updates become reviewable PRs with CI instead of silent build-time drift; the amqp/grpc patch stack disappears. | ||
| - glibc removes a whole class of musl-specific performance and compatibility issues. | ||
|
|
||
| ### Negative | ||
|
|
||
| - Users of the `fpm`, `caddy` and `nginx` variants must migrate to FrankenPHP (or stay on v1 until end of deprecation). This is the largest migration cost and needs a prominent migration guide. | ||
| - Debian-based images are somewhat larger than Alpine-based ones. | ||
| - Anyone relying on the image-baked Shopware env defaults must set them explicitly after upgrading; this must be called out in the migration guide with a complete list of removed variables. | ||
| - Maintaining rebuilds for the deprecated v1 during the transition window temporarily *increases* CI load before it decreases. | ||
| - Version pinning of extensions means we must keep the update automation healthy; a stalled bot would now mean stale extensions rather than (unnoticed) auto-updates. | ||
| - Users lagging on PHP upgrades lose rebuilds the day their PHP version goes EOL upstream — deliberate, but it will surprise anyone who assumed the calendar-version window shields them from PHP's own lifecycle. The published EOL table and upfront announcements are the mitigation. | ||
|
|
||
| ### Migration outline | ||
|
|
||
| 1. Introduce versioned tags for the **current** images (retroactively tag the existing setup as `v2025.x`) so the mechanism exists before the breaking change. | ||
| 2. Publish v2 images (`-v2026.1`) alongside v1. | ||
| 3. Announce deprecation of v1 variants with a fixed end date; add a startup deprecation notice to v1 images. | ||
| 4. After the window: point rolling tags at v2, stop rebuilding v1. | ||
|
|
||
| ## References | ||
|
|
||
| - Issue: [Docker Image Variant v2 (shopware/docker#150)](https://github.com/shopware/docker/issues/150) | ||
| - Versioning model: [pimcore/docker — Versioning](https://github.com/pimcore/docker#versioning) | ||
| - Extension installer and version pinning syntax: [mlocati/docker-php-extension-installer](https://github.com/mlocati/docker-php-extension-installer) | ||
| - musl vs glibc performance: [Chainguard — glibc vs. musl](https://edu.chainguard.dev/chainguard/chainguard-images/about/images-compiled-programs/glibc-vs-musl/), [TuxCare — musl vs glibc](https://tuxcare.com/blog/musl-vs-glibc/) | ||
| - Supply chain: [Docker — SBOM generation for container workflows](https://www.docker.com/blog/sbom-generation-for-container-workflows/), [Docker image security best practices (SBOM, non-root, provenance)](https://bell-sw.com/blog/docker-image-security-best-practices-for-production/) | ||
| - Digest pinning: [Chainguard — container image digests for reproducibility](https://edu.chainguard.dev/chainguard/chainguard-images/how-to-use/container-image-digests/) | ||
| - General build best practices: [Better Stack — Docker build best practices](https://betterstack.com/community/guides/scaling-docker/docker-build-best-practices/) | ||
Uh oh!
There was an error while loading. Please reload this page.