From 0405a562da2841502c05e01f7fb3977193d2f18a Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Sun, 22 Feb 2026 19:50:45 +0000 Subject: [PATCH 1/9] feat(aloft): dockerfile, docker-bake and updated ServeCommand, official docker images for TempestPHP --- packages/aloft/composer.json | 22 +++ packages/aloft/docker/Caddyfile.noworker | 56 ++++++ packages/aloft/docker/Dockerfile | 83 +++++++++ packages/aloft/docker/docker-bake.hcl | 88 +++++++++ packages/aloft/docker/stage-files.sh | 168 ++++++++++++++++++ packages/aloft/src/Commands/ServeCommand.php | 75 ++++++++ .../{router => aloft}/src/Commands/router.php | 0 packages/aloft/tests/tests.txt | 1 + packages/router/src/Commands/ServeCommand.php | 33 ---- 9 files changed, 493 insertions(+), 33 deletions(-) create mode 100644 packages/aloft/composer.json create mode 100644 packages/aloft/docker/Caddyfile.noworker create mode 100644 packages/aloft/docker/Dockerfile create mode 100644 packages/aloft/docker/docker-bake.hcl create mode 100644 packages/aloft/docker/stage-files.sh create mode 100644 packages/aloft/src/Commands/ServeCommand.php rename packages/{router => aloft}/src/Commands/router.php (100%) create mode 100644 packages/aloft/tests/tests.txt delete mode 100644 packages/router/src/Commands/ServeCommand.php diff --git a/packages/aloft/composer.json b/packages/aloft/composer.json new file mode 100644 index 0000000000..e31d1251e0 --- /dev/null +++ b/packages/aloft/composer.json @@ -0,0 +1,22 @@ +{ + "name": "tempest/aloft", + "description": "Development and Production webserver Dockerfiles and utilities for TempestPHP.", + "require": { + "php": "^8.5", + "tempest/core": "3.x-dev", + "tempest/router": "3.x-dev" + }, + "require-dev": {}, + "autoload": { + "psr-4": { + "Tempest\\Aloft\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tempest\\Aloft\\Tests\\": "tests" + } + }, + "license": "MIT", + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/packages/aloft/docker/Caddyfile.noworker b/packages/aloft/docker/Caddyfile.noworker new file mode 100644 index 0000000000..b18c86cd39 --- /dev/null +++ b/packages/aloft/docker/Caddyfile.noworker @@ -0,0 +1,56 @@ +# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server. +# This Caddyfile is provided by the TempestPHP Framework with some added options for convenience. +# +# https://github.com/tempestphp/tempest-framework +# https://frankenphp.dev/docs/config +# https://caddyserver.com/docs/caddyfile + +{ + skip_install_trust + {$CADDY_DEFAULT_BIND} + http_port {$HTTP_PORT:8000} + https_port {$HTTPS_PORT:8443} + + {$CADDY_GLOBAL_OPTIONS} + + frankenphp { + {$FRANKENPHP_CONFIG} + } +} + +{$CADDY_EXTRA_CONFIG} + +{$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTP_PORT:8000}, {$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTPS_PORT:8443} { + #log { + # # Redact the authorization query parameter that can be set by Mercure + # format filter { + # request>uri query { + # replace authorization REDACTED + # } + # } + #} + + root {$CADDY_SERVER_ROOT:public/} + encode zstd br gzip + + # Uncomment the following lines to enable Mercure and Vulcain modules + #mercure { + # # Publisher JWT key + # publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG} + # # Subscriber JWT key + # subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG} + # # Allow anonymous subscribers (double-check that it's what you want) + # anonymous + # # Enable the subscription API (double-check that it's what you want) + # subscriptions + # # Extra directives + # {$MERCURE_EXTRA_DIRECTIVES} + #} + #vulcain + + {$CADDY_SERVER_EXTRA_DIRECTIVES} + + php_server { + #worker /path/to/your/worker.php + } +} \ No newline at end of file diff --git a/packages/aloft/docker/Dockerfile b/packages/aloft/docker/Dockerfile new file mode 100644 index 0000000000..e4b59390a8 --- /dev/null +++ b/packages/aloft/docker/Dockerfile @@ -0,0 +1,83 @@ +# Build-time arguments — set by bake.hcl, can be overridden individually +ARG FRANKENPHP_VERSION=1.11.2 +ARG PHP_VERSION=8.5.3 +ARG BASE_IMAGE=dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} +# Controls which distroless variant is used as the runner base. +# Valid values mirror gcr.io/distroless/cc-debian13 tags: nonroot | debug-nonroot +ARG DISTROLESS_VARIANT=nonroot + +FROM ${BASE_IMAGE} AS frankenphp + +RUN curl -sSLf \ + -o /usr/local/bin/install-php-extensions \ + https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ + chmod +x /usr/local/bin/install-php-extensions + +# Install additional extensions here and they are carried forward into the final image +RUN install-php-extensions \ + gd \ + intl \ + mysqli \ + pcntl \ + pdo_mysql \ + pdo_pgsql \ + pdo_sqlite \ + redis \ + zip + +# Install pax-utils for lddtree +RUN apt-get update && apt-get install -y --no-install-recommends pax-utils && rm -rf /var/lib/apt/lists/* + +# Copy and run the staging script which collects all runtime files into +# /tmp/staging with full paths preserved +COPY stage-files.sh /tmp/stage-files.sh +RUN chmod +x /tmp/stage-files.sh && /tmp/stage-files.sh + +# Re-declare so it is in scope for this stage (ARGs declared before the first +# FROM are not automatically visible inside stages) +ARG DISTROLESS_VARIANT=nonroot + +# Grab distroless image — variant is controlled by DISTROLESS_VARIANT ARG +FROM gcr.io/distroless/cc-debian13:${DISTROLESS_VARIANT} AS common + +# See https://caddyserver.com/docs/conventions#file-locations for details +ENV XDG_CONFIG_HOME=/config +ENV XDG_DATA_HOME=/data + +# Required from frankenphp +ENV GODEBUG=cgocheck=0 + +LABEL org.opencontainers.image.title=TempestPHP +LABEL org.opencontainers.image.description="The framework that gets out of your way" +LABEL org.opencontainers.image.url=https://tempestphp.com +LABEL org.opencontainers.image.source=https://github.com/tempestphp/tempest-framework/ +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.vendor="Brent Roose and contributors" + +# All libs, binaries and config collected by stage-files.sh into /tmp/staging. +# Everything is normalised to usr/ paths so this single COPY is safe against +# the distroless /lib -> usr/lib symlink. +COPY --from=frankenphp /tmp/staging/ / + +# App directories need specific ownership — distroless has no chown so we +# use the COPY --chown flag. These overwrite the copies already in /tmp/staging. +# From the gcr container, the nonroot user is uid 1002, with gid 1000 +COPY --from=frankenphp --chown=1002:1000 /app /app +COPY --from=frankenphp --chown=1002:1000 /config /config +COPY --from=frankenphp --chown=1002:1000 /data /data +COPY --from=frankenphp --chown=1002:1000 /etc/frankenphp /etc/frankenphp +COPY --from=frankenphp --chown=1002:1000 /app/public/index.php /app/public/index.php + +COPY Caddyfile.noworker /etc/frankenphp/Caddyfile + +WORKDIR /app + +EXPOSE 8000 +EXPOSE 8443 +EXPOSE 8443/udp +EXPOSE 2019 + +USER nonroot + +CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] +HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1 diff --git a/packages/aloft/docker/docker-bake.hcl b/packages/aloft/docker/docker-bake.hcl new file mode 100644 index 0000000000..cb803f9514 --- /dev/null +++ b/packages/aloft/docker/docker-bake.hcl @@ -0,0 +1,88 @@ +# ----------------------------------------------------------------------- +# Variables — override via env vars or --set on the CLI +# e.g. FRANKENPHP_VERSION=1.12.0 docker buildx bake +# ----------------------------------------------------------------------- + +variable "FRANKENPHP_VERSION" { + description = "FrankenPHP release version" + default = "1.11.2" +} + +variable "PHP_VERSION" { + description = "PHP release version" + default = "8.5.3" +} + +variable "PUSH" { + default = "0" +} + +# Derived values — not meant to be overridden directly +variable "BASE_IMAGE" { + default = "dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}" +} + +variable "VERSION_TAG" { + default = "${FRANKENPHP_VERSION}-${PHP_VERSION}" +} + +# ----------------------------------------------------------------------- +# Shared platform target — all runner targets inherit from this +# ----------------------------------------------------------------------- + +target "_common" { + dockerfile = "Dockerfile" + context = "." + platforms = PUSH == "1" ? ["linux/amd64", "linux/arm64"] : [] + output = PUSH == "1" ? ["type=registry"] : ["type=docker"] + args = { + FRANKENPHP_VERSION = FRANKENPHP_VERSION + PHP_VERSION = PHP_VERSION + BASE_IMAGE = BASE_IMAGE + } +} + +# ----------------------------------------------------------------------- +# latest-nonroot — runner is gcr.io/distroless/cc-debian13:nonroot +# Tags: tempestphp/aloft:latest-nonroot +# tempestphp/aloft:1.11.2-8.5.3-nonroot +# ----------------------------------------------------------------------- + +target "latest-nonroot" { + inherits = ["_common"] + target = "common" + args = { + DISTROLESS_VARIANT = "nonroot" + } + tags = [ + "tempestphp/aloft:latest-nonroot", + "tempestphp/aloft:${VERSION_TAG}-nonroot", + ] +} + +# ----------------------------------------------------------------------- +# debug-nonroot — runner is gcr.io/distroless/cc-debian13:debug-nonroot +# Includes busybox shell for exec access while still running as nonroot. +# Tags: tempestphp/aloft:debug-nonroot +# tempestphp/aloft:1.11.2-8.5.3-debug-nonroot +# ----------------------------------------------------------------------- + +target "debug-nonroot" { + inherits = ["_common"] + target = "common" + args = { + DISTROLESS_VARIANT = "debug-nonroot" + } + tags = [ + "tempestphp/aloft:debug-nonroot", + "tempestphp/aloft:${VERSION_TAG}-debug-nonroot", + ] +} + +# ----------------------------------------------------------------------- +# Default group — builds both variants in parallel +# ----------------------------------------------------------------------- + +group "default" { + targets = ["latest-nonroot", "debug-nonroot"] +} diff --git a/packages/aloft/docker/stage-files.sh b/packages/aloft/docker/stage-files.sh new file mode 100644 index 0000000000..3b47c45580 --- /dev/null +++ b/packages/aloft/docker/stage-files.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# stage-files.sh +# +# Runs inside the frankenphp intermediate stage during docker build. +# Collects every runtime file needed in the final distroless image into +# /tmp/staging, preserving full filesystem paths so the Dockerfile can +# use a single COPY --from=frankenphp /tmp/staging/ / +# +# Steps: +# 1. lddtree --copy-to-tree for extensions, php, frankenphp +# 2. Resolve soname symlinks → copy versioned targets beside them +# 3. Normalise lib/ → usr/lib/ (distroless has /lib -> usr/lib symlink) +# 4. Explicit copies for dlopen'd plugins lddtree cannot discover +# 5. PHP config, ld config, mime types, frankenphp runtime files + +set -euo pipefail + +STAGING=/tmp/staging +ARCH=$(uname -m | sed 's/x86_64/x86_64-linux-gnu/;s/aarch64/aarch64-linux-gnu/') +DEB_DIR=/tmp/debs +DEB_EXTRACT=/tmp/deb-extract + +mkdir -p "${STAGING}" "${DEB_DIR}" + +# --------------------------------------------------------------------------- +# 1. lddtree — ELF dependency tree for all extensions + binaries +# +# This gives us the soname symlinks and the correct set of libraries needed. +# The versioned files behind those symlinks are fetched via apt in step 2. +# --------------------------------------------------------------------------- + +find /usr/local/lib/php/extensions/ -name '*.so' -print0 \ + | xargs -0 -r lddtree --copy-to-tree "${STAGING}" 2>/dev/null || true + +lddtree --copy-to-tree "${STAGING}" /usr/local/bin/php 2>/dev/null || true +lddtree --copy-to-tree "${STAGING}" /usr/local/bin/frankenphp 2>/dev/null || true +lddtree --copy-to-tree "${STAGING}" /usr/local/lib/libphp.so 2>/dev/null || true + +# Normalise lib/ → usr/lib/ before package resolution so path lookups work +if [ -d "${STAGING}/lib" ]; then + mkdir -p "${STAGING}/usr/lib" + cp -a "${STAGING}/lib/." "${STAGING}/usr/lib/" + rm -rf "${STAGING}/lib" +fi + +# --------------------------------------------------------------------------- +# 2. apt-get download + dpkg-deb extract +# +# lddtree creates relative soname symlinks inside staging, so the versioned +# file each symlink points to only exists on the source system, not in +# staging. Rather than trying to resolve paths manually, we find which +# Debian package owns each collected .so file, download that package, and +# extract it — giving us both the soname symlink and the versioned file +# with no path gymnastics required. +# --------------------------------------------------------------------------- + +# Collect owning packages for all .so files lddtree placed in staging +find "${STAGING}/usr/lib" -name "*.so*" 2>/dev/null \ + | sed "s|${STAGING}||" \ + | xargs -r dpkg -S 2>/dev/null \ + | cut -d: -f1 \ + | sort -u > /tmp/pkgs-needed.txt + +# Also add explicit packages for dlopen'd libs lddtree can't discover +# (kerberos plugins, sasl plugins, libjansson for FrankenPHP admin API) +cat >> /tmp/pkgs-needed.txt << 'EOF' +libjansson4 +libkrb5-3 +libsasl2-2 +EOF + +sort -u /tmp/pkgs-needed.txt > /tmp/pkgs-deduped.txt + +# Download debs (failures are non-fatal — some names may vary by suite) +cd "${DEB_DIR}" +while read -r pkg; do + apt-get download "${pkg}" 2>/dev/null || true +done < /tmp/pkgs-deduped.txt + +# Extract only usr/lib from each deb into staging +for deb in "${DEB_DIR}"/*.deb; do + [ -f "${deb}" ] || continue + rm -rf "${DEB_EXTRACT}" + mkdir -p "${DEB_EXTRACT}" + dpkg-deb -x "${deb}" "${DEB_EXTRACT}" + if [ -d "${DEB_EXTRACT}/usr/lib" ]; then + cp -a "${DEB_EXTRACT}/usr/lib/." "${STAGING}/usr/lib/" + fi +done +rm -rf "${DEB_EXTRACT}" "${DEB_DIR}" + +# --------------------------------------------------------------------------- +# 3. dlopen'd plugin directories +# +# These are subdirectories loaded at runtime via dlopen() — dpkg-deb extract +# above handles the files, but ensure the directories land correctly. +# --------------------------------------------------------------------------- + +# Kerberos pre-authentication plugins +if [ -d "/usr/lib/${ARCH}/krb5" ]; then + cp -a "/usr/lib/${ARCH}/krb5" "${STAGING}/usr/lib/${ARCH}/" +fi + +# SASL mechanism plugins +if [ -d "/usr/lib/${ARCH}/sasl2" ]; then + cp -a "/usr/lib/${ARCH}/sasl2" "${STAGING}/usr/lib/${ARCH}/" + mkdir -p "${STAGING}/usr/lib/sasl2" + cp -a "/usr/lib/${ARCH}/sasl2/." "${STAGING}/usr/lib/sasl2/" +fi + +# libjansson — dlopen'd by FrankenPHP admin API, not in any DT_NEEDED chain. +# Copy directly from the source filesystem; apt-get download is unreliable +# here since libjansson4 may not be registered in the image's dpkg database. +find "/usr/lib/${ARCH}" -maxdepth 1 -name 'libjansson.so*' | while read -r f; do + dest="${STAGING}/usr/lib/${ARCH}/$(basename "${f}")" + [ -e "${dest}" ] && continue + cp -a "${f}" "${dest}" +done + +# --------------------------------------------------------------------------- +# 5. PHP runtime files +# --------------------------------------------------------------------------- + +# Main PHP shared library (already collected via lddtree above, belt+suspenders) +mkdir -p "${STAGING}/usr/local/lib" +[ -f "${STAGING}/usr/local/lib/libphp.so" ] || \ + cp /usr/local/lib/libphp.so "${STAGING}/usr/local/lib/" + +# libwatcher (FrankenPHP file watcher) +cp -a /usr/local/lib/libwatcher* "${STAGING}/usr/local/lib/" + +# PHP extension .so files (already under staging from lddtree but confirm) +mkdir -p "${STAGING}/usr/local/lib/php/extensions" +cp -a /usr/local/lib/php/extensions/. "${STAGING}/usr/local/lib/php/extensions/" + +# PHP configuration +mkdir -p "${STAGING}/usr/local/etc/php/conf.d" +cp -a /usr/local/etc/php/. "${STAGING}/usr/local/etc/php/" + +# --------------------------------------------------------------------------- +# 6. Dynamic linker config +# --------------------------------------------------------------------------- + +mkdir -p "${STAGING}/etc/ld.so.conf.d" +[ -f /etc/ld.so.conf ] && cp /etc/ld.so.conf "${STAGING}/etc/" +[ -f /etc/ld.so.cache ] && cp /etc/ld.so.cache "${STAGING}/etc/" +cp -a /etc/ld.so.conf.d/. "${STAGING}/etc/ld.so.conf.d/" + +# --------------------------------------------------------------------------- +# 7. Misc runtime config +# --------------------------------------------------------------------------- + +# MIME types (FrankenPHP/Caddy uses this for content-type detection) +[ -f /etc/mime.types ] && cp /etc/mime.types "${STAGING}/etc/" + + +FILE_COUNT=$(find "${STAGING}" -type f | wc -l) +LINK_COUNT=$(find "${STAGING}" -type l | wc -l) +echo "✓ Staging complete — ${FILE_COUNT} files, ${LINK_COUNT} symlinks collected" + +# Fail fast if any symlinks are dangling — a versioned lib target missing +# from staging means the final image would have broken .so links at runtime +DANGLING=$(find "${STAGING}" -type l ! -exec test -e {} \; -print) +if [ -n "${DANGLING}" ]; then + echo "✗ Dangling symlinks found in staging:" >&2 + echo "${DANGLING}" >&2 + exit 1 +fi diff --git a/packages/aloft/src/Commands/ServeCommand.php b/packages/aloft/src/Commands/ServeCommand.php new file mode 100644 index 0000000000..132fa0f43a --- /dev/null +++ b/packages/aloft/src/Commands/ServeCommand.php @@ -0,0 +1,75 @@ +contains(':')) { + [$rawHost, $overriddenPort] = explode(':', $resolvedHost->toString(), limit: 2); + + $resolvedHost = new ImmutableString($rawHost ?: '127.0.0.1'); + $resolvedPort = (int) Number\parse($overriddenPort, default: $port); + } + + if ($aloft) { + $this->serveAloft($resolvedHost, $resolvedPort, $resolvedPublicDir); + } else { + $this->serveBuiltin($resolvedHost, $resolvedPort, $resolvedPublicDir); + } + } + + private function serveBuiltin(ImmutableString $host, int $port, ImmutableString $publicDir): void + { + $routerFile = new ImmutableString(__DIR__ . '/router.php'); + + passthru("php -S {$host}:{$port} -t {$publicDir} {$routerFile}"); + } + + private function serveAloft(ImmutableString $host, int $port, ImmutableString $publicDir): void + { + passthru( + "docker run --rm -it -p 80:8000 -p 443:8443 -p 443:8443/udp \ + -v " + . root_path() + . ":/app \ + -v " + . root_path('.tempest/aloft/data') + . ":/data \ + -v " + . root_path('.tempest/aloft/config') + . ":/config \ + -v /Users/iamdadmin/Dev/vewe/:/Users/iamdadmin/Dev/vewe/ \ + tempestphp/aloft:latest-nonroot", + ); + } + } +} diff --git a/packages/router/src/Commands/router.php b/packages/aloft/src/Commands/router.php similarity index 100% rename from packages/router/src/Commands/router.php rename to packages/aloft/src/Commands/router.php diff --git a/packages/aloft/tests/tests.txt b/packages/aloft/tests/tests.txt new file mode 100644 index 0000000000..e1a7747d75 --- /dev/null +++ b/packages/aloft/tests/tests.txt @@ -0,0 +1 @@ +Tests are TBC as it may not be appropriate to use PHPUnit to test here \ No newline at end of file diff --git a/packages/router/src/Commands/ServeCommand.php b/packages/router/src/Commands/ServeCommand.php deleted file mode 100644 index 76bf4b01d9..0000000000 --- a/packages/router/src/Commands/ServeCommand.php +++ /dev/null @@ -1,33 +0,0 @@ - Date: Sun, 22 Feb 2026 20:49:00 +0000 Subject: [PATCH 2/9] feat(aloft): removed a hardcoded path which wasn't needed at all --- packages/aloft/src/Commands/ServeCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/aloft/src/Commands/ServeCommand.php b/packages/aloft/src/Commands/ServeCommand.php index 132fa0f43a..85c527ab14 100644 --- a/packages/aloft/src/Commands/ServeCommand.php +++ b/packages/aloft/src/Commands/ServeCommand.php @@ -67,7 +67,6 @@ private function serveAloft(ImmutableString $host, int $port, ImmutableString $p -v " . root_path('.tempest/aloft/config') . ":/config \ - -v /Users/iamdadmin/Dev/vewe/:/Users/iamdadmin/Dev/vewe/ \ tempestphp/aloft:latest-nonroot", ); } From 206c02d18593f2a7b88b10836ddccbe8117cbc7b Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 23 Feb 2026 05:43:28 +0000 Subject: [PATCH 3/9] feat(aloft): resolved inconsistencies and typos, added .dockerignore initial, moved ServeCommand back to router package --- packages/aloft/composer.json | 2 +- packages/aloft/docker/.dockerignore | 66 +++++++++++++++++++ packages/aloft/docker/Caddyfile.noworker | 4 +- packages/aloft/docker/Dockerfile | 1 - .../aloft/src/Commands/RequireCommand.php | 0 .../src/Commands/ServeCommand.php | 2 +- .../{aloft => router}/src/Commands/router.php | 0 7 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 packages/aloft/docker/.dockerignore create mode 100644 packages/aloft/src/Commands/RequireCommand.php rename packages/{aloft => router}/src/Commands/ServeCommand.php (98%) rename packages/{aloft => router}/src/Commands/router.php (100%) diff --git a/packages/aloft/composer.json b/packages/aloft/composer.json index e31d1251e0..686001f82c 100644 --- a/packages/aloft/composer.json +++ b/packages/aloft/composer.json @@ -4,7 +4,7 @@ "require": { "php": "^8.5", "tempest/core": "3.x-dev", - "tempest/router": "3.x-dev" + "tempest/support": "3.x-dev" }, "require-dev": {}, "autoload": { diff --git a/packages/aloft/docker/.dockerignore b/packages/aloft/docker/.dockerignore new file mode 100644 index 0000000000..27bf69a9d3 --- /dev/null +++ b/packages/aloft/docker/.dockerignore @@ -0,0 +1,66 @@ +# ----------------------------------------------------------------------- +# Git +# ----------------------------------------------------------------------- +.git +.gitignore +.gitattributes + +# ----------------------------------------------------------------------- +# Docker build tooling (never needed inside the image) +# ----------------------------------------------------------------------- +Dockerfile +docker-bake.hcl +.dockerignore + +# ----------------------------------------------------------------------- +# CI / tooling config +# ----------------------------------------------------------------------- +.github +.gitlab-ci.yml +.travis.yml +.editorconfig +.env* +!.env.example + +# ----------------------------------------------------------------------- +# PHP tooling and dev dependencies +# ----------------------------------------------------------------------- +/vendor +composer.lock + +# ----------------------------------------------------------------------- +# Node (if any frontend tooling is present) +# ----------------------------------------------------------------------- +node_modules +npm-debug.log +yarn-error.log + +# ----------------------------------------------------------------------- +# Tests +# ----------------------------------------------------------------------- +/tests +/test +phpunit.xml +phpunit.xml.dist +.phpunit.result.cache +.phpunit.cache + +# ----------------------------------------------------------------------- +# Static analysis and code style +# ----------------------------------------------------------------------- +.php-cs-fixer.cache +.php-cs-fixer.php +phpstan.neon +phpstan.neon.dist +psalm.xml +psalm.xml.dist + +# ----------------------------------------------------------------------- +# IDE and OS noise +# ----------------------------------------------------------------------- +.idea +.vscode +*.swp +*.swo +.DS_Store +Thumbs.db diff --git a/packages/aloft/docker/Caddyfile.noworker b/packages/aloft/docker/Caddyfile.noworker index b18c86cd39..cced0a3ca9 100644 --- a/packages/aloft/docker/Caddyfile.noworker +++ b/packages/aloft/docker/Caddyfile.noworker @@ -8,8 +8,8 @@ { skip_install_trust {$CADDY_DEFAULT_BIND} - http_port {$HTTP_PORT:8000} - https_port {$HTTPS_PORT:8443} + http_port {$CADDY_HTTP_PORT:8000} + https_port {$CADDY_HTTPS_PORT:8443} {$CADDY_GLOBAL_OPTIONS} diff --git a/packages/aloft/docker/Dockerfile b/packages/aloft/docker/Dockerfile index e4b59390a8..496a80a788 100644 --- a/packages/aloft/docker/Dockerfile +++ b/packages/aloft/docker/Dockerfile @@ -66,7 +66,6 @@ COPY --from=frankenphp --chown=1002:1000 /app /app COPY --from=frankenphp --chown=1002:1000 /config /config COPY --from=frankenphp --chown=1002:1000 /data /data COPY --from=frankenphp --chown=1002:1000 /etc/frankenphp /etc/frankenphp -COPY --from=frankenphp --chown=1002:1000 /app/public/index.php /app/public/index.php COPY Caddyfile.noworker /etc/frankenphp/Caddyfile diff --git a/packages/aloft/src/Commands/RequireCommand.php b/packages/aloft/src/Commands/RequireCommand.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/aloft/src/Commands/ServeCommand.php b/packages/router/src/Commands/ServeCommand.php similarity index 98% rename from packages/aloft/src/Commands/ServeCommand.php rename to packages/router/src/Commands/ServeCommand.php index 85c527ab14..52468490e7 100644 --- a/packages/aloft/src/Commands/ServeCommand.php +++ b/packages/router/src/Commands/ServeCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Aloft\Commands; +namespace Tempest\Router\Commands; use Tempest\Console\ConsoleArgument; use Tempest\Console\ConsoleCommand; diff --git a/packages/aloft/src/Commands/router.php b/packages/router/src/Commands/router.php similarity index 100% rename from packages/aloft/src/Commands/router.php rename to packages/router/src/Commands/router.php From 92e58cdd73419c152414df8406bf90ac603c1f24 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 25 Feb 2026 05:46:28 +0000 Subject: [PATCH 4/9] feat(aloft): added temporary usage notes (pre-docs) for testing and development, fixed issue in stage-files under amd64 --- packages/aloft/docker/docker-bake.hcl | 17 +++++++--- packages/aloft/docker/stage-files.sh | 13 ++++++-- packages/aloft/docker/usage-notes.md | 47 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 packages/aloft/docker/usage-notes.md diff --git a/packages/aloft/docker/docker-bake.hcl b/packages/aloft/docker/docker-bake.hcl index cb803f9514..68c29dd687 100644 --- a/packages/aloft/docker/docker-bake.hcl +++ b/packages/aloft/docker/docker-bake.hcl @@ -17,6 +17,15 @@ variable "PUSH" { default = "0" } +variable "REGISTRY" { + default = "" +} + +# Derived — prepends registry if set, otherwise just the image name +variable "IMAGE" { + default = REGISTRY != "" ? "${REGISTRY}/aloft" : "tempestphp/aloft" +} + # Derived values — not meant to be overridden directly variable "BASE_IMAGE" { default = "dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}" @@ -55,8 +64,8 @@ target "latest-nonroot" { DISTROLESS_VARIANT = "nonroot" } tags = [ - "tempestphp/aloft:latest-nonroot", - "tempestphp/aloft:${VERSION_TAG}-nonroot", + "${IMAGE}:latest-nonroot", + "${IMAGE}:${VERSION_TAG}-nonroot", ] } @@ -74,8 +83,8 @@ target "debug-nonroot" { DISTROLESS_VARIANT = "debug-nonroot" } tags = [ - "tempestphp/aloft:debug-nonroot", - "tempestphp/aloft:${VERSION_TAG}-debug-nonroot", + "${IMAGE}:debug-nonroot", + "${IMAGE}:${VERSION_TAG}-debug-nonroot", ] } diff --git a/packages/aloft/docker/stage-files.sh b/packages/aloft/docker/stage-files.sh index 3b47c45580..8d602b400e 100644 --- a/packages/aloft/docker/stage-files.sh +++ b/packages/aloft/docker/stage-files.sh @@ -9,7 +9,7 @@ # Steps: # 1. lddtree --copy-to-tree for extensions, php, frankenphp # 2. Resolve soname symlinks → copy versioned targets beside them -# 3. Normalise lib/ → usr/lib/ (distroless has /lib -> usr/lib symlink) +# 3. Normalise lib/ and lib64/ → usr/lib/ and usr/lib64/ (distroless symlink collision) # 4. Explicit copies for dlopen'd plugins lddtree cannot discover # 5. PHP config, ld config, mime types, frankenphp runtime files @@ -36,13 +36,22 @@ lddtree --copy-to-tree "${STAGING}" /usr/local/bin/php 2>/dev/null || tru lddtree --copy-to-tree "${STAGING}" /usr/local/bin/frankenphp 2>/dev/null || true lddtree --copy-to-tree "${STAGING}" /usr/local/lib/libphp.so 2>/dev/null || true -# Normalise lib/ → usr/lib/ before package resolution so path lookups work +# Normalise lib/ and lib64/ → usr/lib/ and usr/lib64/ before package resolution. +# lddtree follows the /lib -> usr/lib and /lib64 -> usr/lib64 symlinks on the +# source system and may write files under staging/lib/ or staging/lib64/. +# distroless has both as symlinks so COPY / would collide — merge into usr/. if [ -d "${STAGING}/lib" ]; then mkdir -p "${STAGING}/usr/lib" cp -a "${STAGING}/lib/." "${STAGING}/usr/lib/" rm -rf "${STAGING}/lib" fi +if [ -d "${STAGING}/lib64" ]; then + mkdir -p "${STAGING}/usr/lib64" + cp -a "${STAGING}/lib64/." "${STAGING}/usr/lib64/" + rm -rf "${STAGING}/lib64" +fi + # --------------------------------------------------------------------------- # 2. apt-get download + dpkg-deb extract # diff --git a/packages/aloft/docker/usage-notes.md b/packages/aloft/docker/usage-notes.md new file mode 100644 index 0000000000..f16eb13d93 --- /dev/null +++ b/packages/aloft/docker/usage-notes.md @@ -0,0 +1,47 @@ +# Usage notes + +NB: This file is NOT FINAL and will be removed from the PR this is just so that the image can be tested in development + +## Build locally + +cd into the folder +```bash +docker buildx bake +``` +After build you should have something similar, disk usage will vary slightly based on your arch (this is aarch64 on macos m1 17.x). +```bash +docker % docker image ls + +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +gcr.io/distroless/cc-debian13:debug-nonroot f60c5a64690d 38.5MB 0B +gcr.io/distroless/cc-debian13:nonroot 5c5da034ed6e 37.2MB 0B +tempestphp/aloft:1.11.2-8.5.3-debug-nonroot 1bf5840bddb2 219MB 0B +tempestphp/aloft:1.11.2-8.5.3-nonroot a5039ddf9345 218MB 0B +tempestphp/aloft:debug-nonroot 1bf5840bddb2 219MB 0B +tempestphp/aloft:latest-nonroot a5039ddf9345 218MB 0B +``` + +## Push to a registry + +cd into the folder +```bash +PUSH=1 REGISTRY=registry.url/tempestphp docker buildx bake +``` + +View on your registry, but should create the four tags and two images. + +e.g. I pushed to my private gitea + +tempestphp/aloft/versions: + +1.11.2-8.5.3-debug-nonroot +Published 16 hours ago by iamdadmin + +latest-nonroot +Published 16 hours ago by iamdadmin + +1.11.2-8.5.3-nonroot +Published 16 hours ago by iamdadmin + +debug-nonroot +Published 16 hours ago by iamdadmin From f201e1377441bfb27b427b34bee025cc74ccde1e Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 4 Mar 2026 20:33:52 +0000 Subject: [PATCH 5/9] feat(aloft): updated dockerfile and created utility commands --- composer.json | 5 +- packages/aloft/docker/Dockerfile | 82 -------- packages/aloft/docker/docker-bake.hcl | 97 ---------- packages/aloft/docker/stage-files.sh | 177 ------------------ packages/aloft/docker/usage-notes.md | 47 ----- packages/aloft/src/AloftBuildCommand.php | 77 ++++++++ packages/aloft/src/AloftPublishCommand.php | 64 +++++++ packages/aloft/src/AloftServeCommand.php | 79 ++++++++ .../aloft/src/Commands/RequireCommand.php | 0 .../aloft/{docker => stubs}/.dockerignore | 0 .../Caddyfile.noworker => stubs/Caddyfile} | 2 +- packages/aloft/stubs/Dockerfile.debug | 108 +++++++++++ packages/aloft/stubs/Dockerfile.latest | 108 +++++++++++ 13 files changed, 441 insertions(+), 405 deletions(-) delete mode 100644 packages/aloft/docker/Dockerfile delete mode 100644 packages/aloft/docker/docker-bake.hcl delete mode 100644 packages/aloft/docker/stage-files.sh delete mode 100644 packages/aloft/docker/usage-notes.md create mode 100644 packages/aloft/src/AloftBuildCommand.php create mode 100644 packages/aloft/src/AloftPublishCommand.php create mode 100644 packages/aloft/src/AloftServeCommand.php delete mode 100644 packages/aloft/src/Commands/RequireCommand.php rename packages/aloft/{docker => stubs}/.dockerignore (100%) rename packages/aloft/{docker/Caddyfile.noworker => stubs/Caddyfile} (92%) create mode 100644 packages/aloft/stubs/Dockerfile.debug create mode 100644 packages/aloft/stubs/Dockerfile.latest diff --git a/composer.json b/composer.json index 75b6fa585e..be26513821 100644 --- a/composer.json +++ b/composer.json @@ -92,6 +92,7 @@ "wohali/oauth2-discord-new": "^1.2" }, "replace": { + "tempest/aloft": "self.version", "tempest/auth": "self.version", "tempest/cache": "self.version", "tempest/clock": "self.version", @@ -132,6 +133,7 @@ "prefer-stable": true, "autoload": { "psr-4": { + "Tempest\\Aloft\\": "packages/aloft/src", "Tempest\\Auth\\": "packages/auth/src", "Tempest\\Cache\\": "packages/cache/src", "Tempest\\Clock\\": "packages/clock/src", @@ -203,6 +205,7 @@ }, "autoload-dev": { "psr-4": { + "Tempest\\Aloft\\Tests\\": "packages/aloft/tests", "Tempest\\Auth\\Tests\\": "packages/auth/tests", "Tempest\\Cache\\Tests\\": "packages/cache/tests", "Tempest\\Clock\\Tests\\": "packages/clock/tests", @@ -276,4 +279,4 @@ "composer exceptions:build" ] } -} +} \ No newline at end of file diff --git a/packages/aloft/docker/Dockerfile b/packages/aloft/docker/Dockerfile deleted file mode 100644 index 496a80a788..0000000000 --- a/packages/aloft/docker/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# Build-time arguments — set by bake.hcl, can be overridden individually -ARG FRANKENPHP_VERSION=1.11.2 -ARG PHP_VERSION=8.5.3 -ARG BASE_IMAGE=dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} -# Controls which distroless variant is used as the runner base. -# Valid values mirror gcr.io/distroless/cc-debian13 tags: nonroot | debug-nonroot -ARG DISTROLESS_VARIANT=nonroot - -FROM ${BASE_IMAGE} AS frankenphp - -RUN curl -sSLf \ - -o /usr/local/bin/install-php-extensions \ - https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ - chmod +x /usr/local/bin/install-php-extensions - -# Install additional extensions here and they are carried forward into the final image -RUN install-php-extensions \ - gd \ - intl \ - mysqli \ - pcntl \ - pdo_mysql \ - pdo_pgsql \ - pdo_sqlite \ - redis \ - zip - -# Install pax-utils for lddtree -RUN apt-get update && apt-get install -y --no-install-recommends pax-utils && rm -rf /var/lib/apt/lists/* - -# Copy and run the staging script which collects all runtime files into -# /tmp/staging with full paths preserved -COPY stage-files.sh /tmp/stage-files.sh -RUN chmod +x /tmp/stage-files.sh && /tmp/stage-files.sh - -# Re-declare so it is in scope for this stage (ARGs declared before the first -# FROM are not automatically visible inside stages) -ARG DISTROLESS_VARIANT=nonroot - -# Grab distroless image — variant is controlled by DISTROLESS_VARIANT ARG -FROM gcr.io/distroless/cc-debian13:${DISTROLESS_VARIANT} AS common - -# See https://caddyserver.com/docs/conventions#file-locations for details -ENV XDG_CONFIG_HOME=/config -ENV XDG_DATA_HOME=/data - -# Required from frankenphp -ENV GODEBUG=cgocheck=0 - -LABEL org.opencontainers.image.title=TempestPHP -LABEL org.opencontainers.image.description="The framework that gets out of your way" -LABEL org.opencontainers.image.url=https://tempestphp.com -LABEL org.opencontainers.image.source=https://github.com/tempestphp/tempest-framework/ -LABEL org.opencontainers.image.licenses=MIT -LABEL org.opencontainers.image.vendor="Brent Roose and contributors" - -# All libs, binaries and config collected by stage-files.sh into /tmp/staging. -# Everything is normalised to usr/ paths so this single COPY is safe against -# the distroless /lib -> usr/lib symlink. -COPY --from=frankenphp /tmp/staging/ / - -# App directories need specific ownership — distroless has no chown so we -# use the COPY --chown flag. These overwrite the copies already in /tmp/staging. -# From the gcr container, the nonroot user is uid 1002, with gid 1000 -COPY --from=frankenphp --chown=1002:1000 /app /app -COPY --from=frankenphp --chown=1002:1000 /config /config -COPY --from=frankenphp --chown=1002:1000 /data /data -COPY --from=frankenphp --chown=1002:1000 /etc/frankenphp /etc/frankenphp - -COPY Caddyfile.noworker /etc/frankenphp/Caddyfile - -WORKDIR /app - -EXPOSE 8000 -EXPOSE 8443 -EXPOSE 8443/udp -EXPOSE 2019 - -USER nonroot - -CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] -HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1 diff --git a/packages/aloft/docker/docker-bake.hcl b/packages/aloft/docker/docker-bake.hcl deleted file mode 100644 index 68c29dd687..0000000000 --- a/packages/aloft/docker/docker-bake.hcl +++ /dev/null @@ -1,97 +0,0 @@ -# ----------------------------------------------------------------------- -# Variables — override via env vars or --set on the CLI -# e.g. FRANKENPHP_VERSION=1.12.0 docker buildx bake -# ----------------------------------------------------------------------- - -variable "FRANKENPHP_VERSION" { - description = "FrankenPHP release version" - default = "1.11.2" -} - -variable "PHP_VERSION" { - description = "PHP release version" - default = "8.5.3" -} - -variable "PUSH" { - default = "0" -} - -variable "REGISTRY" { - default = "" -} - -# Derived — prepends registry if set, otherwise just the image name -variable "IMAGE" { - default = REGISTRY != "" ? "${REGISTRY}/aloft" : "tempestphp/aloft" -} - -# Derived values — not meant to be overridden directly -variable "BASE_IMAGE" { - default = "dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION}" -} - -variable "VERSION_TAG" { - default = "${FRANKENPHP_VERSION}-${PHP_VERSION}" -} - -# ----------------------------------------------------------------------- -# Shared platform target — all runner targets inherit from this -# ----------------------------------------------------------------------- - -target "_common" { - dockerfile = "Dockerfile" - context = "." - platforms = PUSH == "1" ? ["linux/amd64", "linux/arm64"] : [] - output = PUSH == "1" ? ["type=registry"] : ["type=docker"] - args = { - FRANKENPHP_VERSION = FRANKENPHP_VERSION - PHP_VERSION = PHP_VERSION - BASE_IMAGE = BASE_IMAGE - } -} - -# ----------------------------------------------------------------------- -# latest-nonroot — runner is gcr.io/distroless/cc-debian13:nonroot -# Tags: tempestphp/aloft:latest-nonroot -# tempestphp/aloft:1.11.2-8.5.3-nonroot -# ----------------------------------------------------------------------- - -target "latest-nonroot" { - inherits = ["_common"] - target = "common" - args = { - DISTROLESS_VARIANT = "nonroot" - } - tags = [ - "${IMAGE}:latest-nonroot", - "${IMAGE}:${VERSION_TAG}-nonroot", - ] -} - -# ----------------------------------------------------------------------- -# debug-nonroot — runner is gcr.io/distroless/cc-debian13:debug-nonroot -# Includes busybox shell for exec access while still running as nonroot. -# Tags: tempestphp/aloft:debug-nonroot -# tempestphp/aloft:1.11.2-8.5.3-debug-nonroot -# ----------------------------------------------------------------------- - -target "debug-nonroot" { - inherits = ["_common"] - target = "common" - args = { - DISTROLESS_VARIANT = "debug-nonroot" - } - tags = [ - "${IMAGE}:debug-nonroot", - "${IMAGE}:${VERSION_TAG}-debug-nonroot", - ] -} - -# ----------------------------------------------------------------------- -# Default group — builds both variants in parallel -# ----------------------------------------------------------------------- - -group "default" { - targets = ["latest-nonroot", "debug-nonroot"] -} diff --git a/packages/aloft/docker/stage-files.sh b/packages/aloft/docker/stage-files.sh deleted file mode 100644 index 8d602b400e..0000000000 --- a/packages/aloft/docker/stage-files.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env bash -# stage-files.sh -# -# Runs inside the frankenphp intermediate stage during docker build. -# Collects every runtime file needed in the final distroless image into -# /tmp/staging, preserving full filesystem paths so the Dockerfile can -# use a single COPY --from=frankenphp /tmp/staging/ / -# -# Steps: -# 1. lddtree --copy-to-tree for extensions, php, frankenphp -# 2. Resolve soname symlinks → copy versioned targets beside them -# 3. Normalise lib/ and lib64/ → usr/lib/ and usr/lib64/ (distroless symlink collision) -# 4. Explicit copies for dlopen'd plugins lddtree cannot discover -# 5. PHP config, ld config, mime types, frankenphp runtime files - -set -euo pipefail - -STAGING=/tmp/staging -ARCH=$(uname -m | sed 's/x86_64/x86_64-linux-gnu/;s/aarch64/aarch64-linux-gnu/') -DEB_DIR=/tmp/debs -DEB_EXTRACT=/tmp/deb-extract - -mkdir -p "${STAGING}" "${DEB_DIR}" - -# --------------------------------------------------------------------------- -# 1. lddtree — ELF dependency tree for all extensions + binaries -# -# This gives us the soname symlinks and the correct set of libraries needed. -# The versioned files behind those symlinks are fetched via apt in step 2. -# --------------------------------------------------------------------------- - -find /usr/local/lib/php/extensions/ -name '*.so' -print0 \ - | xargs -0 -r lddtree --copy-to-tree "${STAGING}" 2>/dev/null || true - -lddtree --copy-to-tree "${STAGING}" /usr/local/bin/php 2>/dev/null || true -lddtree --copy-to-tree "${STAGING}" /usr/local/bin/frankenphp 2>/dev/null || true -lddtree --copy-to-tree "${STAGING}" /usr/local/lib/libphp.so 2>/dev/null || true - -# Normalise lib/ and lib64/ → usr/lib/ and usr/lib64/ before package resolution. -# lddtree follows the /lib -> usr/lib and /lib64 -> usr/lib64 symlinks on the -# source system and may write files under staging/lib/ or staging/lib64/. -# distroless has both as symlinks so COPY / would collide — merge into usr/. -if [ -d "${STAGING}/lib" ]; then - mkdir -p "${STAGING}/usr/lib" - cp -a "${STAGING}/lib/." "${STAGING}/usr/lib/" - rm -rf "${STAGING}/lib" -fi - -if [ -d "${STAGING}/lib64" ]; then - mkdir -p "${STAGING}/usr/lib64" - cp -a "${STAGING}/lib64/." "${STAGING}/usr/lib64/" - rm -rf "${STAGING}/lib64" -fi - -# --------------------------------------------------------------------------- -# 2. apt-get download + dpkg-deb extract -# -# lddtree creates relative soname symlinks inside staging, so the versioned -# file each symlink points to only exists on the source system, not in -# staging. Rather than trying to resolve paths manually, we find which -# Debian package owns each collected .so file, download that package, and -# extract it — giving us both the soname symlink and the versioned file -# with no path gymnastics required. -# --------------------------------------------------------------------------- - -# Collect owning packages for all .so files lddtree placed in staging -find "${STAGING}/usr/lib" -name "*.so*" 2>/dev/null \ - | sed "s|${STAGING}||" \ - | xargs -r dpkg -S 2>/dev/null \ - | cut -d: -f1 \ - | sort -u > /tmp/pkgs-needed.txt - -# Also add explicit packages for dlopen'd libs lddtree can't discover -# (kerberos plugins, sasl plugins, libjansson for FrankenPHP admin API) -cat >> /tmp/pkgs-needed.txt << 'EOF' -libjansson4 -libkrb5-3 -libsasl2-2 -EOF - -sort -u /tmp/pkgs-needed.txt > /tmp/pkgs-deduped.txt - -# Download debs (failures are non-fatal — some names may vary by suite) -cd "${DEB_DIR}" -while read -r pkg; do - apt-get download "${pkg}" 2>/dev/null || true -done < /tmp/pkgs-deduped.txt - -# Extract only usr/lib from each deb into staging -for deb in "${DEB_DIR}"/*.deb; do - [ -f "${deb}" ] || continue - rm -rf "${DEB_EXTRACT}" - mkdir -p "${DEB_EXTRACT}" - dpkg-deb -x "${deb}" "${DEB_EXTRACT}" - if [ -d "${DEB_EXTRACT}/usr/lib" ]; then - cp -a "${DEB_EXTRACT}/usr/lib/." "${STAGING}/usr/lib/" - fi -done -rm -rf "${DEB_EXTRACT}" "${DEB_DIR}" - -# --------------------------------------------------------------------------- -# 3. dlopen'd plugin directories -# -# These are subdirectories loaded at runtime via dlopen() — dpkg-deb extract -# above handles the files, but ensure the directories land correctly. -# --------------------------------------------------------------------------- - -# Kerberos pre-authentication plugins -if [ -d "/usr/lib/${ARCH}/krb5" ]; then - cp -a "/usr/lib/${ARCH}/krb5" "${STAGING}/usr/lib/${ARCH}/" -fi - -# SASL mechanism plugins -if [ -d "/usr/lib/${ARCH}/sasl2" ]; then - cp -a "/usr/lib/${ARCH}/sasl2" "${STAGING}/usr/lib/${ARCH}/" - mkdir -p "${STAGING}/usr/lib/sasl2" - cp -a "/usr/lib/${ARCH}/sasl2/." "${STAGING}/usr/lib/sasl2/" -fi - -# libjansson — dlopen'd by FrankenPHP admin API, not in any DT_NEEDED chain. -# Copy directly from the source filesystem; apt-get download is unreliable -# here since libjansson4 may not be registered in the image's dpkg database. -find "/usr/lib/${ARCH}" -maxdepth 1 -name 'libjansson.so*' | while read -r f; do - dest="${STAGING}/usr/lib/${ARCH}/$(basename "${f}")" - [ -e "${dest}" ] && continue - cp -a "${f}" "${dest}" -done - -# --------------------------------------------------------------------------- -# 5. PHP runtime files -# --------------------------------------------------------------------------- - -# Main PHP shared library (already collected via lddtree above, belt+suspenders) -mkdir -p "${STAGING}/usr/local/lib" -[ -f "${STAGING}/usr/local/lib/libphp.so" ] || \ - cp /usr/local/lib/libphp.so "${STAGING}/usr/local/lib/" - -# libwatcher (FrankenPHP file watcher) -cp -a /usr/local/lib/libwatcher* "${STAGING}/usr/local/lib/" - -# PHP extension .so files (already under staging from lddtree but confirm) -mkdir -p "${STAGING}/usr/local/lib/php/extensions" -cp -a /usr/local/lib/php/extensions/. "${STAGING}/usr/local/lib/php/extensions/" - -# PHP configuration -mkdir -p "${STAGING}/usr/local/etc/php/conf.d" -cp -a /usr/local/etc/php/. "${STAGING}/usr/local/etc/php/" - -# --------------------------------------------------------------------------- -# 6. Dynamic linker config -# --------------------------------------------------------------------------- - -mkdir -p "${STAGING}/etc/ld.so.conf.d" -[ -f /etc/ld.so.conf ] && cp /etc/ld.so.conf "${STAGING}/etc/" -[ -f /etc/ld.so.cache ] && cp /etc/ld.so.cache "${STAGING}/etc/" -cp -a /etc/ld.so.conf.d/. "${STAGING}/etc/ld.so.conf.d/" - -# --------------------------------------------------------------------------- -# 7. Misc runtime config -# --------------------------------------------------------------------------- - -# MIME types (FrankenPHP/Caddy uses this for content-type detection) -[ -f /etc/mime.types ] && cp /etc/mime.types "${STAGING}/etc/" - - -FILE_COUNT=$(find "${STAGING}" -type f | wc -l) -LINK_COUNT=$(find "${STAGING}" -type l | wc -l) -echo "✓ Staging complete — ${FILE_COUNT} files, ${LINK_COUNT} symlinks collected" - -# Fail fast if any symlinks are dangling — a versioned lib target missing -# from staging means the final image would have broken .so links at runtime -DANGLING=$(find "${STAGING}" -type l ! -exec test -e {} \; -print) -if [ -n "${DANGLING}" ]; then - echo "✗ Dangling symlinks found in staging:" >&2 - echo "${DANGLING}" >&2 - exit 1 -fi diff --git a/packages/aloft/docker/usage-notes.md b/packages/aloft/docker/usage-notes.md deleted file mode 100644 index f16eb13d93..0000000000 --- a/packages/aloft/docker/usage-notes.md +++ /dev/null @@ -1,47 +0,0 @@ -# Usage notes - -NB: This file is NOT FINAL and will be removed from the PR this is just so that the image can be tested in development - -## Build locally - -cd into the folder -```bash -docker buildx bake -``` -After build you should have something similar, disk usage will vary slightly based on your arch (this is aarch64 on macos m1 17.x). -```bash -docker % docker image ls - -IMAGE ID DISK USAGE CONTENT SIZE EXTRA -gcr.io/distroless/cc-debian13:debug-nonroot f60c5a64690d 38.5MB 0B -gcr.io/distroless/cc-debian13:nonroot 5c5da034ed6e 37.2MB 0B -tempestphp/aloft:1.11.2-8.5.3-debug-nonroot 1bf5840bddb2 219MB 0B -tempestphp/aloft:1.11.2-8.5.3-nonroot a5039ddf9345 218MB 0B -tempestphp/aloft:debug-nonroot 1bf5840bddb2 219MB 0B -tempestphp/aloft:latest-nonroot a5039ddf9345 218MB 0B -``` - -## Push to a registry - -cd into the folder -```bash -PUSH=1 REGISTRY=registry.url/tempestphp docker buildx bake -``` - -View on your registry, but should create the four tags and two images. - -e.g. I pushed to my private gitea - -tempestphp/aloft/versions: - -1.11.2-8.5.3-debug-nonroot -Published 16 hours ago by iamdadmin - -latest-nonroot -Published 16 hours ago by iamdadmin - -1.11.2-8.5.3-nonroot -Published 16 hours ago by iamdadmin - -debug-nonroot -Published 16 hours ago by iamdadmin diff --git a/packages/aloft/src/AloftBuildCommand.php b/packages/aloft/src/AloftBuildCommand.php new file mode 100644 index 0000000000..3e50bb8325 --- /dev/null +++ b/packages/aloft/src/AloftBuildCommand.php @@ -0,0 +1,77 @@ +assumedVariant = match (true) { + exists("{$testPath}latest") => 'latest', + exists("{$testPath}debug") => 'debug', + default => null, + }; + + $this->stubsPublished = $this->assumedVariant !== null; + } + + #[ConsoleCommand( + name: 'aloft:build', + description: 'Build the Aloft Docker image locally, and publish the stub files if not already present.', + )] + public function build( + #[ConsoleArgument( + description: 'The build variant to use.', + )] + string $requestedVariant = '', + #[ConsoleArgument( + name: 'with-php-extensions', + description: 'Space-separated list of extra extensions to include in the build.', + )] + ?string $withPhpExtensions = null, + #[ConsoleArgument( + name: 'with-frankenphp', + description: 'FrankenPHP version to pass as a build ARG.', + )] + ?string $withFrankenphp = null, + #[ConsoleArgument( + name: 'with-php', + description: 'PHP version to pass as a build ARG.', + )] + ?string $withPhp = null, + ): void { + $variant = $requestedVariant === '' ? $this->assumedVariant ?? 'debug' : $requestedVariant; + + $buildArgs = implode('', array_filter([ + $withFrankenphp !== null ? " --build-arg FRANKENPHP_VERSION=\"{$withFrankenphp}\"" : null, + $withPhp !== null ? " --build-arg PHP_VERSION=\"{$withPhp}\"" : null, + $withPhpExtensions !== null ? " --build-arg PHP_EXTRA_EXTENSIONS=\"{$withPhpExtensions}\"" : null, + ])); + + $buildPath = ($this->stubsPublished ? root_path('docker') : dirname(__DIR__) . DIRECTORY_SEPARATOR . 'stubs') . DIRECTORY_SEPARATOR; + $buildFile = "{$buildPath}Dockerfile.{$variant} -t tempestphp/aloft:{$variant}"; + + if ($this->confirm("Do you want to build tempestphp/aloft:{$variant}?", default: false)) { + $this->console->info('Okay, attempting build'); + passthru("docker build -f {$buildFile}{$buildArgs} {$buildPath}"); + } + } +} diff --git a/packages/aloft/src/AloftPublishCommand.php b/packages/aloft/src/AloftPublishCommand.php new file mode 100644 index 0000000000..4d3b7d5a08 --- /dev/null +++ b/packages/aloft/src/AloftPublishCommand.php @@ -0,0 +1,64 @@ + '.dockerignore', + 'Caddyfile' => 'Caddyfile', + "Dockerfile.{$variant}" => "Dockerfile.{$variant}", + ]) + ->each( + function (string $dstFile, string $srcFile) use ($srcPath, $dstPath) { + copy_file( + source: $srcPath . $srcFile, + destination: $dstPath . $dstFile, + ); + }, + ); + + // copy_file will throw a runtime exception if this fails, so write a success + $this->console->success("Stub files copied to {$dstPath}"); + } + + #[ConsoleCommand( + name: 'aloft:publish:latest', + description: 'Publish the Aloft Docker stubs, for the distroless image.', + aliases: ['aloft:publish:distroless', 'aloft:publish:prod'], + )] + public function publishLatest(): void + { + $this->publish('latest'); + } +} diff --git a/packages/aloft/src/AloftServeCommand.php b/packages/aloft/src/AloftServeCommand.php new file mode 100644 index 0000000000..a6b45ce585 --- /dev/null +++ b/packages/aloft/src/AloftServeCommand.php @@ -0,0 +1,79 @@ +assumedVariant = match (true) { + exists("{$testPath}latest") => 'latest', + exists("{$testPath}debug") => 'debug', + default => null, + }; + + $this->stubsPublished = $this->assumedVariant !== null; + + $this->remotePath = 'PLACE.HOLD.ER/'; + } + + #[ConsoleCommand( + name: 'aloft:build', + description: 'Build the Aloft Docker image locally, and publish the stub files if not already present.', + )] + public function build( + #[ConsoleArgument( + description: 'The build variant to use.', + )] + string $requestedVariant = '', + #[ConsoleArgument( + name: 'repository', + description: 'Space-separated list of extra extensions to include in the build.', + )] + ?string $repository = null, + ): void { + $variant = $requestedVariant === '' ? $this->assumedVariant ?? 'debug' : $requestedVariant; + $repo = $repository ?? ($this->stubsPublished ? '' : $this->remotePath); + + // TODO: Catch local development paths from composer.json and insert them as volumes + + $runImage = "{$repo}tempestphp/aloft:{$variant}"; + + if ($this->confirm("Do you want to start dev server from {$runImage}?", default: true)) { + $this->console->info('Okay, starting, use ctrl-c to exit when finished'); + passthru( + "docker run --rm -it -p 80:8000 -p 443:8443 -p 443:8443/udp \ + -v " + . root_path() + . ":/app \ + -v " + . root_path('.frankenpest/data') + . ":/data \ + -v " + . root_path('.frankenpest/config') + . ":/config \ + {$runImage}", + ); + } + } +} diff --git a/packages/aloft/src/Commands/RequireCommand.php b/packages/aloft/src/Commands/RequireCommand.php deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/aloft/docker/.dockerignore b/packages/aloft/stubs/.dockerignore similarity index 100% rename from packages/aloft/docker/.dockerignore rename to packages/aloft/stubs/.dockerignore diff --git a/packages/aloft/docker/Caddyfile.noworker b/packages/aloft/stubs/Caddyfile similarity index 92% rename from packages/aloft/docker/Caddyfile.noworker rename to packages/aloft/stubs/Caddyfile index cced0a3ca9..71f846e85e 100644 --- a/packages/aloft/docker/Caddyfile.noworker +++ b/packages/aloft/stubs/Caddyfile @@ -20,7 +20,7 @@ {$CADDY_EXTRA_CONFIG} -{$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTP_PORT:8000}, {$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTPS_PORT:8443} { +{$CADDY_SERVER_NAME:localhost} { #log { # # Redact the authorization query parameter that can be set by Mercure # format filter { diff --git a/packages/aloft/stubs/Dockerfile.debug b/packages/aloft/stubs/Dockerfile.debug new file mode 100644 index 0000000000..abd546849e --- /dev/null +++ b/packages/aloft/stubs/Dockerfile.debug @@ -0,0 +1,108 @@ +ARG FRANKENPHP_VERSION=1.11.3 +ARG PHP_VERSION=8.5.3 +ARG PHP_EXTRA_EXTENSIONS=" " + +FROM debian:trixie-slim AS builder +ARG FRANKENPHP_VERSION +ARG PHP_VERSION +ARG PHP_EXTRA_EXTENSIONS + +RUN set -eux; \ + CHROOT=/chroot; \ + \ + { \ + echo 'Package: php*'; \ + echo "Pin: version ${PHP_VERSION}*"; \ + echo 'Pin-Priority: 1001'; \ + echo ''; \ + echo 'Package: frankenphp'; \ + echo "Pin: version ${FRANKENPHP_VERSION}+php85*"; \ + echo 'Pin-Priority: 1001'; \ + echo ''; \ + echo 'Package: php*'; \ + echo 'Pin: release o=Debian'; \ + echo 'Pin-Priority: -1'; \ + } > /etc/apt/preferences.d/static-php85; \ + apt-get update; \ + apt-get install -y --no-install-recommends busybox ca-certificates curl mailcap xz-utils; \ + curl https://pkg.henderkes.com/api/packages/85/debian/repository.key -o /etc/apt/keyrings/static-php85.asc; \ + echo "deb [signed-by=/etc/apt/keyrings/static-php85.asc] https://pkg.henderkes.com/api/packages/85/debian php-zts main" > /etc/apt/sources.list.d/static-php85.list; \ + apt-get update; \ + apt-get install -y --download-only --no-install-recommends \ + ca-certificates \ + frankenphp \ + php-zts-gd \ + php-zts-intl \ + php-zts-mysqli \ + php-zts-pdo-mysql \ + php-zts-pdo-pgsql \ + php-zts-pdo-sqlite \ + php-zts-redis \ + php-zts-zip ${PHP_EXTRA_EXTENSIONS}; \ + mkdir -p $CHROOT; \ + for deb in /var/cache/apt/archives/*.deb; do \ + dpkg-deb --extract "$deb" $CHROOT; \ + done; \ + cp /etc/mime.types $CHROOT/etc/mime.types; \ + \ + # Download static curl into the chroot + case "$(dpkg --print-architecture)" in \ + amd64) CURL_ARCH="x86_64" ;; \ + arm64) CURL_ARCH="aarch64" ;; \ + armhf) CURL_ARCH="armv7" ;; \ + armel) CURL_ARCH="armv5" ;; \ + i386) CURL_ARCH="i686" ;; \ + *) echo "Unsupported arch: $(dpkg --print-architecture)" && exit 1 ;; \ + esac; \ + CURL_VERSION=$(curl -fsSL "https://api.github.com/repos/stunnel/static-curl/releases/latest" \ + | grep '"tag_name"' | head -1 \ + | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/'); \ + curl -fsSL -o /tmp/curl.tar.xz \ + "https://github.com/stunnel/static-curl/releases/download/${CURL_VERSION}/curl-linux-${CURL_ARCH}-glibc-${CURL_VERSION}.tar.xz"; \ + tar -xf /tmp/curl.tar.xz -C $CHROOT/usr/bin curl; \ + chmod +x $CHROOT/usr/bin/curl; \ + tar -C $CHROOT -cf /tmp/chroot.tar .; + +FROM gcr.io/distroless/cc-debian13:nonroot AS runner + +USER root + +COPY --from=builder /bin/busybox /usr/bin/busybox + +RUN --mount=type=bind,from=builder,source=/tmp/chroot.tar,target=/tmp/chroot.tar \ + ["/usr/bin/busybox", "sh", "-c", "\ + /usr/bin/busybox --install -s /usr/bin; \ + tar -xf /tmp/chroot.tar -C /; \ + mkdir -p \ + /app/public \ + /data/caddy \ + /config/caddy \ + /etc/frankenphp; \ + chown -R nonroot /app/public /data /config /etc/frankenphp;"] + +COPY --chown=nonroot Caddyfile /etc/frankenphp/Caddyfile + +# See https://caddyserver.com/docs/conventions#file-locations for details +ENV XDG_CONFIG_HOME=/config +ENV XDG_DATA_HOME=/data + +# Required from frankenphp +ENV GODEBUG=cgocheck=0 + +LABEL org.opencontainers.image.title=TempestPHP +LABEL org.opencontainers.image.description="The framework that gets out of your way" +LABEL org.opencontainers.image.url=https://tempestphp.com +LABEL org.opencontainers.image.source=https://github.com/tempestphp/tempest-framework/ +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.vendor="Brent Roose and contributors" + +WORKDIR /app + +EXPOSE 8000 +EXPOSE 8443 +EXPOSE 8443/udp + +USER nonroot + +CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] +HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1 diff --git a/packages/aloft/stubs/Dockerfile.latest b/packages/aloft/stubs/Dockerfile.latest new file mode 100644 index 0000000000..221b07b767 --- /dev/null +++ b/packages/aloft/stubs/Dockerfile.latest @@ -0,0 +1,108 @@ +ARG FRANKENPHP_VERSION=1.11.3 +ARG PHP_VERSION=8.5.3 +ARG PHP_EXTRA_EXTENSIONS=" " + +FROM debian:trixie-slim AS builder +ARG FRANKENPHP_VERSION +ARG PHP_VERSION +ARG PHP_EXTRA_EXTENSIONS + +RUN set -eux; \ + CHROOT=/chroot; \ + \ + { \ + echo 'Package: php*'; \ + echo "Pin: version ${PHP_VERSION}*"; \ + echo 'Pin-Priority: 1001'; \ + echo ''; \ + echo 'Package: frankenphp'; \ + echo "Pin: version ${FRANKENPHP_VERSION}+php85*"; \ + echo 'Pin-Priority: 1001'; \ + echo ''; \ + echo 'Package: php*'; \ + echo 'Pin: release o=Debian'; \ + echo 'Pin-Priority: -1'; \ + } > /etc/apt/preferences.d/static-php85; \ + apt-get update; \ + apt-get install -y --no-install-recommends busybox ca-certificates curl mailcap xz-utils; \ + curl https://pkg.henderkes.com/api/packages/85/debian/repository.key -o /etc/apt/keyrings/static-php85.asc; \ + echo "deb [signed-by=/etc/apt/keyrings/static-php85.asc] https://pkg.henderkes.com/api/packages/85/debian php-zts main" > /etc/apt/sources.list.d/static-php85.list; \ + apt-get update; \ + apt-get install -y --download-only --no-install-recommends \ + ca-certificates \ + frankenphp \ + php-zts-gd \ + php-zts-intl \ + php-zts-mysqli \ + php-zts-pdo-mysql \ + php-zts-pdo-pgsql \ + php-zts-pdo-sqlite \ + php-zts-redis \ + php-zts-zip ${PHP_EXTRA_EXTENSIONS}; \ + mkdir -p $CHROOT; \ + for deb in /var/cache/apt/archives/*.deb; do \ + dpkg-deb --extract "$deb" $CHROOT; \ + done; \ + cp /etc/mime.types $CHROOT/etc/mime.types; \ + \ + # Download static curl into the chroot + case "$(dpkg --print-architecture)" in \ + amd64) CURL_ARCH="x86_64" ;; \ + arm64) CURL_ARCH="aarch64" ;; \ + armhf) CURL_ARCH="armv7" ;; \ + armel) CURL_ARCH="armv5" ;; \ + i386) CURL_ARCH="i686" ;; \ + *) echo "Unsupported arch: $(dpkg --print-architecture)" && exit 1 ;; \ + esac; \ + CURL_VERSION=$(curl -fsSL "https://api.github.com/repos/stunnel/static-curl/releases/latest" \ + | grep '"tag_name"' | head -1 \ + | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/'); \ + curl -fsSL -o /tmp/curl.tar.xz \ + "https://github.com/stunnel/static-curl/releases/download/${CURL_VERSION}/curl-linux-${CURL_ARCH}-glibc-${CURL_VERSION}.tar.xz"; \ + tar -xf /tmp/curl.tar.xz -C $CHROOT/usr/bin curl; \ + chmod +x $CHROOT/usr/bin/curl; \ + tar -C $CHROOT -cf /tmp/chroot.tar .; + +FROM gcr.io/distroless/cc-debian13:nonroot AS runner + +USER root + +COPY --from=builder /bin/busybox /usr/bin/busybox + +RUN --mount=type=bind,from=builder,source=/tmp/chroot.tar,target=/tmp/chroot.tar \ + --mount=type=bind,from=builder,source=/bin/busybox,target=/bin/busybox \ + ["/bin/busybox", "sh", "-c", "\ + /bin/busybox tar -xf /tmp/chroot.tar -C /; \ + /bin/busybox mkdir -p \ + /app/public \ + /data/caddy \ + /config/caddy \ + /etc/frankenphp; \ + /bin/busybox chown -R nonroot /app/public /data /config /etc/frankenphp;"] + +COPY --chown=nonroot Caddyfile /etc/frankenphp/Caddyfile + +# See https://caddyserver.com/docs/conventions#file-locations for details +ENV XDG_CONFIG_HOME=/config +ENV XDG_DATA_HOME=/data + +# Required from frankenphp +ENV GODEBUG=cgocheck=0 + +LABEL org.opencontainers.image.title=TempestPHP +LABEL org.opencontainers.image.description="The framework that gets out of your way" +LABEL org.opencontainers.image.url=https://tempestphp.com +LABEL org.opencontainers.image.source=https://github.com/tempestphp/tempest-framework/ +LABEL org.opencontainers.image.licenses=MIT +LABEL org.opencontainers.image.vendor="Brent Roose and contributors" + +WORKDIR /app + +EXPOSE 8000 +EXPOSE 8443 +EXPOSE 8443/udp + +USER nonroot + +CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"] +HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1 From f4b4eedea780161eec64d596668afa1a27bf78e6 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 4 Mar 2026 20:34:35 +0000 Subject: [PATCH 6/9] docs(aloft): created docs for docker deployment --- docs/0-getting-started/03-docker.md | 235 ++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/0-getting-started/03-docker.md diff --git a/docs/0-getting-started/03-docker.md b/docs/0-getting-started/03-docker.md new file mode 100644 index 0000000000..2fd1304f12 --- /dev/null +++ b/docs/0-getting-started/03-docker.md @@ -0,0 +1,235 @@ +--- +title: Docker +description: Tempest can both be developed or deployed in Production, with our own Docker images. Or, copy the Dockerfiles and customise as you need with our utility commands. +--- + +## Overview + +We are pleased to offer TempestPHP/Aloft, our own set of Docker images for developing with and serving your Tempest applications, for you to use as and customise as you see fit. + +In order to start from the strongest security posture and enable you to run secure and performant Tempest-based applications, we've initially selected FrankenPHP as our server of choice. Further, we've adopted a 'rootless' approach by default, and also offer a 'distroless' production image to further mitigate potential security issues stemming from unnecessary software often found in Docker images. + +## Aloft image architecture, variants and release strategy + +Our CI/CD will automatically generate and publish images to our public repository at https://PLACE.HOLD.ER/tempestphp/aloft following the releases of PHP and FrankenPHP, and also any time we find an issue in the underlying Docker image. Alternatively, you can also customise these images for your own use, see section below. (TODO: link) + +### Architectures + +We publish `amd64` AKA `x86_64`, and `aarch64` AKA `arm64` releases, which should work on most Linux and MacOS host systems, as part of a multi-arch image. The appropriate version should be selected automatically for your host system by docker when providing the image. + +Our upstream providers have some support for other architectures, should you need to support other platforms; see Customising the Docker image for your use, below. + +### Variants and release strategy + +We maintain two variants; 'latest' which is rootless and distroless, and is aimed at your test, qa and production needs, and 'debug' which is the same base image, with busybox available in case you need to access the docker shell. + +```bash +# These periodically updated variant tags will always point at the latest version-pinned images +tempestphp/aloft >> tempestphp/aloft:1.11.3-8.5.3 #at time of writing +tempestphp/aloft:latest >> tempestphp/aloft:1.11.3-8.5.3 #at time of writing +tempestphp/aloft:debug >> tempestphp/aloft:1.11.3-8.5.3-debug #at time of writing + +# We'll also continually publish pinned-versions +tempestphp/aloft:1.11.3-8.5.3 +tempestphp/aloft:1.11.3-8.5.3-debug +# these will accumulate over time +``` +We utilise the [GoogleContainerTools Distroless](https://github.com/GoogleContainerTools/distroless/) [`cc`](https://github.com/GoogleContainerTools/distroless/blob/main/cc/README.md) image, pulling their latest 'nonroot' image as our base, at time of build. +```dockerfile +FROM gcr.io/distroless/cc-debian13:nonroot AS runner +``` + +### Response to security incidents in the software chain + +We monitor our three upstream providers, GoogleContainerTools, PHP, and FrankenPHP, for security defect announcements. + +- We will actively replace pinned-version images utilising an instance of the distroless base image found to have any security defects. +- We will actively retire pinned-version images utilising an instance of PHP or FrankenPHP releases found to have any security defects. + +:::info +We won't automatically retire 'patch' version releases i.e. PHP8.5.3 > PHP8.5.4, unless subject to security defects specifically, in which case we'll re-build the image and re-publish. We encourage you to monitor the upstreams and update regularly, or use the `:debug` or `:latest` releases where possible. +::: + +## Developing your application with Docker + +During development, we'd suggest using the debug image. We've included a convenience command in the `tempest/aloft` package which will run a development server on your device. +```bash +./tempest aloft:serve # by default, this will get debug from the repository and serve it +``` +You may specify the `latest` image if you prefer. +```bash +./tempest aloft:serve latest # latest floating version +``` +You may instead specify the release, if you require a pinned-version. +```bash +./tempest aloft:serve 1.11.3-8.5.3 # pinned-version, distroless +./tempest aloft:serve 1.11.3-8.5.3-debug # pinned-version, debug +``` + +:::info +By default, the `aloft:serve` command will try to pull from the registry. But if you have published the stub for customising the image, this command will attempt to use the local image. You can force this behaviour by adding the optional command `--repository=local` or `--repository=remote`. +::: + +## Testing and production applications with Docker + +For testing and QA, we'd suggest using the distroless image, as it is most representative of your final infrastructure, and should highlight any issues for your attention. +```bash +./tempest aloft:serve latest +``` +As per the section above, you can omit `latest` to default to the `debug` release, or specify a version. + +## Customising the Docker image for your use + +As the `latest` and `debug` images are inherently distroless, albeit with busybox in the `debug` image, you cannot use this as an intermediate stage in a multi-stage Dockerfile build. Instead, you can use the `aloft:publish` Tempest command to publish a copy of the stubs, so you can build and tweak as you need. +```bash +./tempest aloft:publish # by default, this will publish the debug dockerfile +./tempest aloft:publish:latest # select the distroless image, instead +``` +This will publish `.dockerignore`, `Caddyfile`, and `Dockerfile` into your project root `docker/` folder, creating it as necessary. If you already have files in here, it shouldn't overwrite by default. + +You can also retrieve the files manually, from the vendor folder. +```bash +vendor/tempest/framework/packages/aloft/stubs/ +``` +### Building the image + +We've provided a simple `aloft:build` command to build these local images. It won't handle all use cases, and is really only aimed at someone directly running the images. If you are ready to change the Dockerfile to suit your needs, you probably won't want to use this anyway. That said, here's how to use it. + +If you HAVE NOT published the stubs to your project: +```bash +./tempest aloft:build # will attempt to build debug directly from the package stubs folder +./tempest aloft:build debug # will attempt to build debug directly from the package stubs folder +./tempest aloft:build latest # will attempt to build distroless directly from the package stubs folder +``` +If you HAVE published the stubs to your project: +```bash +./tempest aloft:build # will attempt to build debug from `{root_path}/docker/` +./tempest aloft:build debug # will attempt to build debug from `{root_path}/docker/` +./tempest aloft:build latest # will attempt to build distroless from `{root_path}/docker/` +``` +:::info +If you've published both stubs, or renamed the Dockerfile, this won't work. You've moved past the use-case this command was designed for, and will need to build yourself. Or copy the AloftBuildCommand into your project and customise it to suit you! +::: + +### Default versions of FrankenPHP and PHP + +We will update the stubs from time-to-time, but you may find that your PHP and/or FrankenPHP versions are out of step, because you have customised your file and don't wish to republish the stubs losing the changes. + +You can use the `aloft:build` command to pass the arguments: +```bash +./tempest aloft:build {''|debug|latest} --with-frankenphp="1.11.3" --with-php="8.5.3" +``` +:::info +Note that this will tag the image with tempestphp/aloft:debug or :latest, and remains compatible with `aloft:serve`. +::: + +Or, you pass these via build arguments run from the `{root_path}/docker/` folder: +```bash +docker build . -t tempestphp/aloft:1.11.3-8.5.3 --build-arg FRANKENPHP_VERSION="1.11.3" --build-arg PHP_VERSION="8.5.3" +``` +:::info +To retain compatibility with `aloft:serve` ensure that the image retains `tempestphp/aloft:` and then pass `1.11.3-8.5.3` as the image variant i.e. `./tempest aloft:serve 1.11.3-8.5.3`. +::: + +Or you can edit the Dockerfile directly: +```bash +ARG FRANKENPHP_VERSION=1.11.3 +ARG PHP_VERSION=8.5.3 +``` +:::info +This method also retains compatibility with `aloft:serve` and `aloft:build`, as long as you keep the filename unchanged. +::: + +## Adding additional PHP Extensions + +We include PHP Extensions from [Marc Henderkes'](https://pkgs.henderkes.com/) Static PHP Repository. These are static builds, of PHP-ZTS, which is required by FrankenPHP. + +:::info +Note that apt-get packages are kebab-case and should be prefixed `php-zts`. So if you wanted the extension `pdo_mysql`, you'd specify `php-zts-pdo-mysql`. +::: + +### Adding extensions at build time via build arguments + +This method is useful if you need to make a specific build one-off, containing an additional extension. + +Pass the build argument directly if using a published stub Dockerfile: +```bash +docker build . -t aloft:with-yaml --build-arg PHP_EXTRA_EXTENSIONS="php-zts-yaml" +``` +Or, you can use the Tempest aloft:build command and pass the optional argument: +```bash +./tempest aloft:build --with-php-extensions="php-zts-yaml" +``` +:::info +This will work with both the `debug` and `latest` images. +::: + +### Adding extensions to the Dockerfile + +This method is useful if you want to make your own image which always includes + +```dockerfile +RUN + # cropped for brevity + apt-get install -y --download-only --no-install-recommends \ + ca-certificates \ + frankenphp \ + php-zts-gd \ + php-zts-intl \ + php-zts-mysqli \ + php-zts-pdo-mysql \ + php-zts-pdo-pgsql \ + php-zts-pdo-sqlite \ + php-zts-redis \ + # Insert additional extensions here, space separated, or one per line followed by 'space, slash' i.e. ' \' + php-zts-zip ${PHP_EXTRA_EXTENSIONS}; \ +``` + +## Composer + +We don't package composer in the image currently, mostly due to the lack of shell in the distroless image. While it could potentially be included in the debug image, the primary use for the debug image is likely to be a developer's local machine, with a volume mount for the app. The host itself will almost certainly have composer installed as part of the developer's IDE and toolset, meaning it's presence in debug is largely redundant and unlikely to be used commonly. It would also have unequal updates since we are not version-pinning composer, resulting in stale versions present in the docker volumes. + +We suggest one of the following options instead. + +### Run composer from docker composer:latest + +You can use the following command to run composer interactively: +```bash +docker run --rm -i --tty --volume $PWD:/app --user 1002:1002 composer:latest # We suggest running with 1002:1002 to match the file permissions within our rootless image +``` +You could also create an alias script: +```bash +sudo sh -c 'echo "#!/usr/bin/env sh\ndocker run --rm -it --volume \"\$PWD:/app\" --user 1002:1002 composer:latest composer \"\$@\"" > /usr/local/bin/composer' && sudo chmod +x /usr/local/bin/composer +``` +This would allow you to execute `composer install` from the command line, via docker, without it being installed on your host. + +:::info +You can find more detailed instructions for running composer via docker [here](https://github.com/docker-library/docs/tree/master/composer). +::: + +## Frequently asked questions + +### Why no Alpine image? + +FrankenPHP strongly recommends not using Alpine for production environments - see [Don't Use Musl on FrankenPHP docs](https://frankenphp.dev/docs/performance/#dont-use-musl) - due to performance loss under ZTS mode. We decided not to offer even a development image on Alpine, since your application should be developed on a comparable environment to ensure consistency throughout. + +One of the main benefits of Alpine is that it's typically considered 'distroless' being built up from an empty system, and smaller. We chose to address this by selecting a distroless Debian Trixie base image, which while not as small as Alpine, is still minimised and at time of writing approximately 263MB. + +:::info +For comparison, the FrankenPHP official images at time of writing come in at 182MB for Alpine and 613MB for Debian, neither of which include all the extensions we add. This means our 'distroless' image is actually very similar in size to the Alpine image. +::: + +### Why no install-php-extensions, PHPIZE, PIE etc? + +Put simply, the utilities (apt, deb, make, build-essentials, etc etc) required to install with these tools add a lot of bloat to the final image. They're only needed at build time, and potentially represent a security risk should we leave them within the final image. So, we recommend you do not install such utilities and instead use the provided mechanisms above to install from the provided repository. + +### Why aren't you using Sury's / other repository? + +Sury's PHP repository doesn't offer PHP-ZTS, and the Henderkes repository has the benefit of being officially-recognised by the FrankenPHP team; it is the repo in their documentation for apt-get / rpm / apk installs for FrankenPHP itself, and as a consequence also has the PHP-ZTS packages to link with it. + +The Henderkes repos are also static, which is significantly cleaner for a distroless image, as the only dependency they have is effectively just `gcc-base`. + +## More questions? + +- [Join the Discord server](https://tempestphp.com/discord) +- [Raise an issue on github](https://github.com/tempestphp/tempest-framework/issues) \ No newline at end of file From 82dde7addea46800c86df86c0213965f1c1f9dae Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Thu, 5 Mar 2026 05:37:44 +0000 Subject: [PATCH 7/9] feat(aloft): revert changes to ServeCommand --- packages/router/src/Commands/ServeCommand.php | 55 +++---------------- 1 file changed, 7 insertions(+), 48 deletions(-) diff --git a/packages/router/src/Commands/ServeCommand.php b/packages/router/src/Commands/ServeCommand.php index 52468490e7..76bf4b01d9 100644 --- a/packages/router/src/Commands/ServeCommand.php +++ b/packages/router/src/Commands/ServeCommand.php @@ -4,13 +4,9 @@ namespace Tempest\Router\Commands; -use Tempest\Console\ConsoleArgument; use Tempest\Console\ConsoleCommand; use Tempest\Intl\Number; use Tempest\Support\Str; -use Tempest\Support\Str\ImmutableString; - -use function Tempest\root_path; if (class_exists(\Tempest\Console\ConsoleCommand::class)) { final readonly class ServeCommand @@ -19,56 +15,19 @@ name: 'serve', description: 'Starts a PHP development server', )] - public function __invoke( - string $host = '127.0.0.1', - int $port = 8000, - string $publicDir = 'public/', - #[ConsoleArgument( - description: 'Run via Aloft (Docker) instead of the built-in PHP dev server', - aliases: ['--aloft'], - )] - bool $aloft = false, - ): void { - $resolvedHost = new ImmutableString($host); - $resolvedPort = $port; - $resolvedPublicDir = new ImmutableString($publicDir); + public function __invoke(string $host = '127.0.0.1', int $port = 8000, string $publicDir = 'public/'): void + { + $routerFile = __DIR__ . '/router.php'; - if ($resolvedHost->contains(':')) { - [$rawHost, $overriddenPort] = explode(':', $resolvedHost->toString(), limit: 2); + if (Str\contains($host, ':')) { + [$host, $overriddenPort] = explode(':', $host, limit: 2); - $resolvedHost = new ImmutableString($rawHost ?: '127.0.0.1'); - $resolvedPort = (int) Number\parse($overriddenPort, default: $port); - } + $host = $host ?: '127.0.0.1'; - if ($aloft) { - $this->serveAloft($resolvedHost, $resolvedPort, $resolvedPublicDir); - } else { - $this->serveBuiltin($resolvedHost, $resolvedPort, $resolvedPublicDir); + $port = Number\parse($overriddenPort, default: $port); } - } - - private function serveBuiltin(ImmutableString $host, int $port, ImmutableString $publicDir): void - { - $routerFile = new ImmutableString(__DIR__ . '/router.php'); passthru("php -S {$host}:{$port} -t {$publicDir} {$routerFile}"); } - - private function serveAloft(ImmutableString $host, int $port, ImmutableString $publicDir): void - { - passthru( - "docker run --rm -it -p 80:8000 -p 443:8443 -p 443:8443/udp \ - -v " - . root_path() - . ":/app \ - -v " - . root_path('.tempest/aloft/data') - . ":/data \ - -v " - . root_path('.tempest/aloft/config') - . ":/config \ - tempestphp/aloft:latest-nonroot", - ); - } } } From 8abda9eced682ef6b96b19d95f1d849562039b20 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Thu, 5 Mar 2026 10:08:05 +0000 Subject: [PATCH 8/9] chore(aloft): remove unnecessary package from distroless version --- packages/aloft/stubs/Dockerfile.latest | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/aloft/stubs/Dockerfile.latest b/packages/aloft/stubs/Dockerfile.latest index 221b07b767..baf5f4868d 100644 --- a/packages/aloft/stubs/Dockerfile.latest +++ b/packages/aloft/stubs/Dockerfile.latest @@ -67,8 +67,6 @@ FROM gcr.io/distroless/cc-debian13:nonroot AS runner USER root -COPY --from=builder /bin/busybox /usr/bin/busybox - RUN --mount=type=bind,from=builder,source=/tmp/chroot.tar,target=/tmp/chroot.tar \ --mount=type=bind,from=builder,source=/bin/busybox,target=/bin/busybox \ ["/bin/busybox", "sh", "-c", "\ From eb76ad7a2e75d9685934680969589abeb130b38e Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Sat, 7 Mar 2026 05:28:21 +0000 Subject: [PATCH 9/9] fix(aloft): add info message to edge-case where aloft:serve may try to run a non-existent local image --- packages/aloft/src/AloftServeCommand.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/aloft/src/AloftServeCommand.php b/packages/aloft/src/AloftServeCommand.php index a6b45ce585..730b386f7d 100644 --- a/packages/aloft/src/AloftServeCommand.php +++ b/packages/aloft/src/AloftServeCommand.php @@ -61,6 +61,9 @@ public function build( if ($this->confirm("Do you want to start dev server from {$runImage}?", default: true)) { $this->console->info('Okay, starting, use ctrl-c to exit when finished'); + if ($this->stubsPublished === true && ! ($repository ?? null === 'remote')) { + $this->console->info('Stubs are published, and you are using the local repository, therefore ensure that you run aloft:build before using aloft:serve'); + } passthru( "docker run --rm -it -p 80:8000 -p 443:8443 -p 443:8443/udp \ -v "