Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions .github/workflows/build-v2026.1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
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

permissions:
contents: read

jobs:
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

- name: Get all changed files
id: changed-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v46
with:
files: |
v2026.1/**
.github/workflows/build-v2026.1.yml

- 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

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<<EOF"
echo "shopware/docker-base"
echo "ghcr.io/shopware/docker-base"
echo "EOF"
} >> "$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(?<v>[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(":(?<v>[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: [prepare, frankenphp]
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Determine image tag
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
run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }}

- name: grpc and opentelemetry are shipped but not loaded
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
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
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)
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 ${{ 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
runs-on: ubuntu-latest
needs: [dev]
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install container-structure-test
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
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
run: docker pull ${{ steps.image-tag.outputs.IMAGE_TAG }}

- name: Test Dev Image
run: container-structure-test test --config v2026.1/dev/config.yaml --image ${{ steps.image-tag.outputs.IMAGE_TAG }}
2 changes: 2 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:<php>-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:<php>-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
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<name>`.

### v2 tags

| Image | Tags |
|---|---|
| Base | `ghcr.io/shopware/docker-base:<php>-frankenphp-v2026.1`, also on Docker Hub as `shopware/docker-base` |
| Dev | `ghcr.io/shopware/docker-dev:<php>-node<22\|24>-v2026.1` |

`<php>` 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.
Loading