From 3471d4d0ff94414bf72e5345cfff6be96b6ad304 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sun, 2 Aug 2026 10:34:46 +0200 Subject: [PATCH 1/2] Build docker images (#99) --- .github/workflows/docker.yml | 391 +++++++++++++++++++++++++++++++++++ README.md | 10 + docker/Dockerfile.alpine | 76 +++++++ docker/Dockerfile.debian | 97 +++++++++ docker/README.md | 92 +++++++++ 5 files changed, 666 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 docker/Dockerfile.alpine create mode 100644 docker/Dockerfile.debian create mode 100644 docker/README.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..193eeb4b --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,391 @@ +name: Docker Images + +# Builds the phpdebugger/php base images (official php:* images with +# php-debugger statically compiled in) for every release, and weekly to pick +# up new PHP patch releases in the official images. + +on: + release: + types: [published] + # weekly rebuild of the latest release against current official images + schedule: + - cron: '30 4 * * 1' + # manual run; pushes to the -dev repo unless target is set to prod + workflow_dispatch: + inputs: + target: + description: "Docker Hub repo to push to" + type: choice + options: [dev, prod] + default: dev + +permissions: + contents: read + +# one run at a time per target repo: release, schedule and manual runs all +# push the same tags, so they must not interleave +concurrency: + group: docker-${{ (inputs.target == 'prod' || ((github.event_name == 'release' || github.event_name == 'schedule') && github.repository_owner == 'php-debugger')) && 'prod' || 'dev' }} + cancel-in-progress: false + +# --------------------------------------------------------------------------- +# Build matrix configuration +# --------------------------------------------------------------------------- +env: + # releases and the weekly schedule go to production — but only from the + # upstream repo, so fork releases used for testing stay on -dev; manual + # runs go to -dev unless explicitly dispatched with target=prod + IMAGE_REPO: ${{ (inputs.target == 'prod' || ((github.event_name == 'release' || github.event_name == 'schedule') && github.repository_owner == 'php-debugger')) && 'phpdebugger/php' || 'phpdebugger/php-dev' }} + + # supported PHP minor versions, oldest first: the last one is the newest + # stable and its cli image also gets the "latest" tag + PHP_VERSIONS: '["8.2", "8.3", "8.4", "8.5"]' + + # official image variants to mirror (apache only exists on Debian); + # family selects the Dockerfile (docker/Dockerfile.) + VARIANTS: | + [ + {"variant": "cli", "family": "debian"}, + {"variant": "fpm", "family": "debian"}, + {"variant": "apache", "family": "debian"}, + {"variant": "zts", "family": "debian"}, + {"variant": "cli-alpine", "family": "alpine"}, + {"variant": "fpm-alpine", "family": "alpine"}, + {"variant": "zts-alpine", "family": "alpine"} + ] + + # architectures and the native runners that build them (no QEMU) + ARCHS: | + [ + {"arch": "amd64", "runner": "ubuntu-latest"}, + {"arch": "arm64", "runner": "ubuntu-24.04-arm"} + ] + +jobs: + resolve: + runs-on: ubuntu-latest + outputs: + debugger-ref: ${{ steps.release.outputs.ref }} + debugger-sha: ${{ steps.release.outputs.sha }} + build-date: ${{ steps.release.outputs.build-date }} + variants: ${{ steps.matrix.outputs.variants }} + builds: ${{ steps.matrix.outputs.builds }} + latest-php: ${{ steps.matrix.outputs.latest-php }} + base-digests: ${{ steps.base-digests.outputs.digests }} + steps: + - name: Guard manual prod dispatch + if: github.event_name == 'workflow_dispatch' && inputs.target == 'prod' + run: | + # a manual dispatch with target=prod can run from any + # branch the dispatcher picks in the UI, using whatever + # workflow/Dockerfile is on it — unlike release/schedule + # runs, which are inherently tied to a published release. + # Restrict it to the upstream repo's main branch so a + # manual prod run can't publish phpdebugger/php tags + # built from an untrusted or modified branch. + if [ "${{ github.repository_owner }}" != "php-debugger" ] || [ "${{ github.ref }}" != "refs/heads/main" ]; then + echo "::error::Manual dispatch with target=prod is only allowed from php-debugger/php-debugger on refs/heads/main (got owner=${{ github.repository_owner }}, ref=${{ github.ref }})." + exit 1 + fi + + - name: Resolve release to build + id: release + env: + GH_TOKEN: ${{ github.token }} + EVENT_TAG: ${{ github.event.release.tag_name }} + run: | + # on a release event build that release; on schedule or + # manual dispatch fall back to the latest published release + REF="$EVENT_TAG" + if [ -z "$REF" ]; then + REF="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)" + fi + # Resolve to the commit the tag points at: passed to the + # build so a re-pointed tag busts the clone layer's cache + # and a mismatched clone fails loudly. + SHA="$(gh api "repos/$GITHUB_REPOSITORY/commits/$REF" --jq .sha)" + echo "ref=$REF" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "build-date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + echo "Building php-debugger $REF ($SHA)" + + - name: Compute build matrix + id: matrix + run: | + echo "latest-php=$(jq -rn --argjson v "$PHP_VERSIONS" '$v | last')" >> "$GITHUB_OUTPUT" + # PHP versions x variants + jq -cn --argjson versions "$PHP_VERSIONS" --argjson variants "$VARIANTS" \ + '[$versions[] as $php | $variants[] | {php: $php} + .]' > /tmp/variants.json + echo "variants=$(jq -c '{include: .}' /tmp/variants.json)" >> "$GITHUB_OUTPUT" + # ... x architectures + echo "builds=$(jq -c --argjson archs "$ARCHS" '{include: [.[] as $v | $archs[] | $v + .]}' /tmp/variants.json)" >> "$GITHUB_OUTPUT" + + - name: Resolve base image digests + id: base-digests + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + # pin every official php:- tag used by this + # run to a digest, once, up front. The build matrix spans + # dozens of jobs across two architectures; without this, + # each job's independent "FROM php:" pull could + # resolve to a different underlying image if Docker + # Official Images re-pushes the tag mid-run, splitting a + # single variant across PHP patch releases per + # architecture and undermining the all-or-nothing + # consistent tag set the merge job relies on. + # + # Queried directly against the registry API (a HEAD + # request for the Docker-Content-Digest header) instead + # of via `docker`/`buildx imagetools`: those go through + # the runner's local builder, which doesn't reliably + # inherit `docker login` credentials for metadata-only + # calls like this and silently falls back to anonymous + # access — which this workflow's concurrent CI load + # burns through Docker Hub's anonymous rate limit on + # almost immediately. + token="" + for attempt in 1 2 3 4 5; do + token="$(curl -fsS -u "$DOCKERHUB_USERNAME:$DOCKERHUB_TOKEN" \ + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/php:pull" \ + | jq -r .token)" && [ -n "$token" ] && [ "$token" != "null" ] && break + [ "$attempt" -lt 5 ] || { echo "::error::Failed to obtain a Docker Hub registry token" >&2; exit 1; } + sleep $((attempt * 10)) + done + + digests="{}" + while IFS= read -r combo; do + tag="$(jq -r '"\(.php)-\(.variant)"' <<<"$combo")" + digest="" + for attempt in 1 2 3 4 5; do + digest="$(curl -fsSI \ + -H "Authorization: Bearer $token" \ + -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" \ + "https://registry-1.docker.io/v2/library/php/manifests/$tag" \ + | tr -d '\r' | awk -F': ' 'tolower($1) == "docker-content-digest" {print $2}')" && [ -n "$digest" ] && break + [ "$attempt" -lt 5 ] || { echo "::error::Failed to resolve digest for php:$tag" >&2; exit 1; } + sleep $((attempt * 10)) + done + digests="$(jq --arg k "$tag" --arg v "$digest" '. + {($k): $v}' <<<"$digests")" + done < <(jq -c '.[]' /tmp/variants.json) + echo "digests=$(jq -c '.' <<<"$digests")" >> "$GITHUB_OUTPUT" + + build: + needs: resolve + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.resolve.outputs.builds) }} + name: build ${{ matrix.php }}-${{ matrix.variant }} ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: docker + file: docker/Dockerfile.${{ matrix.family }} + platforms: linux/${{ matrix.arch }} + # base digest was resolved once in the resolve job, so + # this always re-pulls that exact image rather than + # relying on a (possibly stale) local layer cache + pull: true + # no provenance attestations: keeps the manifest index to + # exactly one image per arch and halves registry requests + provenance: false + build-args: | + BASE_TAG=${{ matrix.php }}-${{ matrix.variant }}@${{ fromJSON(needs.resolve.outputs.base-digests)[format('{0}-{1}', matrix.php, matrix.variant)] }} + DEBUGGER_REPO=https://github.com/${{ github.repository }}.git + DEBUGGER_REF=${{ needs.resolve.outputs.debugger-ref }} + DEBUGGER_SHA=${{ needs.resolve.outputs.debugger-sha }} + # created/revision distinguish weekly rebuilds that carry + # the same debugger version but a newer PHP patch release + labels: | + org.opencontainers.image.version=${{ needs.resolve.outputs.debugger-ref }} + org.opencontainers.image.revision=${{ needs.resolve.outputs.debugger-sha }} + org.opencontainers.image.created=${{ needs.resolve.outputs.build-date }} + outputs: type=image,name=${{ env.IMAGE_REPO }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=${{ matrix.php }}-${{ matrix.variant }}-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=${{ matrix.php }}-${{ matrix.variant }}-${{ matrix.arch }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + # "--" delimiter keeps the merge job's download pattern from + # also matching sibling variants (e.g. cli vs cli-alpine) + name: digests-${{ matrix.php }}-${{ matrix.variant }}--${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + verify: + # smoke-test each digest-pushed image before any public tag is + # created, so a broken image never becomes reachable via + # phpdebugger/php:* — only images that pass this get tagged by merge + needs: [resolve, build] + strategy: + fail-fast: false + # keep bursts against the Docker Hub rate limit small + max-parallel: 8 + matrix: ${{ fromJSON(needs.resolve.outputs.builds) }} + name: verify ${{ matrix.php }}-${{ matrix.variant }} ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + steps: + - name: Download digest + uses: actions/download-artifact@v4 + with: + name: digests-${{ matrix.php }}-${{ matrix.variant }}--${{ matrix.arch }} + path: /tmp/digests + + # authenticated pulls: anonymous runners share IPs and hit Docker + # Hub's unauthenticated rate limit almost immediately + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Smoke test image + working-directory: /tmp/digests + env: + DEBUGGER_REF: ${{ needs.resolve.outputs.debugger-ref }} + run: | + set -eux + # pull by digest, not by tag: at this point no public tag + # exists yet for this image + IMAGE="$IMAGE_REPO@sha256:$(ls)" + # retry with backoff: transient registry 429s + for attempt in 1 2 3 4 5; do + docker pull --platform "linux/${{ matrix.arch }}" "$IMAGE" && break + [ "$attempt" -lt 5 ] || exit 1 + sleep $((attempt * 60)) + done + # the pulled image must match this runner's architecture + [ "$(docker image inspect --format '{{.Architecture}}' "$IMAGE")" = "${{ matrix.arch }}" ] + # the base distro must match the variant family + case "${{ matrix.family }}" in + debian) docker run --rm "$IMAGE" sh -c 'grep -qi "ID=debian" /etc/os-release' ;; + alpine) docker run --rm "$IMAGE" sh -c 'grep -qi "ID=alpine" /etc/os-release' ;; + esac + docker run --rm "$IMAGE" php -v + # tolerate release tags with or without a leading "v" + docker run --rm "$IMAGE" php -v | grep -F "PHP Debugger v${DEBUGGER_REF#v}" + docker run --rm "$IMAGE" php -m | grep -ix xdebug + docker run --rm "$IMAGE" php -r 'exit(extension_loaded("xdebug") ? 0 : 1);' + # the php_debugger.* function alias must resolve, not just + # the xdebug-compat surface + docker run --rm "$IMAGE" php -r 'exit(function_exists("php_debugger_info") ? 0 : 1);' + docker run --rm "$IMAGE" php -r 'php_debugger_info();' + # the image must still be usable as a base for further + # docker-php-ext-install layers, same as the official image + docker run --rm "$IMAGE" sh -c 'docker-php-ext-install -j$(nproc) bcmath >/dev/null && php -m | grep -qi bcmath' + + merge: + # deliberately waits for the whole build matrix and its verification: + # if any build or smoke test fails, no tag publish is even attempted, + # so the published tag set never has mixed debugger/PHP versions + # across variants, and never points at an image that failed + # verification. That guarantee covers build+verify only, though: the + # publish step below is 28 independent Docker Hub API calls (one per + # variant, each already retried with backoff), and Docker Hub has no + # atomic multi-tag update — a registry-side failure partway through + # can still leave some variants' tags updated and others not. + # fail-fast here stops queued variants from publishing once one + # fails, to shrink that window rather than push every remaining tag + # regardless. + needs: [resolve, build, verify] + strategy: + fail-fast: true + # keep bursts against the Docker Hub rate limit small + max-parallel: 8 + matrix: ${{ fromJSON(needs.resolve.outputs.variants) }} + name: merge ${{ matrix.php }}-${{ matrix.variant }} + runs-on: ubuntu-latest + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-${{ matrix.php }}-${{ matrix.variant }}--* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create multi-arch manifest + working-directory: /tmp/digests + env: + LATEST_PHP: ${{ needs.resolve.outputs.latest-php }} + run: | + # exactly one digest per architecture: a glob that swept up + # a sibling variant's artifacts would show up here + [ "$(ls | wc -l)" -eq "$(jq -rn --argjson a "$ARCHS" '$a | length')" ] + # alias tags mirroring the official image: "8.x" -> cli, + # "8.x-alpine" -> cli-alpine, "latest" -> newest stable cli + EXTRA_TAGS=() + case "${{ matrix.variant }}" in + cli) + EXTRA_TAGS+=(--tag "$IMAGE_REPO:${{ matrix.php }}") + if [ "${{ matrix.php }}" = "$LATEST_PHP" ]; then + EXTRA_TAGS+=(--tag "$IMAGE_REPO:latest") + fi + ;; + cli-alpine) + EXTRA_TAGS+=(--tag "$IMAGE_REPO:${{ matrix.php }}-alpine") + ;; + esac + # retry with backoff: transient 429s from the registry are + # expected when many jobs talk to Docker Hub at once + for attempt in 1 2 3 4 5; do + if docker buildx imagetools create \ + --tag "$IMAGE_REPO:${{ matrix.php }}-${{ matrix.variant }}" \ + "${EXTRA_TAGS[@]}" \ + $(printf "$IMAGE_REPO@sha256:%s " *); then + break + fi + [ "$attempt" -lt 5 ] || exit 1 + sleep $((attempt * 60)) + done + + # keep the Docker Hub repo description (of whichever repo this run + # pushed to) in sync with docker/README.md, after a fully verified, + # fully tagged run + description: + needs: merge + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Update Docker Hub description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ env.IMAGE_REPO }} + short-description: "Official PHP images with the PHP Debugger extension statically compiled in" + readme-filepath: docker/README.md diff --git a/README.md b/README.md index 3230b80f..40227d68 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,16 @@ For this reason, we recommend enabling this setting only if you specifically req ## Installation +### Docker + +Drop-in replacements for the [official PHP images](https://hub.docker.com/_/php) with PHP Debugger statically compiled in — just add the `phpdebugger/` prefix to your base image: + +```dockerfile +FROM phpdebugger/php:8.4-fpm +``` + +All official variants are available (`cli`, `fpm`, `apache`, `zts`, and their Alpine equivalents) for PHP 8.2–8.5, on amd64 and arm64. Tags exist per minor version only (no `8.4.23`); each always contains the latest patch release. Everything from the official images works unchanged, including `docker-php-ext-install`. See [docker/README.md](docker/README.md) for tags and debugging setup. + ### Manual download Grab the right binary from [Releases](https://github.com/php-debugger/php-debugger/releases), copy it to your extension directory, and add to `php.ini`: diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine new file mode 100644 index 00000000..cd9f968d --- /dev/null +++ b/docker/Dockerfile.alpine @@ -0,0 +1,76 @@ +# Rebuilds the official php: Alpine image with php-debugger +# statically compiled into the interpreter. Same approach as +# Dockerfile.debian; see that file for the full rationale. +# +# Usage: +# docker build -f Dockerfile.alpine --build-arg BASE_TAG=8.4-fpm-alpine --build-arg DEBUGGER_REF=0.1.0 . + +ARG BASE_TAG + +FROM php:${BASE_TAG} AS builder + +ARG DEBUGGER_REF +ARG DEBUGGER_REPO=https://github.com/php-debugger/php-debugger.git + +# Unlike the Debian variants, Alpine images do not keep the toolchain +# installed ($PHPIZE_DEPS is only an env var there), so install it along +# with git, linux-headers (php-debugger needs linux/rtnetlink.h on musl) +# and the -dev packages matching the official image's configure options. +RUN set -eux; \ + apk add --no-cache \ + $PHPIZE_DEPS \ + git \ + linux-headers \ + argon2-dev \ + curl-dev \ + gnu-libiconv-dev \ + libedit-dev \ + libsodium-dev \ + libxml2-dev \ + oniguruma-dev \ + openssl-dev \ + readline-dev \ + sqlite-dev \ + zlib-dev + +# DEBUGGER_SHA is optional: when set (CI passes the commit the release tag +# points at) it busts this layer's build cache if the tag is ever re-pointed, +# and fails the build if the clone doesn't match the expected commit. +ARG DEBUGGER_SHA= + +RUN set -eux; \ + docker-php-source extract; \ + git clone --depth 1 --branch "${DEBUGGER_REF}" "${DEBUGGER_REPO}" /usr/src/php/ext/php_debugger; \ + if [ -n "${DEBUGGER_SHA}" ]; then \ + [ "$(git -C /usr/src/php/ext/php_debugger rev-parse HEAD)" = "${DEBUGGER_SHA}" ]; \ + fi; \ + mkdir -p /usr/src/php/m4; \ + cp /usr/src/php/ext/php_debugger/m4/*.m4 /usr/src/php/m4/ + +RUN set -eux; \ + cd /usr/src/php; \ + ./buildconf --force; \ + export CFLAGS="$PHP_CFLAGS" CPPFLAGS="$PHP_CPPFLAGS" LDFLAGS="$PHP_LDFLAGS"; \ + configureCommand="$(php -i | sed -n 's/^Configure Command => *//p' | head -n 1)"; \ + eval "$configureCommand" \ + --enable-php-debugger \ + --disable-opcache-jit; \ + make -j"$(nproc)"; \ + make install; \ + php -v; \ + php -m | grep -ix xdebug; \ + php -r 'exit(extension_loaded("xdebug") ? 0 : 1);' + +FROM php:${BASE_TAG} + +ARG DEBUGGER_REF + +LABEL org.opencontainers.image.source="https://github.com/php-debugger/php-debugger" \ + org.opencontainers.image.description="Official PHP image with php-debugger statically compiled into the interpreter" \ + org.opencontainers.image.version="${DEBUGGER_REF}" + +COPY --from=builder /usr/local /usr/local + +RUN set -eux; \ + php -v; \ + php -m | grep -ix xdebug diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian new file mode 100644 index 00000000..fe7a2e36 --- /dev/null +++ b/docker/Dockerfile.debian @@ -0,0 +1,97 @@ +# Rebuilds the official php: image with php-debugger statically +# compiled into the interpreter. The official image ships its own source +# (docker-php-source) and records its full configure command (the +# "Configure Command" line in php -i), so we re-run the exact same build +# with the extension added and opcache JIT disabled (JIT is incompatible +# with php-debugger). +# +# Usage: +# docker build -f Dockerfile.debian --build-arg BASE_TAG=8.4-cli --build-arg DEBUGGER_REF=0.1.0 . + +ARG BASE_TAG + +FROM php:${BASE_TAG} AS builder + +ARG DEBUGGER_REF +ARG DEBUGGER_REPO=https://github.com/php-debugger/php-debugger.git + +# The toolchain ($PHPIZE_DEPS: autoconf, gcc, make, re2c, ...) is +# preinstalled on Debian variants but listed anyway in case upstream ever +# makes it transient; git is needed for the clone and the -dev packages +# match the official image's configure options (the official build purges +# these after compiling, keeping just the runtime libs). bison is not +# needed: the bundled source tarball ships pre-generated parsers. +# apache2-dev only applies when the base was built --with-apxs2. +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + $PHPIZE_DEPS \ + git \ + libargon2-dev \ + libcurl4-openssl-dev \ + libedit-dev \ + libonig-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + zlib1g-dev \ + ; \ + if php-config --configure-options | grep -q -- --with-apxs2; then \ + apt-get install -y --no-install-recommends apache2-dev; \ + fi; \ + rm -rf /var/lib/apt/lists/* + +# DEBUGGER_SHA is optional: when set (CI passes the commit the release tag +# points at) it busts this layer's build cache if the tag is ever re-pointed, +# and fails the build if the clone doesn't match the expected commit. +ARG DEBUGGER_SHA= + +RUN set -eux; \ + docker-php-source extract; \ + git clone --depth 1 --branch "${DEBUGGER_REF}" "${DEBUGGER_REPO}" /usr/src/php/ext/php_debugger; \ + if [ -n "${DEBUGGER_SHA}" ]; then \ + [ "$(git -C /usr/src/php/ext/php_debugger rev-parse HEAD)" = "${DEBUGGER_SHA}" ]; \ + fi; \ + mkdir -p /usr/src/php/m4; \ + cp /usr/src/php/ext/php_debugger/m4/*.m4 /usr/src/php/m4/ + +RUN set -eux; \ + cd /usr/src/php; \ + ./buildconf --force; \ + export CFLAGS="$PHP_CFLAGS" CPPFLAGS="$PHP_CPPFLAGS" LDFLAGS="$PHP_LDFLAGS"; \ + configureCommand="$(php -i | sed -n 's/^Configure Command => *//p' | head -n 1)"; \ + eval "$configureCommand" \ + --enable-php-debugger \ + --disable-opcache-jit; \ + make -j"$(nproc)"; \ + make install; \ + # the apache SAPI installs libphp outside /usr/local — stage it so the + # final image can pick it up (empty dir for the other variants) + mkdir -p /opt/php-debugger/apache2-modules; \ + if [ -d /usr/lib/apache2/modules ]; then \ + cp -a /usr/lib/apache2/modules/libphp* /opt/php-debugger/apache2-modules/; \ + fi; \ + php -v; \ + php -m | grep -ix xdebug; \ + php -r 'exit(extension_loaded("xdebug") ? 0 : 1);' + +FROM php:${BASE_TAG} + +ARG DEBUGGER_REF + +LABEL org.opencontainers.image.source="https://github.com/php-debugger/php-debugger" \ + org.opencontainers.image.description="Official PHP image with php-debugger statically compiled into the interpreter" \ + org.opencontainers.image.version="${DEBUGGER_REF}" + +COPY --from=builder /usr/local /usr/local +COPY --from=builder /opt/php-debugger/apache2-modules /opt/php-debugger/apache2-modules + +RUN set -eux; \ + if [ -d /usr/lib/apache2/modules ]; then \ + cp -a /opt/php-debugger/apache2-modules/. /usr/lib/apache2/modules/; \ + fi; \ + rm -rf /opt/php-debugger; \ + php -v; \ + php -m | grep -ix xdebug diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..5cd519ad --- /dev/null +++ b/docker/README.md @@ -0,0 +1,92 @@ +# PHP Debugger Docker Images + +Drop-in replacements for the [official PHP images](https://hub.docker.com/_/php) with [PHP Debugger](https://github.com/php-debugger/php-debugger) statically compiled into the interpreter. Change one line and you have a debug-ready container with near-zero overhead when no debug client is connected: + +```dockerfile +# before +FROM php:8.4-fpm + +# after +FROM phpdebugger/php:8.4-fpm +``` + +No `pecl install`, no `docker-php-ext-enable`, no separate dev Dockerfile. The debugger is always there and costs almost nothing until an IDE connects. + +## ⚠️ Development use only + +These images are meant for development environments and should **not** be used in production. A debugger gives whoever connects to it full access to your source code, variables and runtime data — with debugging active by default, a reachable production container is a serious security risk. Keep your production images on the official `php:` base and use these only where the debugger is wanted. + +## Supported tags + +Images are built for PHP **8.2, 8.3, 8.4 and 8.5**, for `linux/amd64` and `linux/arm64`, mirroring the official image variants: + +| Tag | Distro | +|---|---| +| `8.x-cli` (also `8.x`) | Debian | +| `8.x-fpm` | Debian | +| `8.x-apache` | Debian | +| `8.x-zts` | Debian | +| `8.x-cli-alpine` (also `8.x-alpine`) | Alpine | +| `8.x-fpm-alpine` | Alpine | +| `8.x-zts-alpine` | Alpine | +| `latest` | newest stable PHP, cli variant | + +Each tag matches the official `php:` tag of the same name. + +There are no patch-level tags (`8.4.23`): each tag always contains the latest patch release of its PHP minor version. Images are rebuilt on every PHP Debugger release and weekly, so they track new PHP patch releases within a few days. + +## Usage + +Everything from the official images works unchanged — same entrypoints, same helper scripts, same config layout: + +```dockerfile +FROM phpdebugger/php:8.4-fpm + +RUN docker-php-ext-install -j$(nproc) pdo_mysql bcmath +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer +``` + +The only difference from the official image: PHP Debugger is compiled in (as a static extension — it does not appear in `conf.d/` and cannot be uninstalled), and opcache's JIT compiler is disabled, since it is incompatible with the debugger's engine hooks. + +> [!WARNING] +> The JIT compiler is disabled only in the opcache build that ships with the image. If you rebuild opcache yourself (`docker-php-ext-install opcache`), pass `--disable-opcache-jit` to `docker-php-ext-configure` first, or JIT will be compiled back in: +> ```dockerfile +> RUN docker-php-ext-configure opcache --disable-opcache-jit \ +> && docker-php-ext-install -j$(nproc) opcache +> ``` + +### Debugging + +The debugger speaks the DBGp protocol, so PhpStorm, VS Code and any Xdebug-compatible client work as-is (the extension identifies itself as `xdebug` for compatibility). Typical development setup in `docker-compose.yml`: + +```yaml +services: + app: + image: phpdebugger/php:8.4-fpm + environment: + XDEBUG_CONFIG: "client_host=host.docker.internal" + PHP_IDE_CONFIG: "serverName=myapp" + extra_hosts: + - "host.docker.internal:host-gateway" # needed on Linux +``` + +No INI configuration is needed: the debugger is active by default and connects to your IDE whenever it is listening, at near-zero cost when it isn't. If you do need to change settings, both `xdebug.*` and `php_debugger.*` INI prefixes are accepted. + +**Migrating from an Xdebug-based image?** Remove your old Xdebug configuration — with this image the defaults do the right thing: + +- remove the line that loads the extension (`zend_extension=xdebug.so` or the `docker-php-ext-enable xdebug` step) — the debugger is compiled in; +- remove any line that sets `xdebug.mode` — debugging is on by default; +- remove any line that sets `xdebug.start_with_request` — the debugger starts with every request by default. + +### Verifying + +```console +$ docker run --rm phpdebugger/php:8.4-cli php -v +PHP 8.4.x (cli) ... (NTS) + with Zend OPcache ... + with PHP Debugger vX.Y.Z, Copyright (c) 2002-2026, by Derick Rethans +``` + +## License + +PHP Debugger is licensed under [The Xdebug License, version 1.03](https://github.com/php-debugger/php-debugger/blob/main/LICENSE). PHP itself is distributed under the [PHP License](https://www.php.net/license/). From 86ef2feb7989e513b0cffd0ea9da7713822fb969 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Sun, 2 Aug 2026 11:13:43 +0200 Subject: [PATCH 2/2] chore: update version number (#101) --- php_xdebug.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php_xdebug.h b/php_xdebug.h index 3deb0568..a3b44a39 100644 --- a/php_xdebug.h +++ b/php_xdebug.h @@ -18,7 +18,7 @@ #define PHP_XDEBUG_H #define XDEBUG_NAME "PHP Debugger" -#define XDEBUG_VERSION "0.1.0" +#define XDEBUG_VERSION "0.2.0" #define XDEBUG_AUTHOR "Derick Rethans" #define XDEBUG_COPYRIGHT "Copyright (c) 2002-2026 by Derick Rethans" #define XDEBUG_COPYRIGHT_SHORT "Copyright (c) 2002-2026"