From 00f2c7b3efa659895e5645cea3843c00aab5abee Mon Sep 17 00:00:00 2001 From: Tom Schlick Date: Wed, 21 Oct 2026 18:31:42 -0400 Subject: [PATCH 1/3] Fix docker-php-serversideup-download printing shell options to stdout "set -oe" makes the shell print its option table to stdout before it enables errexit. Downloads to a file were unaffected, but stdout downloads got the option table prepended to the body. The two existing callers happened to filter it out. --- src/common/usr/local/bin/docker-php-serversideup-download | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/usr/local/bin/docker-php-serversideup-download b/src/common/usr/local/bin/docker-php-serversideup-download index e587bc8f..38a180ce 100755 --- a/src/common/usr/local/bin/docker-php-serversideup-download +++ b/src/common/usr/local/bin/docker-php-serversideup-download @@ -1,5 +1,5 @@ #!/bin/sh -set -oe +set -e ################################################### # Usage: docker-php-serversideup-download [output-file] From 551ade9bc21408f5dbf1ec194dc00a4458cd6409 Mon Sep 17 00:00:00 2001 From: Tom Schlick Date: Wed, 21 Oct 2026 18:31:42 -0400 Subject: [PATCH 2/3] Add serversideup/php-base, PHP compiled from source src/base/Dockerfile ports docker-library/php's Dockerfile-linux.template (pinned in src/base/bin/UPSTREAM) into one Dockerfile that builds any supported minor, SAPI (cli, fpm, zts), and OS. The result is meant to be interchangeable with the official php:-- image. - scripts/get-php-source.sh resolves a version to its php.net tarball, sha256, and release manager GPG keys (new gpg_keys per minor in the base config; base_image per OS says what to build on). - scripts/dev.sh --variation base --sapi builds one locally. - scripts/test-base-image.sh checks a base image against the official one: ENV, modules, configure flags, /usr/local/etc, www-data, and FPM. - cli, fpm, and fpm-nginx now take BASE_IMAGE like fpm-apache and frankenphp already do. The default is still the official image, so CI builds are unchanged. - hadolint covers src/base/Dockerfile. --- .github/workflows/service_lint.yml | 2 +- .hadolint.yaml | 2 +- AGENTS.md | 13 +- .../docs/1.getting-started/99.contributing.md | 2 +- scripts/conf/php-versions-base-config.yml | 34 ++ scripts/dev.sh | 63 ++- scripts/get-php-source.sh | 88 ++++ scripts/get-php-versions.sh | 3 +- scripts/test-base-image.sh | 297 +++++++++++ src/base/Dockerfile | 494 ++++++++++++++++++ src/base/LICENSE-docker-library-php | 20 + src/base/bin/UPSTREAM | 20 + src/base/bin/docker-php-entrypoint | 9 + src/base/bin/docker-php-ext-configure | 69 +++ src/base/bin/docker-php-ext-enable | 121 +++++ src/base/bin/docker-php-ext-install | 143 +++++ src/base/bin/docker-php-source | 34 ++ src/variations/cli/Dockerfile | 3 +- src/variations/fpm-nginx/Dockerfile | 2 +- src/variations/fpm/Dockerfile | 3 +- 20 files changed, 1412 insertions(+), 10 deletions(-) create mode 100755 scripts/get-php-source.sh create mode 100755 scripts/test-base-image.sh create mode 100644 src/base/Dockerfile create mode 100644 src/base/LICENSE-docker-library-php create mode 100644 src/base/bin/UPSTREAM create mode 100755 src/base/bin/docker-php-entrypoint create mode 100755 src/base/bin/docker-php-ext-configure create mode 100755 src/base/bin/docker-php-ext-enable create mode 100755 src/base/bin/docker-php-ext-install create mode 100755 src/base/bin/docker-php-source diff --git a/.github/workflows/service_lint.yml b/.github/workflows/service_lint.yml index b385cfeb..44102db6 100644 --- a/.github/workflows/service_lint.yml +++ b/.github/workflows/service_lint.yml @@ -55,7 +55,7 @@ jobs: uses: actions/checkout@v7 - name: Lint Dockerfiles - run: docker run --rm -v "$PWD:/repo" -w /repo "hadolint/hadolint:${HADOLINT_VERSION}" hadolint src/variations/*/Dockerfile .oss-scanner/Dockerfile + run: docker run --rm -v "$PWD:/repo" -w /repo "hadolint/hadolint:${HADOLINT_VERSION}" hadolint src/base/Dockerfile src/variations/*/Dockerfile .oss-scanner/Dockerfile script-tests: name: scripts diff --git a/.hadolint.yaml b/.hadolint.yaml index 2dbf9e72..6b61e7f2 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -1,4 +1,4 @@ -# hadolint configuration for src/variations/*/Dockerfile +# hadolint configuration for src/base/Dockerfile, src/variations/*/Dockerfile, and .oss-scanner/Dockerfile # Each ignored rule is a deliberate project decision, not an oversight. ignored: - DL3008 # apt packages are intentionally unpinned: weekly rebuilds pick up security updates diff --git a/AGENTS.md b/AGENTS.md index 3a94d132..a76b7874 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ This project maintains open source PHP Docker images (`serversideup/php`) for La ``` src/ + base/ # serversideup/php-base: PHP compiled from source, a drop-in for the official php images variations/ # One Dockerfile per image variation (cli, fpm, fpm-apache, fpm-nginx, frankenphp) common/ # Shared scripts and configs copied into ALL variations usr/local/bin/ # Entrypoint and helper scripts (POSIX /bin/sh) @@ -15,6 +16,7 @@ src/ utilities-webservers/ # Shared web server entrypoint utilities (SSL, etc.) scripts/ # Build tooling (Bash, not POSIX) dev.sh # Local image builds + get-php-source.sh # Resolves one PHP version to its php.net tarball, sha256, and GPG keys conf/ # PHP version matrix and base config (YAML) generate-matrix.sh # Generates CI build matrix from YAML config assemble-docker-tags.sh @@ -54,8 +56,17 @@ Each variation Dockerfile uses multi-stage builds: 2. Variation-specific configs live in `src/variations//etc/` 3. The build context is the **project root** (not `src/`), so COPY paths are relative to root: `COPY --chmod=755 src/common/ /` +### Base image (`src/base/`) +`src/base/Dockerfile` compiles PHP from the php.net tarball into `serversideup/php-base:--`, an image meant to be interchangeable with the official `php:--`: same paths, ENV, `docker-php-ext-*` scripts, configure flags, and `/usr/local/etc` contents. It is a port of docker-library/php's `Dockerfile-linux.template`, pinned to the commit in `src/base/bin/UPSTREAM`; when upstream changes how the official images are built, port the change here too. One Dockerfile covers every PHP minor, SAPI, and OS: the per-minor differences are computed from `PHP_VERSION` inside the RUN steps. + +- The tarball URL, sha256, and signing keys come from `scripts/get-php-source.sh`, which reads only php.net plus the `gpg_keys` of each minor in the base config. Add a new minor's release manager keys (https://www.php.net/gpg-keys.php) when you add the minor. +- The OS image to build on is `base_image` on each OS entry in the base config. +- Build one locally with `scripts/dev.sh --variation base --version 8.4 --os bookworm --sapi fpm`, then check it against the official image with `scripts/test-base-image.sh serversideup/php-base:8.4-fpm-bookworm 8.4.27 php:8.4.27-fpm-bookworm`. Any difference from the official image other than `PHP_BUILD_PROVIDER` is a bug. +- Variations take the image to build on as `BASE_IMAGE` (default: the official image), so `scripts/dev.sh --variation fpm-nginx --version 8.4 --os bookworm --build-arg BASE_IMAGE=serversideup/php-base:8.4-fpm-bookworm` builds a variation on a local base. + ### Build args used across Dockerfiles - `PHP_VERSION`, `BASE_OS_VERSION`, `PHP_VARIATION` -- set by CI matrix or `scripts/dev.sh` +- `BASE_IMAGE` -- the PHP image a variation builds on; defaults to the official `php:` image - `NGINX_VERSION` -- resolved per-OS from `scripts/conf/php-versions-base-config.yml` - `REPOSITORY_BUILD_VERSION` -- image version label @@ -108,7 +119,7 @@ When modifying the version pipeline, the base config (`php-versions-base-config. - a new way for untrusted input to reach the image (a web server location, a healthcheck, a workflow trigger), a change to `SECURITY.md` scope, or a new intentional default the scanner should not report: `.oss-scanner/threat_model.md` After changing any of these, build it with `docker build -f .oss-scanner/Dockerfile -t oss-scanner .`, then confirm the affected variations start with no network: `docker run --rm -it --privileged --network none --user root oss-scanner bash`, then `.oss-scanner/run-variation &` and `curl localhost:8080/healthcheck`. -- Every PR run starts with `service_lint.yml` (script tests, actionlint for workflows, hadolint for `src/variations/*/Dockerfile` and `.oss-scanner/Dockerfile` with project decisions recorded in `.hadolint.yaml`, ShellCheck at warning severity for `scripts/*.sh`, `src/common/usr/local/bin/*`, `src/s6/usr/local/bin/*`, `.oss-scanner/run-variation`, and `entrypoint.d/*.sh`) before any image builds. New linters go there. The local commands are in the contributing docs. +- Every PR run starts with `service_lint.yml` (script tests, actionlint for workflows, hadolint for `src/base/Dockerfile`, `src/variations/*/Dockerfile`, and `.oss-scanner/Dockerfile` with project decisions recorded in `.hadolint.yaml`, ShellCheck at warning severity for `scripts/*.sh`, `src/common/usr/local/bin/*`, `src/s6/usr/local/bin/*`, `.oss-scanner/run-variation`, and `entrypoint.d/*.sh`) before any image builds. New linters go there. The local commands are in the contributing docs. - `scripts/test-image.sh` checks that an image starts, runs as an unprivileged user, reports the expected PHP version, loads the default extensions, reaches a healthy HEALTHCHECK, and (for images with a web server) serves a mounted `index.php` through NGINX, Apache, or Caddy. Run it locally against any image before opening a PR that touches startup behavior. Keep the checks generic across variations: it detects the web server from the image's `*_HTTP_PORT` environment variables rather than the image name. - Every build job records its image details (tags, Depot Registry reference, build ID, digest) as an `image-details-*` artifact; the publish job adds a `.published.json` record with the compressed size per architecture. `service_report.yml` merges them with `scripts/build-summary.sh` (the published record wins) into one table on the run summary and in the PR comment. - `fail-fast` is off, so one failed image never cancels the others. `trigger_auto-retry-failed-builds.yml` re-runs the failed jobs of a production, beta, or PR run once when only a few jobs failed and no newer run exists. If that also fails, or too many jobs failed to look transient, it opens a `ci-failure` issue assigned to the maintainer with the failed jobs, their first error annotation, the log tail, and a link to re-run. Only production and beta runs open issues; PR runs have the PR comment. GitHub's own failure emails do not cover this case reliably: scheduled runs notify whoever last edited the cron line, and re-run attempts are triggered by the Actions bot. diff --git a/docs/content/docs/1.getting-started/99.contributing.md b/docs/content/docs/1.getting-started/99.contributing.md index e82a000b..b2cb1782 100644 --- a/docs/content/docs/1.getting-started/99.contributing.md +++ b/docs/content/docs/1.getting-started/99.contributing.md @@ -77,7 +77,7 @@ docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color docker run --rm -v "$PWD:/repo" -w /repo koalaman/shellcheck:stable --severity=warning \ scripts/*.sh scripts/tests/*.sh src/common/usr/local/bin/* src/s6/usr/local/bin/* .oss-scanner/run-variation $(find src -path '*entrypoint.d*' -name '*.sh') -docker run --rm -v "$PWD:/repo" -w /repo hadolint/hadolint:latest hadolint src/variations/*/Dockerfile .oss-scanner/Dockerfile +docker run --rm -v "$PWD:/repo" -w /repo hadolint/hadolint:latest hadolint src/base/Dockerfile src/variations/*/Dockerfile .oss-scanner/Dockerfile bash scripts/tests/run.sh ``` diff --git a/scripts/conf/php-versions-base-config.yml b/scripts/conf/php-versions-base-config.yml index f160a263..e63b7676 100644 --- a/scripts/conf/php-versions-base-config.yml +++ b/scripts/conf/php-versions-base-config.yml @@ -12,6 +12,10 @@ php_variations: - "8.1" - "8.2" +# gpg_keys are the release managers who sign each minor's tarballs, from +# https://www.php.net/gpg-keys.php (the same table docker-library/php keeps). The +# base image (src/base/Dockerfile) refuses a tarball whose signature does not +# verify against one of them. php_versions: - major: "8" minor_versions: @@ -24,6 +28,10 @@ php_versions: - name: alpine3.22 - name: bookworm - name: trixie + gpg_keys: + - 528995BFEDFBA7191D46839EF9BA0ADA31CBD89E # patrickallaert + - 39B641343D8C104B2B146DC3F9C39DC0B9698544 # ramsey + - F1F692238FBC1666E5A5CCD4199F9DFEF6FFBAFD # krakjoe patch_versions: - 8.1.34 - minor: "8.2" @@ -32,6 +40,10 @@ php_versions: - name: alpine3.24 - name: bookworm - name: trixie + gpg_keys: + - 39B641343D8C104B2B146DC3F9C39DC0B9698544 # ramsey + - E60913E4DF209907D8E30D96659A97C9CF2A795A # sergey + - 1198C0117593497A5EC5C199286AF1F9897469DC # pierrick patch_versions: # - 8.2.32 # Pull latest from Official PHP source - minor: "8.3" @@ -40,6 +52,10 @@ php_versions: - name: alpine3.24 - name: bookworm - name: trixie + gpg_keys: + - 1198C0117593497A5EC5C199286AF1F9897469DC # pierrick + - C28D937575603EB4ABB725861C0779DC5C0A9DE4 # bukka + - AFD8691FDAEDF03BDF6E460563F15A9B715376CA # ericmann patch_versions: # - 8.3.32 # Pull latest from Official PHP source - minor: "8.4" @@ -48,6 +64,10 @@ php_versions: - name: alpine3.24 - name: bookworm - name: trixie + gpg_keys: + - AFD8691FDAEDF03BDF6E460563F15A9B715376CA # ericmann + - 9D7F99A0CB8F05C8A6958D6256A97AF7600A39A6 # calvinb + - 0616E93D95AF471243E26761770426E17EBBB3DD # saki patch_versions: # - 8.4.23 # Pull latest from Official PHP source - minor: "8.5" @@ -56,6 +76,10 @@ php_versions: - name: alpine3.24 - name: bookworm - name: trixie + gpg_keys: + - 1198C0117593497A5EC5C199286AF1F9897469DC # pierrick + - 49D9AF6BC72A80D6691719C8AA23F5BE9C7097D4 # edorian + - D95C03BC702BE9515344AE3374E44BC9067701A5 # daniels patch_versions: # - 8.5.8 # Pull latest from Official PHP source # PHP 8.6 is blocked upstream: the official php:8.6-rc images no longer ship pecl/pear, @@ -73,21 +97,29 @@ php_versions: # - 8.6-rc # php_extension_overrides: # - redis=phpredis/phpredis@develop + # gpg_keys: + # - D95C03BC702BE9515344AE3374E44BC9067701A5 # daniels + # - 016895DE9A475111D537A6E69134FF30BC5A99B5 # mbeccati + # - 5CFF17B64DC1C244F5D0EAC3E43535E2EB19010E # svpernova09 +# base_image is what src/base/Dockerfile builds PHP on for that OS. operating_systems: - family: alpine versions: - name: "Alpine 3.22" version: alpine3.22 number: 3.22 + base_image: alpine:3.22 nginx_version: 1.30.6-r1 - name: "Alpine 3.23" version: alpine3.23 number: 3.23 + base_image: alpine:3.23 nginx_version: 1.30.6-r1 - name: "Alpine 3.24" version: alpine3.24 number: 3.24 + base_image: alpine:3.24 nginx_version: 1.30.6-r1 - family: debian default: true @@ -95,8 +127,10 @@ operating_systems: - name: "Debian Bookworm" version: bookworm number: 12 + base_image: debian:bookworm-slim nginx_version: 1.30.6-1~bookworm - name: "Debian Trixie" version: trixie number: 13 + base_image: debian:trixie-slim nginx_version: 1.30.6-1~trixie diff --git a/scripts/dev.sh b/scripts/dev.sh index 9a2a3d40..3242ac8b 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -24,7 +24,9 @@ PHP_BUILD_VERSION="" PHP_BUILD_VARIATION="" PHP_BUILD_BASE_OS="" PHP_BUILD_PREFIX="" +PHP_BUILD_SAPI="" DOCKER_REPOSITORY="${DOCKER_REPOSITORY:-"serversideup/php"}" +PHP_BASE_REPOSITORY="${PHP_BASE_REPOSITORY:-"serversideup/php-base"}" DOCKER_ADDITIONAL_BUILD_ARGS=() PUSH_TO_REGISTRY=false PLATFORM="" @@ -126,7 +128,44 @@ build_docker_image() { --file "$PROJECT_ROOT_DIR/src/variations/$PHP_BUILD_VARIATION/Dockerfile" \ "$PROJECT_ROOT_DIR" echo_color_message green "โœ… Docker Image Built: $build_tag" - + push_docker_image "$build_tag" +} + +build_base_image() { + build_tag="${PHP_BASE_REPOSITORY}:${PHP_BUILD_PREFIX}${PHP_BUILD_VERSION}-${PHP_BUILD_SAPI}-${PHP_BUILD_BASE_OS}" + + local base_image source + base_image=$(BASE_OS="$PHP_BUILD_BASE_OS" yq -r '.operating_systems[].versions[] | select(.version == env(BASE_OS)) | .base_image // ""' "$BASE_PHP_VERSIONS_CONFIG_FILE") + if [ -z "$base_image" ]; then + echo_color_message red "โŒ No base_image for OS '$PHP_BUILD_BASE_OS' in $BASE_PHP_VERSIONS_CONFIG_FILE" + exit 1 + fi + source=$("$SCRIPT_DIR/get-php-source.sh" --config "$BASE_PHP_VERSIONS_CONFIG_FILE" "$PHP_BUILD_VERSION") + echo_color_message yellow "๐Ÿณ Building PHP $(jq -r '.php_version' <<< "$source") from source on $base_image: $build_tag" + + if [ -z "$PLATFORM" ]; then + PLATFORM=$(detect_platform) + fi + + docker buildx build \ + ${DOCKER_ADDITIONAL_BUILD_ARGS[@]+"${DOCKER_ADDITIONAL_BUILD_ARGS[@]}"} \ + --platform "$PLATFORM" \ + --build-arg BASE_IMAGE="$base_image" \ + --build-arg PHP_SAPI="$PHP_BUILD_SAPI" \ + --build-arg PHP_VERSION="$(jq -r '.php_version' <<< "$source")" \ + --build-arg PHP_URL="$(jq -r '.url' <<< "$source")" \ + --build-arg PHP_ASC_URL="$(jq -r '.asc_url' <<< "$source")" \ + --build-arg PHP_SHA256="$(jq -r '.sha256' <<< "$source")" \ + --build-arg GPG_KEYS="$(jq -r '.gpg_keys' <<< "$source")" \ + --tag "$build_tag" \ + --file "$PROJECT_ROOT_DIR/src/base/Dockerfile" \ + "$PROJECT_ROOT_DIR" + echo_color_message green "โœ… Docker Image Built: $build_tag" + push_docker_image "$build_tag" +} + +push_docker_image() { + local build_tag=$1 if [ "$PUSH_TO_REGISTRY" = true ]; then echo_color_message yellow "๐Ÿš€ Pushing image to registry: $build_tag" docker push "$build_tag" @@ -143,9 +182,11 @@ help_menu() { echo "arguments to the docker build command." echo echo "Options:" - echo " --variation Set the PHP variation (e.g., apache, fpm)" + echo " --variation Set the PHP variation (e.g., apache, fpm), or 'base' to compile PHP" + echo " from source into serversideup/php-base (needs --sapi)" echo " --version Set the PHP version (e.g., 8.4, 8.5, or a pinned patch like 8.1.34)" echo " --os Set the base OS (e.g., bookworm, trixie, alpine3.24)" + echo " --sapi With --variation base: cli, fpm, or zts" echo " --prefix Set the prefix for the Docker image (e.g., beta)" echo " --registry Set a custom registry (e.g., localhost:5000)" echo " --platform Set the platform (default: detected from system architecture)" @@ -174,6 +215,10 @@ while [[ $# -gt 0 ]]; do PHP_BUILD_PREFIX="$2-" shift 2 ;; + --sapi) + PHP_BUILD_SAPI="$2" + shift 2 + ;; --registry) DOCKER_REPOSITORY="$2" shift 2 @@ -209,6 +254,20 @@ check_vars \ PHP_BUILD_VERSION \ PHP_BUILD_BASE_OS +if [ "$PHP_BUILD_VARIATION" = "base" ]; then + case "$PHP_BUILD_SAPI" in + cli|fpm|zts) ;; + *) + echo_color_message red "๐Ÿšจ --variation base needs --sapi cli, fpm, or zts" + echo + help_menu + exit 1 + ;; + esac + build_base_image + exit 0 +fi + # Auto-resolve PHP extension source overrides for the minor version being built if [ -z "$PHP_EXTENSION_OVERRIDES" ]; then PHP_EXTENSION_OVERRIDES=$(MINOR="$(php_minor_version "$PHP_BUILD_VERSION")" yq -r ' diff --git a/scripts/get-php-source.sh b/scripts/get-php-source.sh new file mode 100755 index 00000000..6e15cf51 --- /dev/null +++ b/scripts/get-php-source.sh @@ -0,0 +1,88 @@ +#!/bin/bash +################################################### +# Usage: get-php-source.sh [--config PATH] +################################################### +# Prints the build args src/base/Dockerfile needs to compile one PHP version, as JSON: +# +# {"php_version": "8.4.27", "minor": "8.4", "url": "https://www.php.net/distributions/php-8.4.27.tar.xz", +# "asc_url": "...tar.xz.asc", "sha256": "...", "gpg_keys": "AFD8... 9D7F... 0616..."} +# +# is a minor (8.4) or an exact release (8.4.27). A minor resolves to the +# patch pinned in the base config if it has one (8.1), otherwise to the current +# release on https://www.php.net/releases/active.php. An exact release is looked up on +# https://www.php.net/releases/index.php. The GPG keys come from the minor's gpg_keys in +# the base config. +# +# Only php.net is consulted. If it does not list the version, this fails instead of +# falling back to an older patch. + +set -euo pipefail + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" +DOWNLOAD="$PROJECT_ROOT_DIR/src/common/usr/local/bin/docker-php-serversideup-download" +BASE_PHP_VERSIONS_CONFIG_FILE="${BASE_PHP_VERSIONS_CONFIG_FILE:-"$SCRIPT_DIR/conf/php-versions-base-config.yml"}" +PHP_NET="${PHP_NET:-https://www.php.net}" + +fail() { + echo "โŒ $*" >&2 + exit 1 +} + +VERSION="" +while [ "$#" -gt 0 ]; do + case "$1" in + --config) BASE_PHP_VERSIONS_CONFIG_FILE="${2:?--config requires a path}"; shift 2 ;; + -*) fail "Unknown option: $1" ;; + *) VERSION="$1"; shift ;; + esac +done + +[[ "$VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail "Usage: $0 , for example 8.4 or 8.4.27" +command -v jq > /dev/null || fail "jq is required" +command -v yq > /dev/null || fail "yq is required (https://github.com/mikefarah/yq)" + +MINOR="$(echo "$VERSION" | cut -d. -f1,2)" + +fetch_json() { + local body + body="$("$DOWNLOAD" "$1")" || fail "Could not fetch $1" + jq -e . > /dev/null 2>&1 <<< "$body" || fail "$1 did not return JSON" + echo "$body" +} + +minor_config="$(MINOR="$MINOR" yq -o=json -I0 '.php_versions[].minor_versions[] | select(.minor == env(MINOR))' "$BASE_PHP_VERSIONS_CONFIG_FILE")" +[ -n "$minor_config" ] || fail "PHP $MINOR is not in $BASE_PHP_VERSIONS_CONFIG_FILE" +gpg_keys="$(jq -r '.gpg_keys // [] | join(" ")' <<< "$minor_config")" +[ -n "$gpg_keys" ] || fail "PHP $MINOR has no gpg_keys in $BASE_PHP_VERSIONS_CONFIG_FILE" + +if [ "$VERSION" = "$MINOR" ]; then + pinned="$(jq -r '.patch_versions // [] | .[0] // empty' <<< "$minor_config")" +else + pinned="$VERSION" +fi + +if [ -n "$pinned" ]; then + release="$(fetch_json "$PHP_NET/releases/index.php?json&version=$pinned")" + if jq -e '.error' > /dev/null <<< "$release"; then + fail "php.net does not know PHP $pinned: $(jq -r '.error' <<< "$release")" + fi + release="$(jq -c --arg v "$pinned" '{version: $v, source}' <<< "$release")" +else + release="$(fetch_json "$PHP_NET/releases/active.php" | jq -c --arg minor "$MINOR" '.[$minor | split(".")[0]][$minor] // empty')" + [ -n "$release" ] || fail "php.net does not list PHP $MINOR as active. If it is end of life, pin its patch_versions in $BASE_PHP_VERSIONS_CONFIG_FILE." +fi + +tarball="$(jq -c '[.source[]? | select(.filename | endswith(".tar.xz"))][0] // empty' <<< "$release")" +[ -n "$tarball" ] || fail "php.net lists no .tar.xz tarball for PHP $(jq -r '.version' <<< "$release")" +sha256="$(jq -r '.sha256 // empty' <<< "$tarball")" +[[ "$sha256" =~ ^[0-9a-f]{64}$ ]] || fail "php.net lists no sha256 for $(jq -r '.filename' <<< "$tarball")" + +url="$PHP_NET/distributions/$(jq -r '.filename' <<< "$tarball")" +jq -n -c \ + --arg php_version "$(jq -r '.version' <<< "$release")" \ + --arg minor "$MINOR" \ + --arg url "$url" \ + --arg sha256 "$sha256" \ + --arg gpg_keys "$gpg_keys" \ + '{php_version: $php_version, minor: $minor, url: $url, asc_url: ($url + ".asc"), sha256: $sha256, gpg_keys: $gpg_keys}' diff --git a/scripts/get-php-versions.sh b/scripts/get-php-versions.sh index 5808058f..d15345d3 100755 --- a/scripts/get-php-versions.sh +++ b/scripts/get-php-versions.sh @@ -447,7 +447,8 @@ if [ "$SKIP_DOWNLOAD" = false ]; then minor: .[0].minor, base_os: (map(.base_os // []) | add), patch_versions: (map(.patch_versions // []) | flatten | unique | select(. != null)), - php_extension_overrides: (map(.php_extension_overrides // []) | add | unique) + php_extension_overrides: (map(.php_extension_overrides // []) | add | unique), + gpg_keys: (map(.gpg_keys // []) | add | unique) }) ) }) diff --git a/scripts/test-base-image.sh b/scripts/test-base-image.sh new file mode 100755 index 00000000..846336f5 --- /dev/null +++ b/scripts/test-base-image.sh @@ -0,0 +1,297 @@ +#!/bin/bash +################################################### +# Usage: scripts/test-base-image.sh [official-ref] +################################################### +# Checks that a php-base image keeps the official php image contract: +# - reports the expected PHP version and runs commands through docker-php-entrypoint +# - sets the ENV the official image sets (PHP_INI_DIR, PHPIZE_DEPS, PHP_CFLAGS, ...) +# - keeps /usr/src/php.tar.xz and the docker-php-* helpers, so docker-php-ext-install works +# - installs serversideup's default extensions with install-php-extensions +# - for FPM images, starts php-fpm and answers /ping and a PHP script over FastCGI +# +# With an official reference image (for example php:8.4.27-fpm-bookworm) it also +# requires the same image config, ENV, `php -m` module list, configure flags, files +# under /usr/local/etc, and www-data user. The reference is pulled for the same +# platform as the image. An extension install that also fails on the reference +# only warns, since that is an upstream problem rather than a difference. +# +# Every check runs even after a failure; the exit code is 1 if any check failed. + +set -uo pipefail + +IMAGE="${1:-}" +EXPECTED_VERSION="${2:-}" +OFFICIAL_REF="${3:-}" + +if [ -z "$IMAGE" ] || [ -z "$EXPECTED_VERSION" ]; then + sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' >&2 + exit 1 +fi + +INSTALL_PHP_EXTENSIONS_URL="https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions" +TEST_EXTENSIONS="redis zip pdo_pgsql" + +failures=0 +containers=() + +cleanup() { + for container in "${containers[@]+"${containers[@]}"}"; do + docker rm -f "$container" > /dev/null 2>&1 || true + done +} +trap cleanup EXIT + +pass() { + echo "โœ… $1" +} + +warn() { + echo "โš ๏ธ $1" + shift + for line in "$@"; do + echo " $line" + done +} + +fail() { + echo "โŒ $1" + shift + for line in "$@"; do + echo " $line" + done + failures=$((failures + 1)) +} + +section() { + echo + echo "โ”€โ”€ $1" +} + +# Run a shell snippet in a fresh container of the given image, bypassing the entrypoint. +in_image() { + local image="$1" + shift + docker run --rm --entrypoint sh "$image" -c "$*" +} + +# Compare two values and report a diff when they differ. +expect_equal() { + local name="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + pass "$name" + else + fail "$name" "expected: $expected" "actual: $actual" + fi +} + +expect_same_lines() { + local name="$1" expected="$2" actual="$3" diff_output + if diff_output="$(diff <(echo "$expected") <(echo "$actual"))"; then + pass "$name" + else + # shellcheck disable=SC2001 + fail "$name" "diff (< official, > php-base):" "$(echo "$diff_output" | sed 's/^/ /')" + fi +} + +if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then + docker pull -q "$IMAGE" > /dev/null || { echo "โŒ Cannot pull $IMAGE"; exit 1; } +fi +PLATFORM="$(docker image inspect -f '{{.Os}}/{{.Architecture}}{{if .Variant}}/{{.Variant}}{{end}}' "$IMAGE")" +echo "๐Ÿงช Testing $IMAGE ($PLATFORM), expecting PHP $EXPECTED_VERSION" + +OFFICIAL_PULLED=false +if [ -n "$OFFICIAL_REF" ]; then + if docker pull -q --platform "$PLATFORM" "$OFFICIAL_REF" > /dev/null; then + OFFICIAL_PULLED=true + else + fail "pull $OFFICIAL_REF for $PLATFORM" + fi +fi + +# Run an install script as root in a fresh container. One that also fails on the +# official image is an upstream problem rather than a difference, so it only warns. +check_install() { + local name="$1" script="$2" output + if output="$(docker run --rm "$IMAGE" sh -c "$script" 2>&1)"; then + pass "$name" + elif [ "$OFFICIAL_PULLED" = true ] && ! docker run --rm "$OFFICIAL_REF" sh -c "$script" > /dev/null 2>&1; then + warn "$name fails on $OFFICIAL_REF too, so it is not counted" "$(echo "$output" | tail -3)" + else + fail "$name" "$(echo "$output" | tail -30)" + fi +} + +IS_FPM=false +if in_image "$IMAGE" 'command -v php-fpm' > /dev/null; then IS_FPM=true; fi +IS_ALPINE=false +if in_image "$IMAGE" '[ -f /etc/alpine-release ]'; then IS_ALPINE=true; fi + +########## +section "Version and entrypoint" +########## +expect_equal "PHP_VERSION is $EXPECTED_VERSION" "$EXPECTED_VERSION" \ + "$(docker run --rm --entrypoint php "$IMAGE" -r 'echo PHP_VERSION;')" +expect_equal "docker-php-entrypoint runs commands" "ok" \ + "$(docker run --rm "$IMAGE" php -r 'echo "ok";')" +expect_equal "docker-php-entrypoint prepends php to flags" "ok" \ + "$(docker run --rm "$IMAGE" -r 'echo "ok";')" + +########## +section "ENV contract" +########## +expect_equal "PHP_INI_DIR is /usr/local/etc/php" "/usr/local/etc/php" "$(in_image "$IMAGE" 'echo "$PHP_INI_DIR"')" +for var in PHPIZE_DEPS PHP_CFLAGS PHP_CPPFLAGS PHP_LDFLAGS PHP_VERSION PHP_URL PHP_ASC_URL PHP_SHA256 GPG_KEYS; do + value="$(in_image "$IMAGE" "printf '%s' \"\$$var\"")" + if [ -n "$value" ]; then pass "$var is set"; else fail "$var is set" "it is empty"; fi +done + +########## +section "Files and helpers" +########## +file_checks=' + check() { if eval "$2"; then echo "ok|$1"; else echo "fail|$1"; fi; } + check "/usr/src/php.tar.xz is kept for docker-php-source" "[ -s /usr/src/php.tar.xz ] && [ -s /usr/src/php.tar.xz.asc ]" + for helper in docker-php-source docker-php-ext-configure docker-php-ext-install docker-php-ext-enable docker-php-entrypoint; do + check "$helper is executable" "[ -x /usr/local/bin/$helper ]" + done + check "php.ini-production and php.ini-development are in PHP_INI_DIR" "[ -f \"\$PHP_INI_DIR/php.ini-production\" ] && [ -f \"\$PHP_INI_DIR/php.ini-development\" ]" + check "sodium is enabled" "php -m | grep -qx sodium" + check "opcache is loaded" "php -m | grep -qx \"Zend OPcache\"" + check "/var/www/html is owned by www-data with mode 1777" "[ \"\$(stat -c %U:%a /var/www/html)\" = www-data:1777 ]" + if [ -f /etc/debian_version ]; then + check "Debian PHP packages are pinned out" "[ -f /etc/apt/preferences.d/no-debian-php ]" + fi +' +while IFS='|' read -r status name; do + if [ "$status" = ok ]; then pass "$name"; else fail "$name"; fi +done < <(in_image "$IMAGE" "$file_checks") + +check_install "docker-php-ext-install pcntl builds and loads the extension" ' + docker-php-ext-install pcntl > /tmp/ext.log 2>&1 || { tail -20 /tmp/ext.log; exit 1; } + php -m | grep -qx pcntl +' + +check_install "install-php-extensions $TEST_EXTENSIONS" " + curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 -o /usr/local/bin/install-php-extensions '$INSTALL_PHP_EXTENSIONS_URL' && + chmod +x /usr/local/bin/install-php-extensions && + install-php-extensions $TEST_EXTENSIONS > /tmp/install.log 2>&1 || { tail -30 /tmp/install.log; exit 1; } + for ext in $TEST_EXTENSIONS; do php -m | grep -qx \"\$ext\" || { echo \"\$ext is not loaded\"; exit 1; }; done +" + +########## +if [ "$IS_FPM" = true ]; then +section "PHP-FPM" +########## + if docker run --rm "$IMAGE" php-fpm -t > /dev/null 2>&1; then + pass "php-fpm -t accepts the configuration" + else + fail "php-fpm -t accepts the configuration" "$(docker run --rm "$IMAGE" php-fpm -t 2>&1 | tail -5)" + fi + + work_dir="$(mktemp -d)" + printf '[www]\nping.path = /ping\nping.response = pong\n' > "$work_dir/zzz-test.conf" + printf ' "$work_dir/index.php" + container="$(docker create "$IMAGE")" + containers+=("$container") + docker cp -q "$work_dir/zzz-test.conf" "$container:/usr/local/etc/php-fpm.d/zzz-test.conf" + docker cp -q "$work_dir/index.php" "$container:/var/www/html/index.php" + rm -rf "$work_dir" + docker start "$container" > /dev/null + + if [ "$IS_ALPINE" = true ]; then + fcgi_install='apk add --no-cache fcgi > /dev/null' + else + fcgi_install='apt-get update > /dev/null && apt-get install -y --no-install-recommends libfcgi-bin > /dev/null' + fi + if ! docker exec "$container" sh -c "$fcgi_install" > /dev/null 2>&1; then + fail "install cgi-fcgi to talk to php-fpm" + fi + + fcgi_request() { + docker exec "$container" sh -c "SCRIPT_NAME='$1' SCRIPT_FILENAME='$2' REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000" 2>/dev/null | tr -d '\r' | sed '1,/^$/d' + } + + ping="" + for _ in $(seq 1 30); do + ping="$(fcgi_request /ping /ping)" + [ "$ping" = pong ] && break + sleep 1 + done + expect_equal "php-fpm answers /ping on port 9000" "pong" "$ping" + expect_equal "php-fpm runs /var/www/html/index.php" "fpm-ok $EXPECTED_VERSION fpm-fcgi" \ + "$(fcgi_request /index.php /var/www/html/index.php)" + if docker logs "$container" 2>&1 | grep -q 'ready to handle connections'; then + pass "php-fpm logs to stderr" + else + fail "php-fpm logs to stderr" "$(docker logs "$container" 2>&1 | tail -5)" + fi +fi + +########## +if [ "$OFFICIAL_PULLED" = true ]; then +section "Parity with $OFFICIAL_REF" +########## + config_of() { + docker image inspect "$1" | jq -c '.[0].Config | {Entrypoint, Cmd, WorkingDir, StopSignal, ExposedPorts, User}' + } + expect_equal "image config (entrypoint, cmd, workdir, stop signal, ports, user)" \ + "$(config_of "$OFFICIAL_REF")" "$(config_of "$IMAGE")" + + env_names_of() { + docker image inspect "$1" | jq -r '.[0].Config.Env[] | split("=")[0]' | sort + } + expect_same_lines "ENV variable names" "$(env_names_of "$OFFICIAL_REF")" "$(env_names_of "$IMAGE")" + + env_value_of() { + docker image inspect "$1" | jq -r --arg name "$2" '.[0].Config.Env[] | select(startswith($name + "=")) | sub("^[^=]*="; "")' | tr -s ' \t' ' ' | sed 's/^ //;s/ $//' + } + same_version=false + [ "$(env_value_of "$OFFICIAL_REF" PHP_VERSION)" = "$EXPECTED_VERSION" ] && same_version=true + compare_vars="PHP_INI_DIR PHPIZE_DEPS PHP_CFLAGS PHP_CPPFLAGS PHP_LDFLAGS GPG_KEYS" + if [ "$same_version" = true ]; then + compare_vars="$compare_vars PHP_VERSION PHP_URL PHP_ASC_URL PHP_SHA256" + else + echo "โ„น๏ธ $OFFICIAL_REF is PHP $(env_value_of "$OFFICIAL_REF" PHP_VERSION), so version-specific ENV is not compared" + fi + for var in $compare_vars; do + expect_equal "ENV $var matches" "$(env_value_of "$OFFICIAL_REF" "$var")" "$(env_value_of "$IMAGE" "$var")" + done + + modules_of() { + docker run --rm --entrypoint php "$1" -m | grep -v '^$' | sort + } + expect_same_lines "php -m module list" "$(modules_of "$OFFICIAL_REF")" "$(modules_of "$IMAGE")" + + # PHP_BUILD_PROVIDER names who built the binary, so it differs on purpose. + configure_flags_of() { + docker run --rm --entrypoint php "$1" -i | grep '^Configure Command' | grep -oE "'[^']*'" | tr -d "'" | grep -v '^PHP_BUILD_PROVIDER=' | sort + } + expect_same_lines "configure flags (php -i)" "$(configure_flags_of "$OFFICIAL_REF")" "$(configure_flags_of "$IMAGE")" + + build_info_of() { + docker run --rm --entrypoint php "$1" -i | grep -E '^(Thread Safety|Zend Extension Build|PHP Extension Build|Debug Build|Server API) =>' | sort + } + expect_same_lines "thread safety and extension ABI" "$(build_info_of "$OFFICIAL_REF")" "$(build_info_of "$IMAGE")" + + # pear.conf records when pecl last refreshed its channels, so it never matches. + etc_checksums_of() { + in_image "$1" 'cd /usr/local/etc && find . -type f ! -name pear.conf | sort | xargs md5sum' + } + expect_same_lines "config files under /usr/local/etc" "$(etc_checksums_of "$OFFICIAL_REF")" "$(etc_checksums_of "$IMAGE")" + + expect_equal "www-data user" "$(in_image "$OFFICIAL_REF" 'id www-data')" "$(in_image "$IMAGE" 'id www-data')" + + size_of() { + docker image inspect -f '{{.Size}}' "$1" | awk '{ printf "%.0f MB", $1 / 1000000 }' + } + echo "โ„น๏ธ Uncompressed size: official $(size_of "$OFFICIAL_REF"), php-base $(size_of "$IMAGE")" +fi + +echo +if [ "$failures" -eq 0 ]; then + echo "โœ… All checks passed for $IMAGE" +else + echo "โŒ $failures check(s) failed for $IMAGE" + exit 1 +fi diff --git a/src/base/Dockerfile b/src/base/Dockerfile new file mode 100644 index 00000000..f483a643 --- /dev/null +++ b/src/base/Dockerfile @@ -0,0 +1,494 @@ +# syntax=docker/dockerfile:1 +# +# Base image the variations can build FROM instead of the official php image. It +# compiles PHP from the php.net release tarball, so a new PHP release does not wait +# on Docker Hub. It is a port of docker-library/php's Dockerfile-linux.template +# (https://github.com/docker-library/php, MIT) with the template conditionals turned +# into build args and shell conditionals, and it keeps the official image's contract: +# paths, ENV, helper scripts, configure flags, and FPM configuration. +# scripts/test-base-image.sh checks that contract against the official image. +# +# The build context is the project root. The build args come from +# scripts/get-php-source.sh (scripts/dev.sh --variation base passes them all): +# BASE_IMAGE debian:bookworm-slim, debian:trixie-slim, alpine:3.22, alpine:3.23, alpine:3.24 +# OS_FAMILY debian or alpine (derived from BASE_IMAGE; set it when using a registry mirror) +# PHP_SAPI cli, fpm, or zts +# PHP_VERSION, PHP_URL, PHP_ASC_URL, PHP_SHA256, GPG_KEYS +# PHP_BUILD_PROVIDER reported by phpinfo() as "Build Provider" + +ARG BASE_IMAGE=debian:bookworm-slim +ARG OS_FAMILY=${BASE_IMAGE%%:*} +ARG PHP_SAPI=cli + +########## +# Debian base +########## +FROM ${BASE_IMAGE} AS base-debian + +# prevent Debian's PHP packages from being installed +# https://github.com/docker-library/php/pull/542 +RUN set -eux; \ + { \ + echo 'Package: php*'; \ + echo 'Pin: release *'; \ + echo 'Pin-Priority: -1'; \ + } > /etc/apt/preferences.d/no-debian-php + +# dependencies required for running "phpize" +# (see persistent deps below) +ENV PHPIZE_DEPS="autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c" + +# persistent / runtime deps +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + $PHPIZE_DEPS \ + ca-certificates \ + curl \ + xz-utils \ + ; \ + if grep -q '^VERSION_CODENAME=bookworm' /etc/os-release; then rm -rf /var/lib/apt/lists/*; else apt-get dist-clean; fi + +########## +# Alpine base +########## +FROM ${BASE_IMAGE} AS base-alpine + +# dependencies required for running "phpize" +# these get automatically installed and removed by "docker-php-ext-*" (unless they're already installed) +ENV PHPIZE_DEPS="autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c" + +# persistent / runtime deps +RUN apk add --no-cache \ + ca-certificates \ + curl \ + openssl \ + tar \ + xz + +# ensure www-data user exists +# 82 is the standard uid/gid for "www-data" in Alpine +# https://git.alpinelinux.org/aports/tree/main/nginx/nginx.pre-install?h=3.14-stable +RUN set -eux; \ + adduser -u 82 -D -S -G www-data www-data + +########## +# PHP build (shared by every OS and SAPI) +########## +# hadolint ignore=DL3006 +FROM base-${OS_FAMILY} AS php + +ENV PHP_INI_DIR=/usr/local/etc/php +RUN set -eux; \ + mkdir -p "$PHP_INI_DIR/conf.d"; \ +# allow running as an arbitrary user (https://github.com/docker-library/php/issues/743) + [ ! -d /var/www/html ]; \ + mkdir -p /var/www/html; \ + chown www-data:www-data /var/www/html; \ + chmod 1777 /var/www/html + +# Apply stack smash protection to functions using local buffers and alloca() +# Make PHP's main executable position-independent (improves ASLR security mechanism, and has no performance impact on x86_64) +# Enable optimization (-O2) +# Enable linker optimization (this sorts the hash buckets to improve cache locality, and is non-default) +# https://github.com/docker-library/php/issues/272 +# -D_LARGEFILE_SOURCE and -D_FILE_OFFSET_BITS=64 (https://www.php.net/manual/en/intro.filesystem.php) +ENV PHP_CFLAGS="-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64" +ENV PHP_CPPFLAGS="$PHP_CFLAGS" +ENV PHP_LDFLAGS="-Wl,-O1 -pie" + +ARG GPG_KEYS +ARG PHP_VERSION +ARG PHP_URL +ARG PHP_ASC_URL +ARG PHP_SHA256 +ENV GPG_KEYS="${GPG_KEYS}" +ENV PHP_VERSION="${PHP_VERSION}" +ENV PHP_URL="${PHP_URL}" PHP_ASC_URL="${PHP_ASC_URL}" +ENV PHP_SHA256="${PHP_SHA256}" + +# The download helper is mounted for the build only, so it does not end up in the image. +# Unquoted lists ($GPG_KEYS, $savedAptMark) are split into words on purpose, as upstream does. +# hadolint ignore=DL3003,SC2086 +RUN --mount=type=bind,source=src/common/usr/local/bin/docker-php-serversideup-download,target=/usr/local/bin/docker-php-serversideup-download \ + set -eux; \ + for var in PHP_VERSION PHP_URL PHP_ASC_URL PHP_SHA256 GPG_KEYS; do \ + eval "value=\${$var}"; \ + if [ -z "$value" ]; then echo >&2 "error: build arg $var is required (see scripts/get-php-source.sh)"; exit 1; fi; \ + done; \ + \ + if [ -f /etc/alpine-release ]; then \ + apk add --no-cache --virtual .fetch-deps gnupg; \ + else \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends gnupg; \ + if grep -q '^VERSION_CODENAME=bookworm' /etc/os-release; then rm -rf /var/lib/apt/lists/*; else apt-get dist-clean; fi; \ + fi; \ + \ + mkdir -p /usr/src; \ + cd /usr/src; \ + \ + docker-php-serversideup-download "$PHP_URL" php.tar.xz; \ + echo "$PHP_SHA256 *php.tar.xz" | sha256sum -c -; \ + \ + docker-php-serversideup-download "$PHP_ASC_URL" php.tar.xz.asc; \ + export GNUPGHOME="$(mktemp -d)"; \ + for key in $GPG_KEYS; do \ + for attempt in 1 2 3 4 5; do \ + if gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"; then break; fi; \ + if [ "$attempt" = 5 ]; then exit 1; fi; \ + sleep "$((attempt * 3))"; \ + done; \ + done; \ + gpg --batch --verify php.tar.xz.asc php.tar.xz; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + \ + if [ -f /etc/alpine-release ]; then \ + apk del --no-network .fetch-deps; \ + else \ + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + fi + +COPY --chmod=755 src/base/bin/docker-php-source /usr/local/bin/ + +ARG PHP_SAPI +ARG PHP_BUILD_PROVIDER=https://github.com/serversideup/docker-php + +# Unquoted lists ($patches, $runDeps, $savedAptMark) are split into words on purpose, as upstream does. +# hadolint ignore=DL3003,SC2086 +RUN --mount=type=bind,source=src/common/usr/local/bin/docker-php-serversideup-download,target=/usr/local/bin/docker-php-serversideup-download \ + set -eux; \ + case "$PHP_SAPI" in \ + cli|fpm|zts) ;; \ + *) echo >&2 "error: PHP_SAPI must be cli, fpm, or zts (got '$PHP_SAPI')"; exit 1 ;; \ + esac; \ + if [ -f /etc/alpine-release ]; then os=alpine; else os=debian; fi; \ + \ +# PHP_VERSION_ID as defined by https://www.php.net/phpversion ("8.6.0RC4" counts as 8.6.0) + version="${PHP_VERSION%%[!0-9.]*}"; \ + major="${version%%.*}"; \ + rest="${version#*.}"; \ + minor="${rest%%.*}"; \ + patch="${rest#*.}"; \ + if [ "$patch" = "$rest" ]; then patch=0; fi; \ + versionId="$((major * 10000 + minor * 100 + patch))"; \ + minorVersion="$major.$minor"; \ + \ +# https://github.com/docker-library/php/issues/1554 (pecl/pear and the oniguruma-backed mbstring are not built for 8.6+) + installPearPecl=; \ + if [ "$versionId" -lt 80600 ]; then installPearPecl=1; fi; \ + \ +# Alpine patches carried by docker-library/php: " " +# https://github.com/docker-library/php/pull/1552 (11678, 14834), https://github.com/docker-library/php/pull/1580 (18743), +# and https://github.com/docker-library/php/pull/1638 (19131 is reversed in 8.1.34 so that 14834 still applies) + patches=; \ + if [ "$os" = alpine ] && [ "$minorVersion" = 8.1 ]; then \ + patches="577b8ae4226368e66fee7a9b5c58f9e2428372fc 6edc20c3bb3e7cc13515abce7f2fffa8ebea6cf7469abfbc78fcdc120350b239 11678 apply"; \ + if [ "$versionId" -ge 80134 ]; then \ + patches="$patches a5df26691d1b1f71b964b869510473a5d5413999 c98c01c74d3a6da197116f8511b16fed361ca6bbb6a8af8df629a933f541c213 19131 reverse"; \ + fi; \ + patches="$patches 67259e451d5d58b4842776c5696a66d74e157609 ed10a1b254091ad676ed204e55628ecbd6c8962004d6185a1821cedecd526c0f 14834 apply"; \ + if [ "$PHP_SAPI" = zts ]; then \ + patches="$patches 6b105d4bc57e20a2472c9a6ff11fba32768556d4 037e1610ae5d444e9a8c3ecd9d5f0cd215fd0aac90bdd7b9f0b259bffdf3566b 18743 apply"; \ + fi; \ + elif [ "$os" = alpine ] && [ "$minorVersion" = 8.2 ] && [ "$PHP_SAPI" = zts ]; then \ + patches="b3c8afe272a6919248986c703c2e1defc73ff707 b334f73434c9732a4b27a42eb5d417e10df842e854c02a3e753b2479f8978bf5 18743 apply"; \ + fi; \ + \ + if [ "$os" = alpine ]; then \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + argon2-dev \ + coreutils \ + curl-dev \ + gnu-libiconv-dev \ + libsodium-dev \ + libxml2-dev \ + linux-headers \ + openssl-dev \ + readline-dev \ + sqlite-dev \ + ${installPearPecl:+oniguruma-dev} \ + ${patches:+patch patchutils} \ + ; \ +# make sure musl's iconv doesn't get used (https://www.php.net/manual/en/intro.iconv.php) + rm -vf /usr/include/iconv.h; \ + else \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libargon2-dev \ + libcurl4-openssl-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + zlib1g-dev \ + ${installPearPecl:+libonig-dev} \ + ; \ + fi; \ + \ + export \ + CFLAGS="$PHP_CFLAGS" \ + CPPFLAGS="$PHP_CPPFLAGS" \ + LDFLAGS="$PHP_LDFLAGS" \ +# https://github.com/php/php-src/blob/d6299206dd828382753453befd1b915491b741c6/configure.ac#L1496-L1511 + PHP_BUILD_PROVIDER="$PHP_BUILD_PROVIDER" \ + PHP_UNAME='Linux - Docker' \ + ; \ + docker-php-source extract; \ + cd /usr/src/php; \ + set -- $patches; \ + while [ "$#" -ge 4 ]; do \ + docker-php-serversideup-download "https://github.com/php/php-src/commit/$1.patch?full_index=1" "$3.patch"; \ + echo "$2 *$3.patch" | sha256sum -c -; \ + if [ "$4" = reverse ]; then \ + patch --reverse -p1 < "$3.patch"; \ + else \ + filterdiff -x '*/NEWS' "$3.patch" | patch -p1; \ + fi; \ + rm "$3.patch"; \ + shift 4; \ + done; \ + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ + if [ "$os" = debian ]; then \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ +# https://bugs.php.net/bug.php?id=74125 + if [ ! -d /usr/include/curl ]; then \ + ln -sT "/usr/include/$debMultiarch/curl" /usr/local/include/curl; \ + fi; \ + fi; \ + case "$gnuArch" in \ + 'i686-linux-'*) \ +# php zend jit assumes SSE registers, xmm0-7, are available (and "Pentium4"/SSE2 is a reasonable baseline) +# https://github.com/php/php-src/pull/22832 + export \ + CFLAGS="$CFLAGS -msse2 -mfpmath=sse" \ + CPPFLAGS="$CPPFLAGS -msse2 -mfpmath=sse" \ + ; \ + ;; \ + esac; \ + test "$PHP_INI_DIR" != "${PHP_INI_DIR%/php}"; \ + \ + set -- \ + --build="$gnuArch" \ + --with-config-file-path="$PHP_INI_DIR" \ + --with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \ +# make sure invalid --configure-flags are fatal errors instead of just warnings + --enable-option-checking=fatal \ +# https://github.com/docker-library/php/issues/439 + --with-mhash \ +# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself) + --enable-mysqlnd \ +# https://wiki.php.net/rfc/argon2_password_hash + --with-password-argon2 \ +# https://wiki.php.net/rfc/libsodium + --with-sodium=shared \ +# always build against system sqlite3 (https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109) + --with-pdo-sqlite=/usr \ + --with-sqlite3=/usr \ + --with-curl \ + --with-openssl \ + --with-readline \ + --with-zlib \ + ; \ +# https://github.com/docker-library/php/pull/1636 (the official 8.1 images predate it; the value is configure's default either way) + if [ "$minorVersion" != 8.1 ]; then set -- "$@" --sysconfdir="${PHP_INI_DIR%/php}"; fi; \ +# https://github.com/docker-library/php/issues/822 +# 8.6+: no --enable-pic; forcing PIC also removes -fpie from executable objects, preventing local-exec TLS: https://github.com/docker-library/php/pull/1692 + if [ "$versionId" -lt 80600 ]; then set -- "$@" --with-pic; fi; \ +# --enable-ftp is included for compatibility with existing versions (https://github.com/docker-library/php/issues/236) + if [ "$minorVersion" = 8.1 ]; then set -- "$@" --enable-ftp; fi; \ +# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195) +# in PHP 7.4+, the pecl/pear installers are officially deprecated (requiring an explicit "--with-pear") + if [ -n "$installPearPecl" ]; then set -- "$@" --enable-mbstring --with-pear; fi; \ + if [ "$os" = alpine ]; then \ + set -- "$@" --with-iconv=/usr; \ + else \ + set -- "$@" --with-iconv --with-libdir="lib/$debMultiarch"; \ + fi; \ + case "$PHP_SAPI" in \ +# https://github.com/docker-library/php/pull/1259 + cli|zts) set -- "$@" --enable-phpdbg --enable-phpdbg-readline ;; \ +# https://github.com/bwoebi/phpdbg-docs/issues/1#issuecomment-163872806 ("phpdbg is primarily a CLI debugger, and is not suitable for debugging an fpm stack.") + *) set -- "$@" --disable-phpdbg ;; \ + esac; \ +# bundled pcre does not support JIT on riscv64 until 10.41 (php 8.3+) + case "$minorVersion:$gnuArch" in \ + 8.1:riscv64-linux-*|8.2:riscv64-linux-*) set -- "$@" --without-pcre-jit ;; \ + esac; \ +# https://github.com/docker-library/php/issues/280 + if [ "$PHP_SAPI" = fpm ]; then set -- "$@" --disable-cgi; fi; \ +# https://github.com/docker-library/php/pull/939#issuecomment-730501748 +# zts + alpine special cased for embed (otherwise zts is effectively cli): https://github.com/docker-library/php/pull/1342 + if [ "$PHP_SAPI" = zts ] || { [ "$PHP_SAPI" = cli ] && { [ "$versionId" -ge 80600 ] || [ "$os" != alpine ]; }; }; then \ + set -- "$@" --enable-embed; \ + fi; \ + if [ "$PHP_SAPI" = fpm ]; then \ + set -- "$@" --enable-fpm --with-fpm-user=www-data --with-fpm-group=www-data; \ + fi; \ +# https://externals.io/message/118859 + if [ "$PHP_SAPI" = zts ] || [ "$versionId" -ge 80600 ]; then \ + set -- "$@" --enable-zts --disable-zend-signals; \ + case "$minorVersion" in 8.1|8.2) set -- "$@" --enable-zend-max-execution-timers ;; esac; \ + fi; \ + ./configure "$@"; \ + make -j "$(nproc)"; \ + find -type f -name '*.a' -delete; \ + make install; \ + find \ + /usr/local \ + -type f \ + -perm '/0111' \ + -exec sh -euxc ' \ + strip --strip-all "$@" || : \ + ' -- '{}' + \ + ; \ + make clean; \ + \ +# https://github.com/docker-library/php/issues/692 (copy default example "php.ini" files somewhere easily discoverable) + cp -v php.ini-* "$PHP_INI_DIR/"; \ + \ + cd /; \ + docker-php-source delete; \ + \ + if [ "$os" = alpine ]; then \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache $runDeps; \ + apk del --no-network .build-deps; \ + else \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \ + find /usr/local -type f -executable -exec ldd '{}' ';' \ + | awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \ + | sort -u \ + | xargs -rt dpkg-query --search \ + | awk 'sub(":$", "", $1) { print $1 }' \ + | sort -u \ + | xargs -r apt-mark manual \ + ; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + if grep -q '^VERSION_CODENAME=bookworm' /etc/os-release; then rm -rf /var/lib/apt/lists/*; else apt-get dist-clean; fi; \ + fi; \ + \ + if [ -n "$installPearPecl" ]; then \ +# update pecl channel definitions https://github.com/docker-library/php/issues/443 + pecl update-channels; \ + rm -rf /tmp/pear ~/.pearrc; \ + fi; \ + \ +# smoke test + php --version + +COPY --chmod=755 src/base/bin/docker-php-ext-configure src/base/bin/docker-php-ext-enable src/base/bin/docker-php-ext-install src/base/bin/docker-php-entrypoint /usr/local/bin/ + +# enable OPcache by default before 8.5, where it became a required built-in (https://wiki.php.net/rfc/make_opcache_required) +# sodium was built as a shared module (so that it can be replaced later if so desired), so let's enable it too (https://github.com/docker-library/php/issues/598) +RUN set -eux; \ + if [ "$(php -r 'echo PHP_VERSION_ID;')" -lt 80500 ]; then docker-php-ext-enable opcache; fi; \ + docker-php-ext-enable sodium + +ENTRYPOINT ["docker-php-entrypoint"] + +########## +# SAPI stages +########## +FROM php AS sapi-cli +CMD ["php", "-a"] + +FROM php AS sapi-zts +CMD ["php", "-a"] + +FROM php AS sapi-fpm +WORKDIR /var/www/html + +# The official 8.1 images predate https://github.com/docker-library/php/pull/1636 and keep the +# older layout: no --sysconfdir (so "NONE/" is stripped from the include), the localhost listen +# line left in www.conf, and "listen = 9000" in zz-docker.conf instead of docker.conf. +# hadolint ignore=DL3003 +RUN set -eux; \ + cd "${PHP_INI_DIR%/php}"; \ + legacyLayout=; \ + if [ "$(php -r 'echo PHP_VERSION_ID;')" -lt 80200 ]; then legacyLayout=1; fi; \ + \ + if [ -n "$legacyLayout" ]; then \ + sed 's!=NONE/!=!g' php-fpm.conf.default | tee php-fpm.conf > /dev/null; \ + cp php-fpm.d/www.conf.default php-fpm.d/www.conf; \ + else \ + cp -v php-fpm.conf.default php-fpm.conf; \ + cp -v php-fpm.d/www.conf.default php-fpm.d/www.conf; \ + \ + # comment out localhost-only listen address + grep -E '^listen = 127.0.0.1:9000' php-fpm.d/www.conf; \ + sed -ri 's/^(listen = 127.0.0.1:9000)/;\1/' php-fpm.d/www.conf; \ + grep -E '^;listen = 127.0.0.1:9000' php-fpm.d/www.conf; \ + fi; \ + \ + { \ + echo '[global]'; \ + echo 'error_log = /proc/self/fd/2'; \ + echo; echo '; https://github.com/docker-library/php/pull/725#issuecomment-443540114'; echo 'log_limit = 8192'; \ + echo; \ + echo '[www]'; \ + echo '; php-fpm closes STDOUT on startup, so sending logs to /proc/self/fd/1 does not work.'; \ + echo '; https://bugs.php.net/bug.php?id=73886'; \ + echo 'access.log = /proc/self/fd/2'; \ + echo; \ + echo 'clear_env = no'; \ + echo; \ + echo '; Ensure worker stdout and stderr are sent to the main error log.'; \ + echo 'catch_workers_output = yes'; \ + echo 'decorate_workers_output = no'; \ + if [ -z "$legacyLayout" ]; then \ + echo; \ + echo '; default listen address for easy override in later php-fpm.d/*.conf files'; \ + echo 'listen = 9000'; \ + fi; \ + } | tee php-fpm.d/docker.conf; \ + { \ + echo '[global]'; \ + echo 'daemonize = no'; \ + if [ -n "$legacyLayout" ]; then \ + echo; \ + echo '[www]'; \ + echo 'listen = 9000'; \ + elif [ "$(php -r 'echo PHP_VERSION_ID;')" -lt 80600 ]; then \ + echo; \ + echo '; the [www] ini section below is for backwards compatibility and will be removed in 8.6+'; \ + echo '[www]'; \ + fi; \ + } | tee php-fpm.d/zz-docker.conf; \ + mkdir -p "$PHP_INI_DIR/conf.d"; \ + { \ + echo '; https://github.com/docker-library/php/issues/878#issuecomment-938595965'; \ + echo 'fastcgi.logging = Off'; \ + } > "$PHP_INI_DIR/conf.d/docker-fpm.ini" + +# Override stop signal to stop process gracefully +# https://github.com/php/php-src/blob/17baa87faddc2550def3ae7314236826bc1b1398/sapi/fpm/php-fpm.8.in#L163 +STOPSIGNAL SIGQUIT + +EXPOSE 9000 +CMD ["php-fpm"] + +########## +# Final image +########## +# hadolint ignore=DL3006 +FROM sapi-${PHP_SAPI} +ARG PHP_BUILD_PROVIDER +LABEL org.opencontainers.image.title="serversideup/php-base" \ + org.opencontainers.image.description="PHP compiled from the php.net release tarball, compatible with the official php Docker images. The base for serversideup/php." \ + org.opencontainers.image.source="${PHP_BUILD_PROVIDER}" \ + org.opencontainers.image.version="${PHP_VERSION}" diff --git a/src/base/LICENSE-docker-library-php b/src/base/LICENSE-docker-library-php new file mode 100644 index 00000000..fbab368d --- /dev/null +++ b/src/base/LICENSE-docker-library-php @@ -0,0 +1,20 @@ +Copyright (c) 2014 Docker, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/base/bin/UPSTREAM b/src/base/bin/UPSTREAM new file mode 100644 index 00000000..9b4f02b7 --- /dev/null +++ b/src/base/bin/UPSTREAM @@ -0,0 +1,20 @@ +These scripts are copied verbatim from docker-library/php and are covered by +../LICENSE-docker-library-php (MIT). Do not edit them here; update them from +upstream so the base images keep the same behavior as the official php images. + +Repository: https://github.com/docker-library/php +Commit: 5ec2a6a0dad498e89a91a39454dbb52e98ce2bdb (2026-10-21) + +Files: + docker-php-entrypoint + docker-php-ext-configure + docker-php-ext-enable + docker-php-ext-install + docker-php-source + +To update: + git clone https://github.com/docker-library/php /tmp/docker-library-php + for f in docker-php-entrypoint docker-php-ext-configure docker-php-ext-enable docker-php-ext-install docker-php-source; do + cp /tmp/docker-library-php/$f src/base/bin/$f + done + git -C /tmp/docker-library-php log -1 --format='%H (%cs)' # record it above diff --git a/src/base/bin/docker-php-entrypoint b/src/base/bin/docker-php-entrypoint new file mode 100755 index 00000000..88a016c1 --- /dev/null +++ b/src/base/bin/docker-php-entrypoint @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- php "$@" +fi + +exec "$@" diff --git a/src/base/bin/docker-php-ext-configure b/src/base/bin/docker-php-ext-configure new file mode 100755 index 00000000..34fc1337 --- /dev/null +++ b/src/base/bin/docker-php-ext-configure @@ -0,0 +1,69 @@ +#!/bin/sh +set -e + +# prefer user supplied CFLAGS, but default to our PHP_CFLAGS +: ${CFLAGS:=$PHP_CFLAGS} +: ${CPPFLAGS:=$PHP_CPPFLAGS} +: ${LDFLAGS:=$PHP_LDFLAGS} +export CFLAGS CPPFLAGS LDFLAGS + +srcExists= +if [ -d /usr/src/php ]; then + srcExists=1 +fi +docker-php-source extract +if [ -z "$srcExists" ]; then + touch /usr/src/php/.docker-delete-me +fi + +cd /usr/src/php/ext + +usage() { + echo "usage: $0 ext-name [configure flags]" + echo " ie: $0 gd --with-jpeg-dir=/usr/local/something" + echo + echo 'Possible values for ext-name:' + find . \ + -mindepth 2 \ + -maxdepth 2 \ + -type f \ + -name 'config.m4' \ + | xargs -n1 dirname \ + | xargs -n1 basename \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +ext="$1" +if [ -z "$ext" ] || [ ! -d "$ext" ]; then + usage >&2 + exit 1 +fi +shift + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +if [ "$pm" = 'apk' ]; then + if \ + [ -n "$PHPIZE_DEPS" ] \ + && ! apk info --installed .phpize-deps > /dev/null \ + && ! apk info --installed .phpize-deps-configure > /dev/null \ + ; then + apk add --no-cache --virtual .phpize-deps-configure $PHPIZE_DEPS + fi +fi + +if command -v dpkg-architecture > /dev/null; then + gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" + set -- --build="$gnuArch" "$@" +fi + +cd "$ext" +phpize +./configure --enable-option-checking=fatal "$@" diff --git a/src/base/bin/docker-php-ext-enable b/src/base/bin/docker-php-ext-enable new file mode 100755 index 00000000..41d20bbe --- /dev/null +++ b/src/base/bin/docker-php-ext-enable @@ -0,0 +1,121 @@ +#!/bin/sh +set -e + +extDir="$(php -d 'display_errors=stderr' -r 'echo ini_get("extension_dir");')" +cd "$extDir" + +usage() { + echo "usage: $0 [options] module-name [module-name ...]" + echo " ie: $0 gd mysqli" + echo " $0 pdo pdo_mysql" + echo " $0 --ini-name 0-apc.ini apcu apc" + echo + echo 'Possible values for module-name:' + find -maxdepth 1 \ + -type f \ + -name '*.so' \ + -exec basename '{}' ';' \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +opts="$(getopt -o 'h?' --long 'help,ini-name:' -- "$@" || { usage >&2 && false; })" +eval set -- "$opts" + +iniName= +while true; do + flag="$1" + shift + case "$flag" in + --help|-h|'-?') usage && exit 0 ;; + --ini-name) iniName="$1" && shift ;; + --) break ;; + *) + { + echo "error: unknown flag: $flag" + usage + } >&2 + exit 1 + ;; + esac +done + +modules= +for module; do + if [ -z "$module" ]; then + continue + fi + if ! [ -f "$module" ] && ! [ -f "$module.so" ]; then + echo >&2 "error: '$module' does not exist" + echo >&2 + usage >&2 + exit 1 + fi + modules="$modules $module" +done + +if [ -z "$modules" ]; then + usage >&2 + exit 1 +fi + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +apkDel= +if [ "$pm" = 'apk' ]; then + if \ + [ -n "$PHPIZE_DEPS" ] \ + && ! apk info --installed .phpize-deps > /dev/null \ + && ! apk info --installed .phpize-deps-configure > /dev/null \ + ; then + apk add --no-cache --virtual '.docker-php-ext-enable-deps' binutils + apkDel='.docker-php-ext-enable-deps' + fi +fi + +for module in $modules; do + moduleFile="$module" + if [ -f "$module.so" ] && ! [ -f "$module" ]; then + moduleFile="$module.so" + fi + if readelf --wide --syms "$moduleFile" | grep -q ' zend_extension_entry$'; then + # https://wiki.php.net/internals/extensions#loading_zend_extensions + line="zend_extension=$module" + else + line="extension=$module" + fi + + ext="$(basename "$module")" + ext="${ext%.*}" + if php -d 'display_errors=stderr' -r 'exit(extension_loaded("'"$ext"'") ? 0 : 1);'; then + # this isn't perfect, but it's better than nothing + # (for example, 'opcache.so' presents inside PHP as 'Zend OPcache', not 'opcache') + echo >&2 + echo >&2 "warning: $ext ($module) is already loaded!" + echo >&2 + continue + fi + + case "$iniName" in + /*) + # allow an absolute path + ini="$iniName" + ;; + *) + ini="$PHP_INI_DIR/conf.d/${iniName:-"docker-php-ext-$ext.ini"}" + ;; + esac + if ! grep -qFx -e "$line" -e "$line.so" "$ini" 2>/dev/null; then + echo "$line" >> "$ini" + fi +done + +if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then + apk del --no-network $apkDel +fi diff --git a/src/base/bin/docker-php-ext-install b/src/base/bin/docker-php-ext-install new file mode 100755 index 00000000..fbf44849 --- /dev/null +++ b/src/base/bin/docker-php-ext-install @@ -0,0 +1,143 @@ +#!/bin/sh +set -e + +# prefer user supplied CFLAGS, but default to our PHP_CFLAGS +: ${CFLAGS:=$PHP_CFLAGS} +: ${CPPFLAGS:=$PHP_CPPFLAGS} +: ${LDFLAGS:=$PHP_LDFLAGS} +export CFLAGS CPPFLAGS LDFLAGS + +srcExists= +if [ -d /usr/src/php ]; then + srcExists=1 +fi +docker-php-source extract +if [ -z "$srcExists" ]; then + touch /usr/src/php/.docker-delete-me +fi + +cd /usr/src/php/ext + +usage() { + echo "usage: $0 [-jN] [--ini-name file.ini] ext-name [ext-name ...]" + echo " ie: $0 gd mysqli" + echo " $0 pdo pdo_mysql" + echo " $0 -j5 gd mbstring mysqli pdo pdo_mysql shmop" + echo + echo 'if custom ./configure arguments are necessary, see docker-php-ext-configure' + echo + echo 'Possible values for ext-name:' + find . \ + -mindepth 2 \ + -maxdepth 2 \ + -type f \ + -name 'config.m4' \ + | xargs -n1 dirname \ + | xargs -n1 basename \ + | sort \ + | xargs + echo + echo 'Some of the above modules are already compiled into PHP; please check' + echo 'the output of "php -i" to see which modules are already loaded.' +} + +opts="$(getopt -o 'h?j:' --long 'help,ini-name:,jobs:' -- "$@" || { usage >&2 && false; })" +eval set -- "$opts" + +j=1 +iniName= +while true; do + flag="$1" + shift + case "$flag" in + --help|-h|'-?') usage && exit 0 ;; + --ini-name) iniName="$1" && shift ;; + --jobs|-j) j="$1" && shift ;; + --) break ;; + *) + { + echo "error: unknown flag: $flag" + usage + } >&2 + exit 1 + ;; + esac +done + +exts= +for ext; do + if [ -z "$ext" ]; then + continue + fi + if [ ! -d "$ext" ]; then + echo >&2 "error: $PWD/$ext does not exist" + echo >&2 + usage >&2 + exit 1 + fi + exts="$exts $ext" +done + +if [ -z "$exts" ]; then + usage >&2 + exit 1 +fi + +pm='unknown' +if [ -e /lib/apk/db/installed ]; then + pm='apk' +fi + +apkDel= +if [ "$pm" = 'apk' ]; then + if [ -n "$PHPIZE_DEPS" ]; then + if apk info --installed .phpize-deps-configure > /dev/null; then + apkDel='.phpize-deps-configure' + elif ! apk info --installed .phpize-deps > /dev/null; then + apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS + apkDel='.phpize-deps' + fi + fi +fi + +popDir="$PWD" +for ext in $exts; do + cd "$ext" + + [ -e Makefile ] || docker-php-ext-configure "$ext" + + make -j"$j" + + if ! php -n -d 'display_errors=stderr' -r 'exit(ZEND_DEBUG_BUILD ? 0 : 1);' > /dev/null; then + # only "strip" modules if we aren't using a debug build of PHP + # (none of our builds are debug builds, but PHP might be recompiled with "--enable-debug" configure option) + # https://github.com/docker-library/php/issues/1268 + + find modules \ + -maxdepth 1 \ + -name '*.so' \ + -exec sh -euxc ' \ + strip --strip-all "$@" || : + ' -- '{}' + + fi + + make -j"$j" install + + find modules \ + -maxdepth 1 \ + -name '*.so' \ + -exec basename '{}' '.so' ';' \ + | xargs -r docker-php-ext-enable ${iniName:+--ini-name "$iniName"} + + make -j"$j" clean + + cd "$popDir" +done + +if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then + apk del --no-network $apkDel +fi + +if [ -e /usr/src/php/.docker-delete-me ]; then + docker-php-source delete +fi diff --git a/src/base/bin/docker-php-source b/src/base/bin/docker-php-source new file mode 100755 index 00000000..9033d243 --- /dev/null +++ b/src/base/bin/docker-php-source @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +dir=/usr/src/php + +usage() { + echo "usage: $0 COMMAND" + echo + echo "Manage php source tarball lifecycle." + echo + echo "Commands:" + echo " extract extract php source tarball into directory $dir if not already done." + echo " delete delete extracted php source located into $dir if not already done." + echo +} + +case "$1" in + extract) + mkdir -p "$dir" + if [ ! -f "$dir/.docker-extracted" ]; then + tar -Jxf /usr/src/php.tar.xz -C "$dir" --strip-components=1 + touch "$dir/.docker-extracted" + fi + ;; + + delete) + rm -rf "$dir" + ;; + + *) + usage + exit 1 + ;; +esac diff --git a/src/variations/cli/Dockerfile b/src/variations/cli/Dockerfile index c83b57a1..2c3ec9f5 100644 --- a/src/variations/cli/Dockerfile +++ b/src/variations/cli/Dockerfile @@ -1,11 +1,12 @@ ARG BASE_OS_VERSION='bookworm' ARG PHP_VERSION='8.5' ARG PHP_VARIATION='cli' +ARG BASE_IMAGE="php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION}" ########## # CLI: Main Image ########## -FROM php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION} +FROM ${BASE_IMAGE} ARG DEPENDENCY_PACKAGES_ALPINE="\ shadow" diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index 3f50a7b9..b6d8955a 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -20,7 +20,7 @@ RUN docker-php-serversideup-s6-install #################### # NGINX Signing Key #################### -FROM php:${PHP_VERSION}-fpm-${BASE_OS_VERSION} AS nginx-repo-config +FROM ${BASE_IMAGE} AS nginx-repo-config ARG SIGNING_KEY_URL="https://nginx.org/keys/nginx_signing.key" ARG SIGNING_FINGERPRINT="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62" diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index e0e9ec49..58678c04 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -1,11 +1,12 @@ ARG BASE_OS_VERSION='bookworm' ARG PHP_VERSION='8.5' ARG PHP_VARIATION='fpm' +ARG BASE_IMAGE="php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION}" ########## # FPM: Main Image ########## -FROM php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION} +FROM ${BASE_IMAGE} ARG DEPENDENCY_PACKAGES_ALPINE="\ fcgi \ From eaff8ff5f9928d681e31aaab944cf6b56ee0caeb Mon Sep 17 00:00:00 2001 From: Tom Schlick Date: Wed, 21 Oct 2026 18:31:43 -0400 Subject: [PATCH 3/3] Use set -e instead of set -oe in the other helper scripts "set -oe" still enables errexit, but it first prints the shell's option table to stdout, which adds a page of noise to every image build log and to the builds of anyone who calls these helpers in their own Dockerfile. Failure behavior is unchanged: a failed package, extension, or installer download already exited non-zero. --- .../usr/local/bin/docker-php-serversideup-dep-install-alpine | 2 +- .../usr/local/bin/docker-php-serversideup-dep-install-debian | 2 +- .../local/bin/docker-php-serversideup-install-php-ext-installer | 2 +- .../local/bin/docker-php-serversideup-install-php-extensions | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/usr/local/bin/docker-php-serversideup-dep-install-alpine b/src/common/usr/local/bin/docker-php-serversideup-dep-install-alpine index b9644df6..9bdbf212 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-dep-install-alpine +++ b/src/common/usr/local/bin/docker-php-serversideup-dep-install-alpine @@ -1,5 +1,5 @@ #!/bin/sh -set -oe +set -e ################################################### # Usage: docker-php-serversideup-dep-install-alpine [alpine-packages] diff --git a/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian b/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian index 967cf91f..90287ad3 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian +++ b/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian @@ -1,5 +1,5 @@ #!/bin/sh -set -oe +set -e ################################################### # Usage: docker-php-serversideup-dep-install-debian [debian-packages] diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index ad37020e..cd494a0c 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -1,5 +1,5 @@ #!/bin/sh -set -oe +set -e ################################################### # Usage: docker-php-serversideup-install-php-ext-installer [version] diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions b/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions index b27199ba..82110b86 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions @@ -1,5 +1,5 @@ #!/bin/sh -set -oe +set -e ################################################### # Usage: docker-php-serversideup-install-php-extensions [extensions] [overrides]