PHP workloads¶
How it works¶
Brewlet separates your PHP application from the PHP interpreter. You publish
your scripts, resolved Composer dependencies and an explicit launch descriptor
as an OCI artifact, without bundling PHP. The platform approves and provisions
a runtime image containing PHP, its extensions and its
Linux userland; your Kubernetes Deployment selects its runtime ID. Brewlet runs
your declared php command with application files read-only under /app.
Because the interpreter belongs to the node, a PHP security release is a platform change: approve the patched runtime image, then replace Pods. The application image digest stays the same. See rotate without rebuilding.
Tested runtimes¶
Brewlet's PHP example fixture
runs daily against main in the PHP example workflow.
That workflow packages the fixture, runs it on a real node with Docker Official
php:8.4.26-cli-alpine and php:8.4.27-cli-alpine runtime images, and rotates
between them. It does not gate pull requests or releases; Python is the only
qualification fixture (see verification).
The workflow on this page was also
verified manually with Brewlet 0.8.1 on a single-node arm64 kind cluster,
following the local tutorial, using the unmodified Docker
Official php:8.4.26-cli-alpine and php:8.4.27-cli-alpine images:
- The application served requests as UID 65532 with PHP 8.4.26.
- After the RuntimeImage was patched to 8.4.27, the running Pod kept 8.4.26. The replacement Pod reported 8.4.27 with the same application digest.
kubectl execwith/usr/local/bin/phpused the same runtime.
Choose the runtime image carefully:
- Alpine variants can be approved unchanged. The
-cli-alpineimages contain no set-ID binaries. - Debian variants are rejected.
php:<version>-clicontains setuid tools such aschage,suandmount. The provisioner reportsruntime setuid/setgid entry usr/bin/chagein its logs, and the NodeProfile staysProvisioning. Remove set-ID bits in your golden image pipeline, as the fixture Dockerfiles do, or use an Alpine variant. - Brewlet 0.8.1 cannot pull runtime images from
docker.io. The node provisioner fails with401 Unauthorized. Use the same digest from the Docker Official Images mirror atpublic.ecr.aws/docker/library/php, or copy the image into your platform registry.
These images are test inputs, not a security recommendation. Use the runtime
images your platform team approves. Extensions are part of the runtime: check
php -m in the image for every extension your application needs.
Build the payload¶
This example is a single script served by PHP's built-in web server:
WORK="$(mktemp -d)"
mkdir "$WORK/app"
cat > "$WORK/app/index.php" <<'EOF'
<?php
header('Content-Type: application/json');
echo json_encode([
'php' => PHP_VERSION,
'sapi' => PHP_SAPI,
'uid' => posix_getuid(),
]), "\n";
EOF
chmod 0755 "$WORK/app"
chmod 0644 "$WORK/app/index.php"
For a Composer project, run composer install --no-dev in your build pipeline
and package the resulting vendor/ directory. Never run Composer on the node or
at startup. Package dependencies with native code only if they match the
runtime's PHP version, libc and CPU architecture.
Launch descriptor¶
The Docker Official PHP images install PHP at /usr/local/bin/php. Save as
$WORK/launch.json:
{
"schemaVersion": 1,
"process": {
"entrypoint": [
"/usr/local/bin/php"
],
"args": [
"-S",
"0.0.0.0:8000",
"/app/index.php"
],
"workingDir": "/app"
}
}
The last argument makes index.php the router script for every request.
Package¶
Files must be world-readable and directories searchable (see producer filesystem requirements). Replace the registry and declare your actual platform:
brewlet build registry.example.com/apps/php-demo:v1 \
--platform linux/arm64 \
--launch "$WORK/launch.json" \
--layer "app=$WORK/app:/app"
brewlet push registry.example.com/apps/php-demo:v1 --push-result "$WORK/push.json"
To share vendor/ across releases, package it as a separate layer, for example
--layer "dependency=$WORK/vendor:/app/vendor". See
dependency layer sets.
Approve the runtime¶
The platform team approves the runtime by digest and offers it on a pool:
apiVersion: node.brewlet.sh/v1alpha1
kind: RuntimeImage
metadata:
name: php-8.4
spec:
source:
image: public.ecr.aws/docker/library/php@sha256:<php-8.4-cli-alpine-digest>
user: {uid: 65532, gid: 65532}
Add php-8.4 to a NodeProfile's runtimeImages, as described in
runtime images and pools.
Native deployment¶
Use the digest from push.json and the approved runtime ID:
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-demo
namespace: apps
spec:
replicas: 1
selector:
matchLabels: {app: php-demo}
template:
metadata:
labels: {app: php-demo}
annotations:
brewlet.sh/runtime-image: php-8.4
spec:
runtimeClassName: brewlet
nodeSelector: {kubernetes.io/arch: arm64}
containers:
- name: app
image: registry.example.com/apps/php-demo@sha256:<application-digest>
securityContext: {readOnlyRootFilesystem: true}
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {cpu: "1", memory: 256Mi}
readinessProbe:
httpGet: {path: /, port: 8000}
Verify¶
kubectl -n apps rollout status deployment/php-demo --timeout=5m
kubectl -n apps exec deployment/php-demo -- /usr/local/bin/php -v
kubectl -n apps port-forward deployment/php-demo 18080:8000
From another terminal, curl --fail http://127.0.0.1:18080/ should report
the runtime's PHP version, "sapi":"cli-server" and "uid":65532.
Apply a PHP security release¶
When PHP publishes a security release, approve the patched image digest under the same runtime ID, wait for rotation, then replace Pods:
kubectl patch runtimeimage php-8.4 --type=merge \
-p '{"spec":{"source":{"image":"public.ecr.aws/docker/library/php@sha256:<patched-digest>"}}}'
kubectl wait runtimeimage/php-8.4 --for=condition=RotationComplete --timeout=5m
kubectl -n apps rollout restart deployment/php-demo
kubectl -n apps rollout status deployment/php-demo --timeout=5m
brewlet k8s runtime status php-8.4
Running Pods keep the old interpreter until they are replaced. Use
brewlet k8s runtime status to find Pods still bound to an older generation,
or opt in to restart on rotation. A request sent during the
rollout can still reach a terminating Pod with the old version.
Pitfalls and limits¶
- PHP's built-in web server is for evaluation, not production. PHP-FPM needs a separate web server, which this guide does not cover.
- The runtime root and
/appare read-only. Sessions, uploads, OPcache file caches and logs need approved writable volumes and matchingphp.inior-dsettings. - Extensions and
php.inidefaults come from the runtime image, not your application. A runtime change can alter them. - A runtime-only update does not update Composer dependencies. Rebuild the application for those.