From 4c4838dbe795758569a7c367fadb48a2db120c17 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Sun, 22 Feb 2026 19:50:45 +0000 Subject: [PATCH 01/12] 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 02/12] 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 74616ce1ab1a87b0a38396ed8642f6174a4cc3ad Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Mon, 23 Feb 2026 05:43:28 +0000 Subject: [PATCH 03/12] 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 7a8c100e183de632d371a24f867a02f715dcb183 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 25 Feb 2026 05:46:28 +0000 Subject: [PATCH 04/12] 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 1e5e88ad91c393ccc3028639eaa6d64b92654fc5 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 4 Mar 2026 20:33:52 +0000 Subject: [PATCH 05/12] feat(aloft): updated dockerfile and created utility commands --- composer.json | 3 + 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, 440 insertions(+), 404 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 2e39d7bef6..d5bce98446 100644 --- a/composer.json +++ b/composer.json @@ -93,6 +93,7 @@ "wohali/oauth2-discord-new": "^1.2" }, "replace": { + "tempest/aloft": "self.version", "tempest/auth": "self.version", "tempest/cache": "self.version", "tempest/clock": "self.version", @@ -134,6 +135,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", @@ -206,6 +208,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", 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 67fe55e4253964fa016cb77d769cda671cab1c57 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 4 Mar 2026 20:34:35 +0000 Subject: [PATCH 06/12] 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 0e5a9c4c538ee7959cb9e4e312c87f23d71bc530 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Thu, 5 Mar 2026 05:37:44 +0000 Subject: [PATCH 07/12] 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 cfed79c9bd54f960d54665fbda914309b6f7a1f4 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Thu, 5 Mar 2026 10:08:05 +0000 Subject: [PATCH 08/12] 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 2871cb0a227ef1eb7e59362b604490da180df762 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Sat, 7 Mar 2026 05:28:21 +0000 Subject: [PATCH 09/12] 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 " From 1a9a68f771c0f198da089f186b1d9cfad1a5d430 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 11 Mar 2026 13:36:18 +0000 Subject: [PATCH 10/12] refactor(ship): change name to ship, as the starting point for all shipping and deploying utilities --- composer.json | 4 +- docs/0-getting-started/03-docker.md | 76 +++++++++---------- packages/{aloft => ship}/composer.json | 4 +- .../src/ShipBuildCommand.php} | 10 +-- .../src/ShipPublishCommand.php} | 12 +-- .../src/ShipServeCommand.php} | 30 +++----- packages/{aloft => ship}/stubs/.dockerignore | 0 packages/{aloft => ship}/stubs/Caddyfile | 0 .../{aloft => ship}/stubs/Dockerfile.debug | 0 .../{aloft => ship}/stubs/Dockerfile.latest | 0 packages/{aloft => ship}/tests/tests.txt | 0 11 files changed, 63 insertions(+), 73 deletions(-) rename packages/{aloft => ship}/composer.json (82%) rename packages/{aloft/src/AloftBuildCommand.php => ship/src/ShipBuildCommand.php} (91%) rename packages/{aloft/src/AloftPublishCommand.php => ship/src/ShipPublishCommand.php} (86%) rename packages/{aloft/src/AloftServeCommand.php => ship/src/ShipServeCommand.php} (67%) rename packages/{aloft => ship}/stubs/.dockerignore (100%) rename packages/{aloft => ship}/stubs/Caddyfile (100%) rename packages/{aloft => ship}/stubs/Dockerfile.debug (100%) rename packages/{aloft => ship}/stubs/Dockerfile.latest (100%) rename packages/{aloft => ship}/tests/tests.txt (100%) diff --git a/composer.json b/composer.json index d5bce98446..55b27af218 100644 --- a/composer.json +++ b/composer.json @@ -135,7 +135,7 @@ "prefer-stable": true, "autoload": { "psr-4": { - "Tempest\\Aloft\\": "packages/aloft/src", + "Tempest\\Ship\\": "packages/ship/src", "Tempest\\Auth\\": "packages/auth/src", "Tempest\\Cache\\": "packages/cache/src", "Tempest\\Clock\\": "packages/clock/src", @@ -208,7 +208,7 @@ }, "autoload-dev": { "psr-4": { - "Tempest\\Aloft\\Tests\\": "packages/aloft/tests", + "Tempest\\Ship\\Tests\\": "packages/ship/tests", "Tempest\\Auth\\Tests\\": "packages/auth/tests", "Tempest\\Cache\\Tests\\": "packages/cache/tests", "Tempest\\Clock\\Tests\\": "packages/clock/tests", diff --git a/docs/0-getting-started/03-docker.md b/docs/0-getting-started/03-docker.md index 2fd1304f12..1e04e0b1cf 100644 --- a/docs/0-getting-started/03-docker.md +++ b/docs/0-getting-started/03-docker.md @@ -5,13 +5,13 @@ description: Tempest can both be developed or deployed in Production, with our o ## 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. +We are pleased to offer 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 +## 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) +Our CI/CD will automatically generate and publish images to our public repository at https://PLACE.HOLD.ER/tempestphp/ship 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 @@ -25,13 +25,13 @@ We maintain two variants; 'latest' which is rootless and distroless, and is aime ```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 +tempestphp/ship >> tempestphp/ship:1.11.3-8.5.3 #at time of writing +tempestphp/ship:latest >> tempestphp/ship:1.11.3-8.5.3 #at time of writing +tempestphp/ship:debug >> tempestphp/ship: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 +tempestphp/ship:1.11.3-8.5.3 +tempestphp/ship: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. @@ -52,83 +52,83 @@ We won't automatically retire 'patch' version releases i.e. PHP8.5.3 > PHP8.5.4, ## 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. +During development, we'd suggest using the debug image. We've included a convenience command in the `tempest/ship` 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 +./tempest ship: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 +./tempest ship: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 +./tempest ship:serve 1.11.3-8.5.3 # pinned-version, distroless +./tempest ship: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`. +By default, the `ship: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 +./tempest ship: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. +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 `ship: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 +./tempest ship:publish # by default, this will publish the debug dockerfile +./tempest ship: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/ +vendor/tempest/framework/packages/ship/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. +We've provided a simple `ship: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 +./tempest ship:build # will attempt to build debug directly from the package stubs folder +./tempest ship:build debug # will attempt to build debug directly from the package stubs folder +./tempest ship: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/` +./tempest ship:build # will attempt to build debug from `{root_path}/docker/` +./tempest ship:build debug # will attempt to build debug from `{root_path}/docker/` +./tempest ship: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! +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 ShipBuildCommand 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: +You can use the `ship:build` command to pass the arguments: ```bash -./tempest aloft:build {''|debug|latest} --with-frankenphp="1.11.3" --with-php="8.5.3" +./tempest ship: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`. +Note that this will tag the image with 'tempestphp/ship:debug' or 'tempestphp/ship:latest', and remains compatible with `ship: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" +docker build . -t tempestphp/ship: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`. +To retain compatibility with `ship:serve` ensure that the image retains 'tempestphp/ship:' in the filename and then pass `1.11.3-8.5.3` as the image variant i.e. `./tempest ship:serve 1.11.3-8.5.3`. ::: Or you can edit the Dockerfile directly: @@ -137,7 +137,7 @@ 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. +This method also retains compatibility with `ship:serve` and `ship:build`, as long as you keep the filename unchanged. ::: ## Adding additional PHP Extensions @@ -145,7 +145,7 @@ This method also retains compatibility with `aloft:serve` and `aloft:build`, as 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`. +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`. The command won't convert the syntax automatically. ::: ### Adding extensions at build time via build arguments @@ -154,11 +154,11 @@ This method is useful if you need to make a specific build one-off, containing a 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" +docker build . -t ship:with-yaml --build-arg PHP_EXTRA_EXTENSIONS="php-zts-yaml" ``` -Or, you can use the Tempest aloft:build command and pass the optional argument: +Or, you can use the Tempest `ship:build` command and pass the optional argument: ```bash -./tempest aloft:build --with-php-extensions="php-zts-yaml" +./tempest ship:build --with-php-extensions="php-zts-yaml" ``` :::info This will work with both the `debug` and `latest` images. @@ -195,7 +195,7 @@ We suggest one of the following options instead. 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 +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 images ``` You could also create an alias script: ```bash diff --git a/packages/aloft/composer.json b/packages/ship/composer.json similarity index 82% rename from packages/aloft/composer.json rename to packages/ship/composer.json index 686001f82c..0385860a91 100644 --- a/packages/aloft/composer.json +++ b/packages/ship/composer.json @@ -1,6 +1,6 @@ { - "name": "tempest/aloft", - "description": "Development and Production webserver Dockerfiles and utilities for TempestPHP.", + "name": "tempest/ship", + "description": "Development and Production webserver Dockerfiles and utilities for shipping/deploying TempestPHP applications.", "require": { "php": "^8.5", "tempest/core": "3.x-dev", diff --git a/packages/aloft/src/AloftBuildCommand.php b/packages/ship/src/ShipBuildCommand.php similarity index 91% rename from packages/aloft/src/AloftBuildCommand.php rename to packages/ship/src/ShipBuildCommand.php index 3e50bb8325..c6d6096dfa 100644 --- a/packages/aloft/src/AloftBuildCommand.php +++ b/packages/ship/src/ShipBuildCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Aloft; +namespace Tempest\Ship; use Tempest\Console\Console; use Tempest\Console\ConsoleArgument; @@ -12,7 +12,7 @@ use function Tempest\root_path; use function Tempest\Support\Filesystem\exists; -final readonly class AloftBuildCommand +final readonly class ShipBuildCommand { use HasConsole; @@ -34,7 +34,7 @@ public function __construct() } #[ConsoleCommand( - name: 'aloft:build', + name: 'ship:build', description: 'Build the Aloft Docker image locally, and publish the stub files if not already present.', )] public function build( @@ -67,9 +67,9 @@ public function build( ])); $buildPath = ($this->stubsPublished ? root_path('docker') : dirname(__DIR__) . DIRECTORY_SEPARATOR . 'stubs') . DIRECTORY_SEPARATOR; - $buildFile = "{$buildPath}Dockerfile.{$variant} -t tempestphp/aloft:{$variant}"; + $buildFile = "{$buildPath}Dockerfile.{$variant} -t tempestphp/ship:{$variant}"; - if ($this->confirm("Do you want to build tempestphp/aloft:{$variant}?", default: false)) { + if ($this->confirm("Do you want to build tempestphp/ship:{$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/ship/src/ShipPublishCommand.php similarity index 86% rename from packages/aloft/src/AloftPublishCommand.php rename to packages/ship/src/ShipPublishCommand.php index 4d3b7d5a08..df972b15e2 100644 --- a/packages/aloft/src/AloftPublishCommand.php +++ b/packages/ship/src/ShipPublishCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Aloft; +namespace Tempest\Ship; use Tempest\Console\Console; use Tempest\Console\ConsoleArgument; @@ -19,14 +19,14 @@ use function Tempest\Support\Filesystem\exists; use function Tempest\Support\str; -final readonly class AloftPublishCommand +final readonly class ShipPublishCommand { use HasConsole; #[ConsoleCommand( - name: 'aloft:publish', + name: 'ship:publish', description: 'Publish the Aloft Docker stubs, for the debug image.', - aliases: ['aloft:publish:debug', 'aloft:publish:dev'], + aliases: ['ship:publish:debug', 'ship:publish:dev'], )] public function publish(string $variant = 'debug'): void { @@ -53,9 +53,9 @@ function (string $dstFile, string $srcFile) use ($srcPath, $dstPath) { } #[ConsoleCommand( - name: 'aloft:publish:latest', + name: 'ship:publish:latest', description: 'Publish the Aloft Docker stubs, for the distroless image.', - aliases: ['aloft:publish:distroless', 'aloft:publish:prod'], + aliases: ['ship:publish:distroless', 'ship:publish:prod'], )] public function publishLatest(): void { diff --git a/packages/aloft/src/AloftServeCommand.php b/packages/ship/src/ShipServeCommand.php similarity index 67% rename from packages/aloft/src/AloftServeCommand.php rename to packages/ship/src/ShipServeCommand.php index 730b386f7d..e720f9b00f 100644 --- a/packages/aloft/src/AloftServeCommand.php +++ b/packages/ship/src/ShipServeCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Aloft; +namespace Tempest\Ship; use Tempest\Console\Console; use Tempest\Console\ConsoleArgument; @@ -12,7 +12,7 @@ use function Tempest\root_path; use function Tempest\Support\Filesystem\exists; -final readonly class AloftServeCommand +final readonly class ServeCommand { use HasConsole; @@ -38,17 +38,17 @@ public function __construct() } #[ConsoleCommand( - name: 'aloft:build', - description: 'Build the Aloft Docker image locally, and publish the stub files if not already present.', + name: 'ship:serve', + description: 'Run a development or production docker container to serve your application.', )] - public function build( + public function serve( #[ConsoleArgument( - description: 'The build variant to use.', + description: 'The docker container variant to use.', )] string $requestedVariant = '', #[ConsoleArgument( name: 'repository', - description: 'Space-separated list of extra extensions to include in the build.', + description: 'The repository path to retrieve the docker container from.', )] ?string $repository = null, ): void { @@ -57,25 +57,15 @@ public function build( // TODO: Catch local development paths from composer.json and insert them as volumes - $runImage = "{$repo}tempestphp/aloft:{$variant}"; + $runImage = "{$repo}tempestphp/ship:{$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'); 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'); + $this->console->info('Stubs are published, and you are using the local repository, therefore ensure that you run ship:build before using ship:serve'); } 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}", + 'docker run --rm -it -p 80:8000 -p 443:8443 -p 443:8443/udp -v ' . root_path() . ":/app {$runImage}", ); } } diff --git a/packages/aloft/stubs/.dockerignore b/packages/ship/stubs/.dockerignore similarity index 100% rename from packages/aloft/stubs/.dockerignore rename to packages/ship/stubs/.dockerignore diff --git a/packages/aloft/stubs/Caddyfile b/packages/ship/stubs/Caddyfile similarity index 100% rename from packages/aloft/stubs/Caddyfile rename to packages/ship/stubs/Caddyfile diff --git a/packages/aloft/stubs/Dockerfile.debug b/packages/ship/stubs/Dockerfile.debug similarity index 100% rename from packages/aloft/stubs/Dockerfile.debug rename to packages/ship/stubs/Dockerfile.debug diff --git a/packages/aloft/stubs/Dockerfile.latest b/packages/ship/stubs/Dockerfile.latest similarity index 100% rename from packages/aloft/stubs/Dockerfile.latest rename to packages/ship/stubs/Dockerfile.latest diff --git a/packages/aloft/tests/tests.txt b/packages/ship/tests/tests.txt similarity index 100% rename from packages/aloft/tests/tests.txt rename to packages/ship/tests/tests.txt From 7b63f76e8287f1564efdcb3a2b623db160ded8b4 Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 11 Mar 2026 13:55:45 +0000 Subject: [PATCH 11/12] docs(ship): add env vars to docs --- docs/0-getting-started/03-docker.md | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/0-getting-started/03-docker.md b/docs/0-getting-started/03-docker.md index 1e04e0b1cf..fbf746a5c0 100644 --- a/docs/0-getting-started/03-docker.md +++ b/docs/0-getting-started/03-docker.md @@ -207,6 +207,44 @@ This would allow you to execute `composer install` from the command line, via do You can find more detailed instructions for running composer via docker [here](https://github.com/docker-library/docs/tree/master/composer). ::: +## Environment variables + +The following .env variables are exposed within the Caddyfile. Those requiring defaults already have them, so in many cases you can simply ignore these. + +```bash +# Defaults to 8000, can be changed to any port above 1024 +CADDY_HTTP_PORT +# Defaults to 8443, can be changed to any port above 1024, must be unique +CADDY_HTTPS_PORT +# Use this to insert any Caddy global options, carried forward from FrankenPHP +CADDY_GLOBAL_OPTIONS +# Use this to specify any FrankenPHP global options, carried forward from FrankenPHP +FRANKENPHP_CONFIG + +# Use this to specify any extra Caddy config that doesn't belong in the global or site blocks, carried forward from FrankenPHP +CADDY_EXTRA_CONFIG + +# Specify the FQDN, defaults to localhost +CADDY_SERVER_NAME +# Where to serve the app from, defaults to public/, meaning /app/public/ as app is the WORKDIR +CADDY_SERVER_ROOT + +# Configure the mercure module, carried forward from FrankenPHP +MERCURE_PUBLISHER_JWT_KEY +MERCURE_PUBLISHER_JWT_ALG +MERCURE_SUBSCRIBER_JWT_KEY +MERCURE_SUBSCRIBER_JWT_ALG +MERCURE_EXTRA_DIRECTIVES + +# Any additional Caddy directives for the site-block, carried forward from FrankenPHP +CADDY_SERVER_EXTRA_DIRECTIVES +``` + +You can also map a volume to a folder containing Caddyfile and pass your own Caddyfile, should you wish. Assuming that you've stored the Caddyfile in `/my/local/caddyconfig`: +```bash +docker run -v /my/local/caddyconfig:/etc/frankenphp/ +``` + ## Frequently asked questions ### Why no Alpine image? From 01ce719e688548cb7e396497c246734cf99cd4cb Mon Sep 17 00:00:00 2001 From: iamdadmin Date: Wed, 11 Mar 2026 13:56:14 +0000 Subject: [PATCH 12/12] refactor(ship): tidy-up Caddyfile and remove unused ENV var --- packages/ship/stubs/Caddyfile | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ship/stubs/Caddyfile b/packages/ship/stubs/Caddyfile index 71f846e85e..c4e4baf5ca 100644 --- a/packages/ship/stubs/Caddyfile +++ b/packages/ship/stubs/Caddyfile @@ -7,7 +7,6 @@ { skip_install_trust - {$CADDY_DEFAULT_BIND} http_port {$CADDY_HTTP_PORT:8000} https_port {$CADDY_HTTPS_PORT:8443}