From f867b0705bdc872477d26acc5efc5787a4d1ad06 Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Thu, 30 Jul 2026 00:24:12 -0400 Subject: [PATCH 1/4] feat: optimize FrankenPHP images --- AGENTS.md | 46 +++++----- CLAUDE.md | 33 ------- Dockerfile | 262 +++++++++++++++++++++++++++++++++++++++++------------ README.md | 111 +++++++++++++++++++++++ test.php | 21 ++++- 5 files changed, 356 insertions(+), 117 deletions(-) delete mode 100644 CLAUDE.md create mode 100644 README.md diff --git a/AGENTS.md b/AGENTS.md index 00941fe..f92d92a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,33 +1,29 @@ -# Agent Guidelines for FrankenPHP Docker Images +# Repository Guidelines -## Build Commands +## Project Structure & Module Organization -- **Build all variants**: `docker buildx bake` -- **Build specific variant**: `docker buildx bake runner-php-8-4-bookworm` -- **Build with push**: `docker buildx bake --push` -- **Print build matrix**: `docker buildx bake --print` +This repository builds the `ghcr.io/prvious/frankenphp` development and production images. `Dockerfile` defines the binary/tool builders and the shared `base`, `dev`, and `prod` stages. `docker-bake.hcl` expands PHP versions and targets across `linux/amd64` and `linux/arm64`. Container behavior is configured by `.env`, `.zshrc`, and `.zshrc.prod`. Executable Laravel health checks live under `usr/local/bin/`. Image validation is centralized in `test.php`, while `.github/workflows/pipeline.yml` builds, tests, and publishes the matrix. -## Test Commands +## Build, Test, and Development Commands -- **Test image**: `docker run --rm -it php -v` -- **Test container**: `docker run --rm -p 80:80 ` -- **Run with shell**: `docker run --rm -it bash` +- `docker build --build-arg VERSION=8.4 --target dev -t frankenphp:dev .` builds the local development image. +- `docker build --build-arg VERSION=8.4 --target prod -t frankenphp:prod .` builds the production image. +- `docker buildx bake --print` inspects the expanded build matrix; provide `PHP_VERSION`, `SHA`, and `LATEST` as CI does when required. +- `docker buildx bake` builds all configured variants and architectures. +- `docker run --rm -v "$PWD/test.php:/app/test.php" frankenphp:dev php /app/test.php dev` validates extensions, binaries, and pnpm configuration. Substitute the production image and `production` for that target. -## Available Aliases (from env.sh) +## Coding Style & Naming Conventions -- `pint` - Laravel Pint formatter: `./vendor/bin/pint` -- `pa` - PHP Artisan: `php artisan` -- `stan`/`phpstan` - PHPStan: `./vendor/bin/phpstan` -- `pest` - Pest testing: `./vendor/bin/pest` -- `amf` - Migrate fresh: `php artisan migrate:fresh` -- `amfs` - Migrate fresh with seed: `php artisan migrate:fresh --seed` +Use four-space indentation in PHP, HCL, and workflow YAML. Keep PHP strictly typed and follow the existing PSR-12-style class and method layout; use `camelCase` methods and `UPPER_SNAKE_CASE` constants. In Dockerfile steps, group related packages, use uppercase build arguments, quote shell variables, and clean package caches in the same layer. Health-check filenames use the `healthcheck-` pattern and must remain executable POSIX shell scripts. -## Code Style Guidelines +## Testing Guidelines -- **HCL**: 4-space indentation, descriptive variables (e.g., `IMAGE_NAME`, `PHP_VERSION`) -- **Shell**: Use `set -exo pipefail`, proper quoting, environment variable prefixes -- **Docker**: Multi-platform builds (`linux/amd64`, `linux/arm64`), proper labels -- **Tags**: Use semantic versioning, clean tag function for sanitization -- **Functions**: HCL functions for reusable logic (tag generation, version parsing) -- **Matrix builds**: Support multiple PHP versions and OS variants -- **Labels**: Include OpenContainers standard labels with metadata +There is no external test framework or coverage threshold. `test.php` is the acceptance suite. Update its expected extension and binary arrays whenever image contents change, then test both `dev` and `production`. For architecture-sensitive changes, run the Buildx matrix or rely on the PR workflow for both supported platforms. + +## Commit & Pull Request Guidelines + +Prefer short, imperative Conventional Commit subjects such as `feat: add healthcheck script`, `fix: correct pnpm path`, or `chore: remove package`; avoid `wip` commits in review-ready branches. Pull requests should explain the affected stage or variant, note tag/platform impact, link relevant issues, and list exact build and test commands with results. Screenshots are only useful for user-visible shell behavior. + +## Security & Configuration + +`.env` is copied into `/etc/profile.d/.env`; keep it limited to non-secret container defaults and aliases. Never commit registry tokens, credentials, or application secrets. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 00941fe..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,33 +0,0 @@ -# Agent Guidelines for FrankenPHP Docker Images - -## Build Commands - -- **Build all variants**: `docker buildx bake` -- **Build specific variant**: `docker buildx bake runner-php-8-4-bookworm` -- **Build with push**: `docker buildx bake --push` -- **Print build matrix**: `docker buildx bake --print` - -## Test Commands - -- **Test image**: `docker run --rm -it php -v` -- **Test container**: `docker run --rm -p 80:80 ` -- **Run with shell**: `docker run --rm -it bash` - -## Available Aliases (from env.sh) - -- `pint` - Laravel Pint formatter: `./vendor/bin/pint` -- `pa` - PHP Artisan: `php artisan` -- `stan`/`phpstan` - PHPStan: `./vendor/bin/phpstan` -- `pest` - Pest testing: `./vendor/bin/pest` -- `amf` - Migrate fresh: `php artisan migrate:fresh` -- `amfs` - Migrate fresh with seed: `php artisan migrate:fresh --seed` - -## Code Style Guidelines - -- **HCL**: 4-space indentation, descriptive variables (e.g., `IMAGE_NAME`, `PHP_VERSION`) -- **Shell**: Use `set -exo pipefail`, proper quoting, environment variable prefixes -- **Docker**: Multi-platform builds (`linux/amd64`, `linux/arm64`), proper labels -- **Tags**: Use semantic versioning, clean tag function for sanitization -- **Functions**: HCL functions for reusable logic (tag generation, version parsing) -- **Matrix builds**: Support multiple PHP versions and OS variants -- **Labels**: Include OpenContainers standard labels with metadata diff --git a/Dockerfile b/Dockerfile index cb15c91..c2a278a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,93 @@ ARG VERSION -FROM dunglas/frankenphp:php${VERSION} AS base +FROM dunglas/frankenphp:php${VERSION} AS frankenphp-base + +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# BUILDER STAGE: Download static binaries and tools +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +FROM debian:bookworm-slim AS binaries-builder + +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + jq \ + tar \ + gzip \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /tmp/binaries + +# Download minimal Node.js binary (just node, no npm) +RUN NODE_VERSION=26 \ + && if [ "$TARGETARCH" = "amd64" ]; then NODE_ARCH="x64"; else NODE_ARCH="arm64"; fi \ + && LATEST_NODE=$(curl -s https://nodejs.org/dist/latest-v${NODE_VERSION}.x/ | grep -oP "node-v${NODE_VERSION}\.\d+\.\d+" | head -1) \ + && curl -sL "https://nodejs.org/dist/latest-v${NODE_VERSION}.x/${LATEST_NODE}-linux-${NODE_ARCH}.tar.gz" | tar -xz \ + && mkdir -p /tmp/binaries/node/bin \ + && mv ${LATEST_NODE}-linux-${NODE_ARCH}/bin/node /tmp/binaries/node/bin/ \ + && rm -rf ${LATEST_NODE}-linux-${NODE_ARCH} + +# Download the latest pnpm 11 release +RUN PNPM_VERSION=$(curl -fsSL https://registry.npmjs.org/@pnpm/exe | jq -r '.["dist-tags"]["latest-11"]') \ + && test -n "${PNPM_VERSION}" \ + && test "${PNPM_VERSION}" != "null" \ + && curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION="${PNPM_VERSION}" PNPM_HOME=/tmp/binaries/pnpm bash - + +# Install svgo using pnpm in builder +RUN export PATH="/tmp/binaries/node/bin:/tmp/binaries/pnpm:$PATH" \ + && export PNPM_HOME=/tmp/binaries/pnpm \ + && mkdir -p /tmp/binaries/pnpm-global \ + && pnpm config set store-dir /tmp/pnpm-store --global \ + && pnpm config set global-dir /tmp/binaries/pnpm-global --global \ + && pnpm add -g svgo \ + && rm -rf /tmp/pnpm-store + +# Download development-only binaries +FROM binaries-builder AS dev-binaries-builder + +# Download GitHub CLI +RUN GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | jq -r .tag_name | sed 's/^v//') \ + && if [ "$TARGETARCH" = "amd64" ]; then ARCH="amd64"; else ARCH="arm64"; fi \ + && curl -sL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.tar.gz" -o gh.tar.gz \ + && tar -xzf gh.tar.gz \ + && mv gh_${GH_VERSION}_linux_${ARCH}/bin/gh gh \ + && rm -rf gh*.tar.gz gh_* + +# Download eza +RUN EZA_VERSION=$(curl -s https://api.github.com/repos/eza-community/eza/releases/latest | jq -r .tag_name | sed 's/^v//') \ + && if [ "$TARGETARCH" = "amd64" ]; then EZA_ARCH="x86_64"; else EZA_ARCH="aarch64"; fi \ + && curl -sL "https://github.com/eza-community/eza/releases/download/v${EZA_VERSION}/eza_${EZA_ARCH}-unknown-linux-gnu.tar.gz" | tar -xz + +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# BUILDER STAGE: Image optimization tools +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +FROM debian:bookworm-slim AS tools-builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + jpegoptim \ + optipng \ + pngquant \ + gifsicle \ + libavif-bin \ + ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +# Copy binaries to a clean location +RUN mkdir -p /tmp/tools \ + && cp /usr/bin/jpegoptim /tmp/tools/ \ + && cp /usr/bin/optipng /tmp/tools/ \ + && cp /usr/bin/pngquant /tmp/tools/ \ + && cp /usr/bin/gifsicle /tmp/tools/ \ + && cp /usr/bin/avifenc /tmp/tools/ \ + && cp /usr/bin/ffmpeg /tmp/tools/ + +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# BASE STAGE: FrankenPHP base with minimal dependencies +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +FROM frankenphp-base AS base SHELL [ "/bin/bash", "-l", "-exo", "pipefail", "-c" ] @@ -18,73 +106,128 @@ ENV PATH=$PNPM_HOME:$PATH ENV PNPM_STORE_DIR=/home/${USER}/.pnpm-store COPY ./.env /etc/profile.d/.env -COPY ./usr/local/bin/* /usr/local/bin/ +COPY --chmod=755 ./usr/local/bin/* /usr/local/bin/ + +# Copy composer from official image +COPY --from=composer:2 --chmod=755 /usr/bin/composer /usr/bin/composer + +# Copy Node.js (just the binary) and pnpm from builder +COPY --from=binaries-builder /tmp/binaries/node /usr/local/node +COPY --from=binaries-builder /tmp/binaries/pnpm /usr/local/share/pnpm +COPY --from=binaries-builder /tmp/binaries/pnpm-global /usr/local/share/pnpm-global -RUN apt update \ - && apt-get install -y gnupg lsb-release ca-certificates curl \ +# Copy image optimization tools from builder +COPY --from=tools-builder --chmod=755 /tmp/tools/* /usr/local/bin/ + +# Add Node.js, pnpm, and global packages to PATH +ENV PATH=/usr/local/node/bin:/usr/local/share/pnpm:/usr/local/share/pnpm-global/bin:$PATH + +# Minimal system packages (no Node, no image tools, no gh/eza) +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + gnupg \ + lsb-release \ + ca-certificates \ + supervisor \ + git \ + unzip \ + zsh \ + procps \ + # Required runtime libs for image tools + libjpeg62-turbo \ + libpng16-16 \ + libavif15 \ + libavcodec59 \ + libavformat59 \ + libavutil57 \ + libswscale6 \ + libswresample4 \ && echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ && curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/pgdg.gpg \ && apt-get update \ - && apt-get install -y supervisor git unzip postgresql-client-17 default-mysql-client zsh procps \ + && apt-get install -y --no-install-recommends \ + postgresql-client-17 \ + default-mysql-client \ && echo 'source /etc/profile.d/.env' >> /etc/bash.bashrc \ - && curl -fsSL https://get.pnpm.io/install.sh | env PNPM_HOME="${PNPM_HOME}" bash - \ - && export PATH="${PNPM_HOME}:${PATH}" \ - && pnpm config set store-dir /home/${USER}/.pnpm-store --global \ - && pnpm env use --global 24 \ - && npm install -g npm \ - && apt install -y jpegoptim optipng pngquant gifsicle libavif-bin ffmpeg \ - && pnpm add -g svgo \ - && install-php-extensions @composer mysqli pdo_mysql pgsql pdo_pgsql bcmath gd imagick imap pcntl zip intl exif ftp xml pdo_sqlsrv sqlsrv sockets \ - && cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" \ - && groupadd --force -g $WWWGROUP ${USER} \ - && useradd -m --no-user-group -o -g $WWWGROUP -u ${WWWUSER} -s /bin/zsh ${USER} \ - && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp \ - && chmod +x /usr/local/bin/* \ - && mkdir -p /home/${USER}/.config/psysh \ - && chown -R ${USER}:${USER} /home/${USER}/.config \ - && chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy && chown -R ${USER}:${USER} /app \ - && apt-get -y autoremove \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + && rm -rf /var/lib/apt/lists/* + +# Configure pnpm store directory +RUN export PATH="/usr/local/node/bin:/usr/local/share/pnpm:$PATH" \ + && pnpm config set store-dir /home/${USER}/.pnpm-store --global + +# PHP extensions +RUN install-php-extensions \ + mysqli \ + pdo_mysql \ + pgsql \ + pdo_pgsql \ + bcmath \ + gd \ + imagick \ + imap \ + pcntl \ + zip \ + intl \ + exif \ + ftp \ + xml \ + pdo_sqlsrv \ + sqlsrv \ + sockets \ + && cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" + +# User creation and permissions +RUN groupadd --force -g ${WWWGROUP} ${USER} \ + && useradd -m --no-user-group -o -g ${WWWGROUP} -u ${WWWUSER} -s /bin/zsh ${USER} \ + && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp \ + && mkdir -p /home/${USER}/.local/bin \ + && chown -R ${USER}:${USER} /home/${USER} /data/caddy /config/caddy + +# Final cleanup - remove unnecessary files +RUN rm -rf \ + /usr/share/doc/* \ + /usr/share/man/* \ + /usr/share/locale/* \ + /var/cache/apt/* \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /root/.cache \ + /root/.npm #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +# DEV STAGE +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- FROM base AS dev +# Copy development binaries +COPY --from=dev-binaries-builder --chmod=755 /tmp/binaries/gh /usr/local/bin/gh +COPY --from=dev-binaries-builder --chmod=755 /tmp/binaries/eza /usr/local/bin/eza + +# Minimal development packages RUN apt-get update \ - && install-php-extensions xdebug \ - && (type -p wget >/dev/null || (apt update && apt install wget -y)) \ - && mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && mkdir -p -m 755 /etc/apt/sources.list.d \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && apt-get update \ - && apt-get install -y htop nano gh zsh \ - && curl -sS https://starship.rs/install.sh | sh -s -- --yes \ - && mkdir -p /etc/apt/keyrings \ - && wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc | gpg --dearmor -o /etc/apt/keyrings/gierens.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" | tee /etc/apt/sources.list.d/gierens.list \ - && chmod 644 /etc/apt/keyrings/gierens.gpg /etc/apt/sources.list.d/gierens.list \ - && apt update \ - && apt install -y eza \ - && pnpm add -g opencode-ai \ - && apt-get -y autoremove \ + && apt-get install -y --no-install-recommends \ + htop \ + nano \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + && rm -rf /var/lib/apt/lists/* -# Copy zshrc first so we can pre-download plugins -COPY --chown=${USER}:${USER} ./.zshrc /home/${USER}/.zshrc - -# Create directories for user tools -RUN mkdir -p /home/${USER}/.local/share /home/${USER}/.config /home/${USER}/.fzf \ - && chown -R ${USER}:${USER} /home/${USER} +# Development PHP extensions +RUN install-php-extensions xdebug -# Switch to the deploy user to install zinit and tools +# Switch to deploy user for user-level installations USER ${USER} -# Install zinit, fzf, zoxide, starship preset, and pre-download all zinit plugins +# Install starship and opencode +RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir=/home/${USER}/.local/bin \ + && curl -fsSL https://opencode.ai/install | bash + +# Copy zshrc and create user directories +COPY --chown=${USER}:${USER} ./.zshrc /home/${USER}/.zshrc + +# Install zinit, fzf, zoxide, and configure starship RUN ZINIT_HOME="/home/${USER}/.local/share/zinit" NO_EDIT=1 NO_TUTORIAL=1 \ bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)" \ && git clone --depth 1 https://github.com/junegunn/fzf.git /home/${USER}/.fzf \ @@ -94,22 +237,27 @@ RUN ZINIT_HOME="/home/${USER}/.local/share/zinit" NO_EDIT=1 NO_TUTORIAL=1 \ && starship preset no-nerd-font -o /home/${USER}/.config/starship.toml \ && zsh -i -c 'zinit self-update && exit 0' || true -# Add user bin directories to PATH for fzf and zoxide ENV PATH=/home/deploy/.local/bin:/home/deploy/.fzf/bin:$PATH WORKDIR /app - #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - +# PROD STAGE +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + FROM base AS prod +# Use production PHP configuration RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" -# Copy zshrc first so we can pre-download plugins +# Copy production zshrc COPY --chown=${USER}:${USER} ./.zshrc.prod /home/${USER}/.zshrc -# Switch to the deploy user to install zinit and pre-download plugins +# Create user directories before switching user +RUN mkdir -p /home/${USER}/.local/share \ + && chown -R ${USER}:${USER} /home/${USER} + +# Switch to deploy user for user-level installations USER ${USER} # Install zinit and pre-download plugins @@ -117,4 +265,4 @@ RUN ZINIT_HOME="/home/${USER}/.local/share/zinit" NO_EDIT=1 NO_TUTORIAL=1 \ bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)" \ && zsh -i -c 'zinit self-update && exit 0' || true -WORKDIR /app \ No newline at end of file +WORKDIR /app diff --git a/README.md b/README.md new file mode 100644 index 0000000..35d6a14 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# FrankenPHP Docker Images + +Custom [FrankenPHP](https://frankenphp.dev) Docker images with batteries included for Laravel development and production. + +## Quick Start + +```bash +# Development (includes xdebug, dev tools) +docker pull ghcr.io/prvious/frankenphp:php8.4-dev + +# Production +docker pull ghcr.io/prvious/frankenphp:php8.4 +``` + +## What's Included + +### Base (both dev & prod) +- **PHP Extensions**: mysqli, pdo_mysql, pgsql, pdo_pgsql, bcmath, gd, imagick, imap, pcntl, zip, intl, exif, xml, sqlsrv, pdo_sqlsrv, sockets +- **Node.js**: v26 +- **Package Managers**: Composer, pnpm v11 +- **Database Clients**: PostgreSQL 17, MySQL +- **Image Tools**: jpegoptim, optipng, pngquant, gifsicle, avifenc, svgo, ffmpeg +- **Process Manager**: Supervisor +- **Shell**: Zsh with zinit + +### Dev Only +- **PHP**: Xdebug +- **Tools**: GitHub CLI, htop, nano, fzf, zoxide, eza +- **Shell**: Starship prompt, syntax highlighting, autosuggestions, fzf-tab + +## Available Tags + +| Tag | Description | +|-----|-------------| +| `php8.4` | PHP 8.4 production | +| `php8.4-dev` | PHP 8.4 development | +| `php8.3` | PHP 8.3 production | +| `php8.3-dev` | PHP 8.3 development | +| `latest` | Latest PHP production | +| `latest-dev` | Latest PHP development | + +All images are multi-arch: `linux/amd64` and `linux/arm64`. + +## Usage with Laravel + +```yaml +# docker-compose.yml +services: + app: + image: ghcr.io/prvious/frankenphp:php8.4-dev + ports: + - "80:80" + - "443:443" + volumes: + - .:/app + working_dir: /app +``` + +### Laravel Aliases + +These aliases are available inside the container: + +```bash +pa # php artisan +pint # ./vendor/bin/pint +pest # ./vendor/bin/pest +stan # ./vendor/bin/phpstan +amf # php artisan migrate:fresh +amfs # php artisan migrate:fresh --seed +``` + +### Health Checks + +Built-in health check scripts for Laravel services: + +```bash +healthcheck-horizon # Check Laravel Horizon +healthcheck-octane # Check Laravel Octane +healthcheck-queue # Check queue workers +healthcheck-schedule # Check scheduler +``` + +## Building Locally + +```bash +# Build dev image +docker build --build-arg VERSION=8.4 --target dev -t frankenphp:dev . + +# Build prod image +docker build --build-arg VERSION=8.4 --target prod -t frankenphp:prod . + +# Build all variants with bake +docker buildx bake +``` + +## Configuration + +The container runs as user `deploy` (UID 1000). Key paths: + +- **App**: `/app` +- **pnpm store**: `/home/deploy/.pnpm-store` +- **Caddy data**: `/data/caddy` +- **Caddy config**: `/config/caddy` + +Environment variables: +- `SERVER_NAME=:80` - Caddy server name +- `TZ=UTC` - Timezone + +## License + +MIT diff --git a/test.php b/test.php index a49f6fa..c19e131 100644 --- a/test.php +++ b/test.php @@ -2,6 +2,9 @@ declare(strict_types=1); +const NODE_MAJOR_VERSION = 26; +const PNPM_MAJOR_VERSION = 11; + const PRODUCTION_EXTENSIONS = ['mysqli', 'pdo_mysql', 'pgsql', 'pdo_pgsql', 'bcmath', 'gd', 'imagick', 'imap', 'pcntl', 'zip', 'intl', 'exif', 'ftp', 'xml', 'pdo_sqlsrv', 'sqlsrv', 'sockets']; const PRODUCTION_ONLY_EXTENSIONS = []; @@ -13,11 +16,11 @@ ...DEV_ONLY_EXTENSIONS, ]; -const PRODUCTION_BINARIES = ['php', 'composer', 'node', 'npm', 'pnpm', 'jpegoptim', 'optipng', 'pngquant', 'gifsicle', 'ffmpeg', 'svgo', 'avifenc', 'zsh']; +const PRODUCTION_BINARIES = ['php', 'composer', 'node', 'pnpm', 'jpegoptim', 'optipng', 'pngquant', 'gifsicle', 'ffmpeg', 'svgo', 'avifenc', 'zsh']; const PRODUCTION_ONLY_BINARIES = []; -const DEV_ONLY_BINARIES = ['gh', 'htop', 'nano', 'fzf', 'zoxide']; +const DEV_ONLY_BINARIES = ['gh', 'eza', 'htop', 'nano', 'fzf', 'zoxide']; const DEV_BINARIES = [ ...PRODUCTION_BINARIES, @@ -209,6 +212,20 @@ function sanity(Runner $runner): void $modules = @shell_exec('php -m 2>/dev/null'); $runner->check('php-cli works', is_string($modules) && trim($modules) !== ''); + $nodeVersion = trim(@shell_exec('node --version 2>/dev/null') ?? ''); + $runner->check( + 'node major version', + str_starts_with($nodeVersion, 'v' . NODE_MAJOR_VERSION . '.'), + 'expected v' . NODE_MAJOR_VERSION . ".x, got {$nodeVersion}" + ); + + $pnpmVersion = trim(@shell_exec('pnpm --version 2>/dev/null') ?? ''); + $runner->check( + 'pnpm major version', + str_starts_with($pnpmVersion, PNPM_MAJOR_VERSION . '.'), + 'expected ' . PNPM_MAJOR_VERSION . ".x, got {$pnpmVersion}" + ); + if ($runner->environment === 'dev') { $pnpmStorePath = trim(@shell_exec('pnpm store path 2>/dev/null') ?? ''); $expectedPath = '/home/deploy/.pnpm-store'; From 5bc824c342a7bf075c2b5bb8ea8d7738d8febe73 Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Thu, 30 Jul 2026 01:53:50 -0400 Subject: [PATCH 2/4] fix: harden image builds --- .env | 4 +- .github/workflows/pipeline.yml | 19 +- .zshrc | 42 ++--- .zshrc.prod | 23 +-- AGENTS.md | 8 +- Dockerfile | 320 ++++++++++++++++++--------------- LICENSE | 21 +++ README.md | 34 ++-- docker-bake.hcl | 13 +- test.php | 124 ++++++++----- 10 files changed, 363 insertions(+), 245 deletions(-) create mode 100644 LICENSE diff --git a/.env b/.env index 10f769d..82ae482 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ export XDG_CONFIG_HOME=$HOME/.config export XDG_DATA_HOME=$HOME/.data export XDG_CACHE_HOME=$HOME/.cache export XDG_STATE_HOME=$HOME/.state -export PNPM_STORE_PATH=${XDG_DATA_HOME}/pnpm-store +export PNPM_STORE_PATH=$HOME/.pnpm-store export ZSH="$HOME/.oh-my-zsh" mkdir -p $XDG_CONFIG_HOME $XDG_DATA_HOME $XDG_CACHE_HOME $XDG_STATE_HOME $PNPM_STORE_PATH @@ -13,4 +13,4 @@ alias stan="./vendor/bin/phpstan" alias phpstan="./vendor/bin/phpstan" alias pest="./vendor/bin/pest" alias amf="php artisan migrate:fresh" -alias amfs="php artisan migrate:fresh --seed" \ No newline at end of file +alias amfs="php artisan migrate:fresh --seed" diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index b0b6ba4..921980a 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-24.04 outputs: # Push if it's a scheduled job, a tag, or if we're committing to the main branch - push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }} + push: ${{ github.event_name == 'schedule' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request') }} variants: ${{ steps.matrix.outputs.variants }} platforms: ${{ steps.matrix.outputs.platforms }} metadata: ${{ steps.matrix.outputs.metadata }} @@ -65,15 +65,15 @@ jobs: done <<< "$ALL_PHP_VERSIONS" # Get the last 2 minor versions - MINOR_VERSIONS_SORTED=($(printf '%s\n' "${!LATEST_MINOR_VERSIONS[@]}" | sort -V | tail -2)) + mapfile -t MINOR_VERSIONS_SORTED < <(printf '%s\n' "${!LATEST_MINOR_VERSIONS[@]}" | sort -V | tail -2) if [[ ${#MINOR_VERSIONS_SORTED[@]} -lt 2 ]]; then echo "โŒ Could not find 2 different minor versions" exit 1 fi - PHP_VERSION_1="8.3" - PHP_VERSION_2="8.4" + PHP_VERSION_1="${MINOR_VERSIONS_SORTED[0]}" + PHP_VERSION_2="${MINOR_VERSIONS_SORTED[1]}" echo "โœ… Selected PHP minor versions: ${PHP_VERSION_1} and ${PHP_VERSION_2}" echo " Latest patches: ${LATEST_MINOR_VERSIONS[$PHP_VERSION_1]} and ${LATEST_MINOR_VERSIONS[$PHP_VERSION_2]}" @@ -199,6 +199,7 @@ jobs: env: PHP_VERSION: ${{ steps.check.outputs.php_version }} LATEST: ${{ steps.check.outputs.latest_version }} + SHA: ${{ github.sha }} build: runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} needs: @@ -265,6 +266,7 @@ jobs: env: PHP_VERSION: ${{ needs.prepare.outputs.php_version }} LATEST: ${{ needs.prepare.outputs.latest_version }} + SHA: ${{ github.sha }} - name: Upload image artifact uses: actions/upload-artifact@v4 @@ -367,11 +369,10 @@ jobs: - name: Test image environment run: | # Get the built image name - if [[ "${{ matrix.variant }}" == *"-dev" ]]; then - ENV_TYPE="dev" - else - ENV_TYPE="production" - fi + case "${{ matrix.variant }}" in + *-dev) ENV_TYPE="dev" ;; + *) ENV_TYPE="production" ;; + esac echo "๐Ÿงช Testing ${ENV_TYPE} environment in image: ${LOADED_IMAGE_ID}" diff --git a/.zshrc b/.zshrc index b97e663..58f7392 100644 --- a/.zshrc +++ b/.zshrc @@ -8,45 +8,46 @@ if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi -# Zinit installation +# Load the build-pinned Zinit installation ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git" -if [[ ! -d "$ZINIT_HOME" ]]; then - mkdir -p "$(dirname $ZINIT_HOME)" - git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME" +if [[ ! -r "$ZINIT_HOME/zinit.zsh" ]]; then + print -u2 "Missing pinned Zinit installation: $ZINIT_HOME" + return 1 fi source "${ZINIT_HOME}/zinit.zsh" +# Initialize completion before sourcing Oh My Zsh plugins that register compdefs +autoload -Uz compinit +compinit + # Completions path export FPATH="$HOME/.eza/completions/zsh:$FPATH" # Essential plugins (loaded immediately) +zinit ice ver"85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5" zinit light zsh-users/zsh-autosuggestions +zinit ice ver"24105b15714bfec37989ed5c5b6e60f572253019" zinit light Aloxaf/fzf-tab -# OMZ libs (only essential ones, loaded with turbo) -zinit wait lucid for \ - OMZL::git.zsh \ - OMZL::completion.zsh - -# OMZ plugins (turbo mode - deferred loading) -zinit wait lucid for \ - OMZP::git \ - OMZP::aws \ - OMZP::gh +# Build-pinned Oh My Zsh libraries and plugins +source "$ZSH/lib/git.zsh" +source "$ZSH/lib/completion.zsh" +source "$ZSH/plugins/git/git.plugin.zsh" +source "$ZSH/plugins/aws/aws.plugin.zsh" +source "$ZSH/plugins/gh/gh.plugin.zsh" # Syntax highlighting (load last, with turbo) -zinit wait lucid for \ - atinit"zicompinit; zicdreplay" \ +zinit wait lucid ver"3d574ccf48804b10dca52625df13da5edae7f553" for \ zdharma-continuum/fast-syntax-highlighting # Tool initializations (synchronous - needed for prompt) eval "$(starship init zsh)" eval "$(zoxide init zsh --cmd cd)" -# FZF (turbo) -zinit wait lucid for \ - atload"source <(fzf --zsh)" \ - zdharma-continuum/null +# FZF integration +if [[ -t 0 && -t 1 ]]; then + source <(fzf --zsh) +fi # Eza aliases (immediate, not deferred) alias l='eza -lah --icons --git --group-directories-first' @@ -95,4 +96,3 @@ zstyle ':fzf-tab:complete:(ls|l|ll|la|lt|eza):*' fzf-preview '[[ -d $realpath ]] zstyle ':fzf-tab:*' fzf-flags --color=fg:1,fg+:2 --bind=tab:accept zstyle ':fzf-tab:*' use-fzf-default-opts yes zstyle ':fzf-tab:*' switch-group '<' '>' - diff --git a/.zshrc.prod b/.zshrc.prod index acf736e..937f2d3 100644 --- a/.zshrc.prod +++ b/.zshrc.prod @@ -5,25 +5,28 @@ if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi -# Zinit installation +# Load the build-pinned Zinit installation ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git" -if [[ ! -d "$ZINIT_HOME" ]]; then - mkdir -p "$(dirname $ZINIT_HOME)" - git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME" +if [[ ! -r "$ZINIT_HOME/zinit.zsh" ]]; then + print -u2 "Missing pinned Zinit installation: $ZINIT_HOME" + return 1 fi source "${ZINIT_HOME}/zinit.zsh" +# Initialize completion before sourcing Oh My Zsh plugins that register compdefs +autoload -Uz compinit +compinit + # Minimal plugins for production (loaded immediately) +zinit ice ver"85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5" zinit light zsh-users/zsh-autosuggestions -# OMZ libs and git plugin -zinit wait lucid for \ - OMZL::git.zsh \ - OMZP::git +# Build-pinned Oh My Zsh library and git plugin +source "$ZSH/lib/git.zsh" +source "$ZSH/plugins/git/git.plugin.zsh" # Syntax highlighting (load last) -zinit wait lucid for \ - atinit"zicompinit; zicdreplay" \ +zinit wait lucid ver"3d574ccf48804b10dca52625df13da5edae7f553" for \ zdharma-continuum/fast-syntax-highlighting # Basic aliases diff --git a/AGENTS.md b/AGENTS.md index f92d92a..a41951b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,15 +6,15 @@ This repository builds the `ghcr.io/prvious/frankenphp` development and producti ## Build, Test, and Development Commands -- `docker build --build-arg VERSION=8.4 --target dev -t frankenphp:dev .` builds the local development image. -- `docker build --build-arg VERSION=8.4 --target prod -t frankenphp:prod .` builds the production image. -- `docker buildx bake --print` inspects the expanded build matrix; provide `PHP_VERSION`, `SHA`, and `LATEST` as CI does when required. +- `docker build --build-arg VERSION=8.5-trixie --target dev -t frankenphp:dev .` builds the local development image. +- `docker build --build-arg VERSION=8.5-trixie --target prod -t frankenphp:prod .` builds the production image. +- `docker buildx bake --print` inspects the default PHP 8.4/8.5 matrix; override `PHP_VERSION`, `SHA`, and `LATEST` to reproduce CI inputs. - `docker buildx bake` builds all configured variants and architectures. - `docker run --rm -v "$PWD/test.php:/app/test.php" frankenphp:dev php /app/test.php dev` validates extensions, binaries, and pnpm configuration. Substitute the production image and `production` for that target. ## Coding Style & Naming Conventions -Use four-space indentation in PHP, HCL, and workflow YAML. Keep PHP strictly typed and follow the existing PSR-12-style class and method layout; use `camelCase` methods and `UPPER_SNAKE_CASE` constants. In Dockerfile steps, group related packages, use uppercase build arguments, quote shell variables, and clean package caches in the same layer. Health-check filenames use the `healthcheck-` pattern and must remain executable POSIX shell scripts. +Use four-space indentation in PHP, HCL, and workflow YAML. Keep PHP strictly typed and follow the existing PSR-12-style class and method layout; use `camelCase` methods and `UPPER_SNAKE_CASE` constants. In Dockerfile steps, group related packages, use uppercase build arguments, quote shell variables, and clean package caches in the same layer. Health-check filenames use the `healthcheck-` pattern; the Dockerfile copies them into the image as executable POSIX shell scripts. ## Testing Guidelines diff --git a/Dockerfile b/Dockerfile index c2a278a..948524e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,86 +2,140 @@ ARG VERSION FROM dunglas/frankenphp:php${VERSION} AS frankenphp-base #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -# BUILDER STAGE: Download static binaries and tools +# BUILDER STAGE: Download pinned binaries and tools #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- FROM debian:bookworm-slim AS binaries-builder ARG TARGETARCH +ARG NODE_VERSION=26.5.1 +ARG PNPM_VERSION=11.18.0 +ARG SVGO_VERSION=4.0.2 RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - ca-certificates \ - jq \ - tar \ - gzip \ + ca-certificates \ + curl \ + gzip \ + libatomic1 \ + tar \ && rm -rf /var/lib/apt/lists/* WORKDIR /tmp/binaries -# Download minimal Node.js binary (just node, no npm) -RUN NODE_VERSION=26 \ - && if [ "$TARGETARCH" = "amd64" ]; then NODE_ARCH="x64"; else NODE_ARCH="arm64"; fi \ - && LATEST_NODE=$(curl -s https://nodejs.org/dist/latest-v${NODE_VERSION}.x/ | grep -oP "node-v${NODE_VERSION}\.\d+\.\d+" | head -1) \ - && curl -sL "https://nodejs.org/dist/latest-v${NODE_VERSION}.x/${LATEST_NODE}-linux-${NODE_ARCH}.tar.gz" | tar -xz \ +# Download Node.js without npm and verify the pinned archive +RUN case "${TARGETARCH}" in \ + amd64) NODE_ARCH="x64"; NODE_SHA256="2b07f09c218d473a26442bff5a90151f53f7b7c0a23bad244eda2c26303a2ba7" ;; \ + arm64) NODE_ARCH="arm64"; NODE_SHA256="21194bbf41c18d9ec277545c4d14cce8597d57a9d9f494c323d8121a25de33e8" ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && NODE_ARCHIVE="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" \ + && curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_ARCHIVE}" -o "${NODE_ARCHIVE}" \ + && echo "${NODE_SHA256} ${NODE_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${NODE_ARCHIVE}" \ && mkdir -p /tmp/binaries/node/bin \ - && mv ${LATEST_NODE}-linux-${NODE_ARCH}/bin/node /tmp/binaries/node/bin/ \ - && rm -rf ${LATEST_NODE}-linux-${NODE_ARCH} - -# Download the latest pnpm 11 release -RUN PNPM_VERSION=$(curl -fsSL https://registry.npmjs.org/@pnpm/exe | jq -r '.["dist-tags"]["latest-11"]') \ - && test -n "${PNPM_VERSION}" \ - && test "${PNPM_VERSION}" != "null" \ - && curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION="${PNPM_VERSION}" PNPM_HOME=/tmp/binaries/pnpm bash - - -# Install svgo using pnpm in builder -RUN export PATH="/tmp/binaries/node/bin:/tmp/binaries/pnpm:$PATH" \ + && mv "node-v${NODE_VERSION}-linux-${NODE_ARCH}/bin/node" /tmp/binaries/node/bin/ \ + && rm -rf "${NODE_ARCHIVE}" "node-v${NODE_VERSION}-linux-${NODE_ARCH}" + +# Download pnpm's architecture-independent package and verify the pinned archive +RUN PNPM_ARCHIVE="pnpm-${PNPM_VERSION}.tgz" \ + && curl -fsSL "https://registry.npmjs.org/pnpm/-/${PNPM_ARCHIVE}" -o "${PNPM_ARCHIVE}" \ + && echo "29c35ca8d2a287988fdee3e0f36e07d9b93783f567b579b7fd5b798a4563dd81 ${PNPM_ARCHIVE}" | sha256sum -c - \ + && mkdir -p /tmp/binaries/pnpm \ + && tar -xzf "${PNPM_ARCHIVE}" -C /tmp/binaries/pnpm --strip-components=1 \ + && ln -s bin/pnpm.mjs /tmp/binaries/pnpm/pnpm \ + && rm -f "${PNPM_ARCHIVE}" + +# Install a pinned svgo release using the verified Node.js and pnpm binaries +RUN mkdir -p /tmp/binaries/pnpm/bin /tmp/binaries/pnpm-global \ + && export PATH="/tmp/binaries/node/bin:/tmp/binaries/pnpm:/tmp/binaries/pnpm/bin:$PATH" \ && export PNPM_HOME=/tmp/binaries/pnpm \ - && mkdir -p /tmp/binaries/pnpm-global \ && pnpm config set store-dir /tmp/pnpm-store --global \ && pnpm config set global-dir /tmp/binaries/pnpm-global --global \ - && pnpm add -g svgo \ - && rm -rf /tmp/pnpm-store + && pnpm config set global-bin-dir /tmp/binaries/pnpm/bin --global \ + && pnpm add -g "svgo@${SVGO_VERSION}" -# Download development-only binaries +# Download and verify development-only binaries FROM binaries-builder AS dev-binaries-builder -# Download GitHub CLI -RUN GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | jq -r .tag_name | sed 's/^v//') \ - && if [ "$TARGETARCH" = "amd64" ]; then ARCH="amd64"; else ARCH="arm64"; fi \ - && curl -sL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${ARCH}.tar.gz" -o gh.tar.gz \ - && tar -xzf gh.tar.gz \ - && mv gh_${GH_VERSION}_linux_${ARCH}/bin/gh gh \ - && rm -rf gh*.tar.gz gh_* - -# Download eza -RUN EZA_VERSION=$(curl -s https://api.github.com/repos/eza-community/eza/releases/latest | jq -r .tag_name | sed 's/^v//') \ - && if [ "$TARGETARCH" = "amd64" ]; then EZA_ARCH="x86_64"; else EZA_ARCH="aarch64"; fi \ - && curl -sL "https://github.com/eza-community/eza/releases/download/v${EZA_VERSION}/eza_${EZA_ARCH}-unknown-linux-gnu.tar.gz" | tar -xz +ARG EZA_VERSION=0.23.5 +ARG FZF_VERSION=0.74.1 +ARG GH_VERSION=2.96.0 +ARG OPENCODE_VERSION=1.18.9 +ARG STARSHIP_VERSION=1.26.0 +ARG ZOXIDE_VERSION=0.10.0 + +RUN mkdir -p /tmp/binaries/dev \ + && case "${TARGETARCH}" in \ + amd64) \ + EZA_ARCH="x86_64"; EZA_SHA256="35c70c5c43c29108075e58b893234c67ef585f0b53a7eaf8e9e7d4eec9f339b4"; \ + FZF_ARCH="amd64"; FZF_SHA256="df53438be5f51e151bb4044d78fda72bdfe209e3ecd2baecae48e8dea370c81b"; \ + GH_ARCH="amd64"; GH_SHA256="83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60"; \ + OPENCODE_ARCH="x64"; OPENCODE_SHA256="a0fa4b7b8bdacbd013e79a5f69d4220d36b545cd3ea296ba765f3016fa501b5b"; \ + STARSHIP_ARCH="x86_64"; STARSHIP_SHA256="b7c232b0e8249d8e55a40beb79c5c43a7d370f3f9408bd215deb0170daeaadf3"; \ + ZOXIDE_ARCH="x86_64"; ZOXIDE_SHA256="2d93385b99f3e82cf2701609a1bffcad863fbeb75aa3fe7eb6be4d29be68b1ae" \ + ;; \ + arm64) \ + EZA_ARCH="aarch64"; EZA_SHA256="40b87ae8628aa2ff0f0d2dc24ab52f689631366385c3da630bae745671fd71ec"; \ + FZF_ARCH="arm64"; FZF_SHA256="f22204dd1a091d43e102268d062fd53b47133c8d8581671ee5eb225b75e31183"; \ + GH_ARCH="arm64"; GH_SHA256="06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909"; \ + OPENCODE_ARCH="arm64"; OPENCODE_SHA256="b16bd7593ea960a25d9c6849b3023bcd9b9244a6f51675341fd2052043b0670f"; \ + STARSHIP_ARCH="aarch64"; STARSHIP_SHA256="dc30189378d2f2e287384e8a692d3f95ad1df64cf0e8c36aa9201516028aed6b"; \ + ZOXIDE_ARCH="aarch64"; ZOXIDE_SHA256="f1f16c5d6298d63dee467eedea1cdcd8490e43e493bea43acd416dc9033ef641" \ + ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && EZA_ARCHIVE="eza_${EZA_ARCH}-unknown-linux-gnu.tar.gz" \ + && curl -fsSL "https://github.com/eza-community/eza/releases/download/v${EZA_VERSION}/${EZA_ARCHIVE}" -o "${EZA_ARCHIVE}" \ + && echo "${EZA_SHA256} ${EZA_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${EZA_ARCHIVE}" -C /tmp/binaries/dev \ + && FZF_ARCHIVE="fzf-${FZF_VERSION}-linux_${FZF_ARCH}.tar.gz" \ + && curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/${FZF_ARCHIVE}" -o "${FZF_ARCHIVE}" \ + && echo "${FZF_SHA256} ${FZF_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${FZF_ARCHIVE}" -C /tmp/binaries/dev \ + && GH_ARCHIVE="gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" \ + && curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/${GH_ARCHIVE}" -o "${GH_ARCHIVE}" \ + && echo "${GH_SHA256} ${GH_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${GH_ARCHIVE}" \ + && mv "gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh" /tmp/binaries/dev/gh \ + && OPENCODE_ARCHIVE="opencode-linux-${OPENCODE_ARCH}.tar.gz" \ + && curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/${OPENCODE_ARCHIVE}" -o "${OPENCODE_ARCHIVE}" \ + && echo "${OPENCODE_SHA256} ${OPENCODE_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${OPENCODE_ARCHIVE}" -C /tmp/binaries/dev \ + && STARSHIP_ARCHIVE="starship-${STARSHIP_ARCH}-unknown-linux-musl.tar.gz" \ + && curl -fsSL "https://github.com/starship/starship/releases/download/v${STARSHIP_VERSION}/${STARSHIP_ARCHIVE}" -o "${STARSHIP_ARCHIVE}" \ + && echo "${STARSHIP_SHA256} ${STARSHIP_ARCHIVE}" | sha256sum -c - \ + && tar -xzf "${STARSHIP_ARCHIVE}" -C /tmp/binaries/dev \ + && ZOXIDE_ARCHIVE="zoxide-${ZOXIDE_VERSION}-${ZOXIDE_ARCH}-unknown-linux-musl.tar.gz" \ + && curl -fsSL "https://github.com/ajeetdsouza/zoxide/releases/download/v${ZOXIDE_VERSION}/${ZOXIDE_ARCHIVE}" -o "${ZOXIDE_ARCHIVE}" \ + && echo "${ZOXIDE_SHA256} ${ZOXIDE_ARCHIVE}" | sha256sum -c - \ + && mkdir -p /tmp/zoxide \ + && tar -xzf "${ZOXIDE_ARCHIVE}" -C /tmp/zoxide \ + && mv /tmp/zoxide/zoxide /tmp/binaries/dev/zoxide \ + && chmod 755 /tmp/binaries/dev/* \ + && rm -rf ./*.tar.gz ./gh_* /tmp/zoxide #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -# BUILDER STAGE: Image optimization tools +# BUILDER STAGE: Pin the shell framework without executing a remote installer #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -FROM debian:bookworm-slim AS tools-builder +FROM debian:bookworm-slim AS shell-builder + +ARG ZINIT_COMMIT=429ab136312dfce68ad7d87a0ecb08c5063e7287 +ARG OH_MY_ZSH_COMMIT=7ea697fd8138550ddf7262456d412f0dcd1cbf84 RUN apt-get update && apt-get install -y --no-install-recommends \ - jpegoptim \ - optipng \ - pngquant \ - gifsicle \ - libavif-bin \ - ffmpeg \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* -# Copy binaries to a clean location -RUN mkdir -p /tmp/tools \ - && cp /usr/bin/jpegoptim /tmp/tools/ \ - && cp /usr/bin/optipng /tmp/tools/ \ - && cp /usr/bin/pngquant /tmp/tools/ \ - && cp /usr/bin/gifsicle /tmp/tools/ \ - && cp /usr/bin/avifenc /tmp/tools/ \ - && cp /usr/bin/ffmpeg /tmp/tools/ +RUN git clone --filter=blob:none https://github.com/zdharma-continuum/zinit.git /tmp/zinit \ + && git -C /tmp/zinit checkout "${ZINIT_COMMIT}" \ + && test "$(git -C /tmp/zinit rev-parse HEAD)" = "${ZINIT_COMMIT}" \ + && git clone --filter=blob:none https://github.com/ohmyzsh/ohmyzsh.git /tmp/oh-my-zsh \ + && git -C /tmp/oh-my-zsh checkout "${OH_MY_ZSH_COMMIT}" \ + && test "$(git -C /tmp/oh-my-zsh rev-parse HEAD)" = "${OH_MY_ZSH_COMMIT}" + +FROM composer:2.10.2@sha256:5946476338742b200bb9ff88f8be56275ddae4b3949c72305cb0dbf10cfcb760 AS composer-builder #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- # BASE STAGE: FrankenPHP base with minimal dependencies @@ -103,58 +157,40 @@ ENV SERVER_NAME=:80 ENV DEBIAN_FRONTEND=noninteractive ENV PNPM_HOME=/usr/local/share/pnpm ENV PATH=$PNPM_HOME:$PATH -ENV PNPM_STORE_DIR=/home/${USER}/.pnpm-store - -COPY ./.env /etc/profile.d/.env -COPY --chmod=755 ./usr/local/bin/* /usr/local/bin/ - -# Copy composer from official image -COPY --from=composer:2 --chmod=755 /usr/bin/composer /usr/bin/composer - -# Copy Node.js (just the binary) and pnpm from builder -COPY --from=binaries-builder /tmp/binaries/node /usr/local/node -COPY --from=binaries-builder /tmp/binaries/pnpm /usr/local/share/pnpm -COPY --from=binaries-builder /tmp/binaries/pnpm-global /usr/local/share/pnpm-global +ENV pnpm_config_store_dir=/home/${USER}/.pnpm-store -# Copy image optimization tools from builder -COPY --from=tools-builder --chmod=755 /tmp/tools/* /usr/local/bin/ - -# Add Node.js, pnpm, and global packages to PATH -ENV PATH=/usr/local/node/bin:/usr/local/share/pnpm:/usr/local/share/pnpm-global/bin:$PATH - -# Minimal system packages (no Node, no image tools, no gh/eza) +# Install shared runtime packages and image tools from the target Debian suite RUN apt-get update \ && apt-get install -y --no-install-recommends \ - gnupg \ - lsb-release \ ca-certificates \ - supervisor \ + curl \ + ffmpeg \ + gifsicle \ git \ + jpegoptim \ + libatomic1 \ + libavif-bin \ + optipng \ + pngquant \ + procps \ + supervisor \ unzip \ zsh \ - procps \ - # Required runtime libs for image tools - libjpeg62-turbo \ - libpng16-16 \ - libavif15 \ - libavcodec59 \ - libavformat59 \ - libavutil57 \ - libswscale6 \ - libswresample4 \ - && echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ - && curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/pgdg.gpg \ + && . /etc/os-release \ + && install -d -m 0755 /etc/apt/keyrings \ + && curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc -o /etc/apt/keyrings/postgresql.asc \ + && echo "0144068502a1eddd2a0280ede10ef607d1ec592ce819940991203941564e8e76 /etc/apt/keyrings/postgresql.asc" | sha256sum -c - \ + && echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ && apt-get update \ && apt-get install -y --no-install-recommends \ postgresql-client-17 \ default-mysql-client \ && echo 'source /etc/profile.d/.env' >> /etc/bash.bashrc \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Configure pnpm store directory -RUN export PATH="/usr/local/node/bin:/usr/local/share/pnpm:$PATH" \ - && pnpm config set store-dir /home/${USER}/.pnpm-store --global + && rm -rf \ + /usr/share/doc/* \ + /usr/share/man/* \ + /var/lib/apt/lists/* # PHP extensions RUN install-php-extensions \ @@ -172,29 +208,41 @@ RUN install-php-extensions \ exif \ ftp \ xml \ - pdo_sqlsrv \ - sqlsrv \ + pdo_sqlsrv-5.13.1 \ + sqlsrv-5.13.1 \ sockets \ && cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" +# Copy runtime configuration and verified artifacts after the expensive package layers +COPY ./.env /etc/profile.d/.env +COPY --chown=0:0 --chmod=755 ./usr/local/bin/* /usr/local/bin/ +COPY --from=composer-builder --chown=0:0 --chmod=755 /usr/bin/composer /usr/bin/composer +COPY --from=binaries-builder --chown=0:0 /tmp/binaries/node /usr/local/node +COPY --from=binaries-builder --chown=0:0 /tmp/binaries/pnpm /usr/local/share/pnpm +COPY --from=binaries-builder --chown=0:0 /tmp/binaries/pnpm-global /usr/local/share/pnpm-global +COPY --from=binaries-builder --chown=0:0 /tmp/pnpm-store /usr/local/pnpm-store + +# Add Node.js, pnpm, and global packages to PATH +ENV PATH=/usr/local/node/bin:/usr/local/share/pnpm:/usr/local/share/pnpm/bin:/usr/local/share/pnpm-global/bin:$PATH + # User creation and permissions -RUN groupadd --force -g ${WWWGROUP} ${USER} \ - && useradd -m --no-user-group -o -g ${WWWGROUP} -u ${WWWUSER} -s /bin/zsh ${USER} \ +RUN groupadd --force -g "${WWWGROUP}" "${USER}" \ + && useradd -m --no-user-group -o -g "${WWWGROUP}" -u "${WWWUSER}" -s /bin/zsh "${USER}" \ && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp \ - && mkdir -p /home/${USER}/.local/bin \ - && chown -R ${USER}:${USER} /home/${USER} /data/caddy /config/caddy - -# Final cleanup - remove unnecessary files -RUN rm -rf \ - /usr/share/doc/* \ - /usr/share/man/* \ - /usr/share/locale/* \ - /var/cache/apt/* \ - /var/lib/apt/lists/* \ - /tmp/* \ - /var/tmp/* \ - /root/.cache \ - /root/.npm + && mkdir -p \ + "/home/${USER}/.config/psysh" \ + "/home/${USER}/.local/bin" \ + "/home/${USER}/.pnpm-store" \ + /app \ + /config/opencode \ + /data/opencode \ + && chown -R "${USER}:${USER}" \ + "/home/${USER}" \ + /app \ + /data/caddy \ + /data/opencode \ + /config/caddy \ + /config/opencode #--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- # DEV STAGE @@ -202,9 +250,8 @@ RUN rm -rf \ FROM base AS dev -# Copy development binaries -COPY --from=dev-binaries-builder --chmod=755 /tmp/binaries/gh /usr/local/bin/gh -COPY --from=dev-binaries-builder --chmod=755 /tmp/binaries/eza /usr/local/bin/eza +# Copy verified development binaries +COPY --from=dev-binaries-builder --chown=0:0 --chmod=755 /tmp/binaries/dev/* /usr/local/bin/ # Minimal development packages RUN apt-get update \ @@ -217,27 +264,21 @@ RUN apt-get update \ # Development PHP extensions RUN install-php-extensions xdebug -# Switch to deploy user for user-level installations -USER ${USER} - -# Install starship and opencode -RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir=/home/${USER}/.local/bin \ - && curl -fsSL https://opencode.ai/install | bash - -# Copy zshrc and create user directories +# Copy the pinned shell frameworks and development configuration +COPY --from=shell-builder --chown=${USER}:${USER} /tmp/zinit /home/${USER}/.data/zinit/zinit.git +COPY --from=shell-builder --chown=${USER}:${USER} /tmp/oh-my-zsh /home/${USER}/.oh-my-zsh COPY --chown=${USER}:${USER} ./.zshrc /home/${USER}/.zshrc -# Install zinit, fzf, zoxide, and configure starship -RUN ZINIT_HOME="/home/${USER}/.local/share/zinit" NO_EDIT=1 NO_TUTORIAL=1 \ - bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)" \ - && git clone --depth 1 https://github.com/junegunn/fzf.git /home/${USER}/.fzf \ - && /home/${USER}/.fzf/install --all --no-update-rc \ - && curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash \ - && mkdir -p /home/${USER}/.config \ - && starship preset no-nerd-font -o /home/${USER}/.config/starship.toml \ - && zsh -i -c 'zinit self-update && exit 0' || true +# Switch to the runtime user and pre-download pinned shell plugins +USER ${USER} + +ENV PATH=/home/${USER}/.local/bin:/home/${USER}/.opencode/bin:$PATH -ENV PATH=/home/deploy/.local/bin:/home/deploy/.fzf/bin:$PATH +RUN starship preset no-nerd-font -o "/home/${USER}/.config/starship.toml" \ + && zsh -i -c 'exit 0' \ + && test "$(git -C "/home/${USER}/.data/zinit/plugins/zsh-users---zsh-autosuggestions" rev-parse HEAD)" = "85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5" \ + && test "$(git -C "/home/${USER}/.data/zinit/plugins/Aloxaf---fzf-tab" rev-parse HEAD)" = "24105b15714bfec37989ed5c5b6e60f572253019" \ + && test "$(git -C "/home/${USER}/.data/zinit/plugins/zdharma-continuum---fast-syntax-highlighting" rev-parse HEAD)" = "3d574ccf48804b10dca52625df13da5edae7f553" WORKDIR /app @@ -253,16 +294,15 @@ RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" # Copy production zshrc COPY --chown=${USER}:${USER} ./.zshrc.prod /home/${USER}/.zshrc -# Create user directories before switching user -RUN mkdir -p /home/${USER}/.local/share \ - && chown -R ${USER}:${USER} /home/${USER} +# Copy the pinned shell frameworks +COPY --from=shell-builder --chown=${USER}:${USER} /tmp/zinit /home/${USER}/.data/zinit/zinit.git +COPY --from=shell-builder --chown=${USER}:${USER} /tmp/oh-my-zsh /home/${USER}/.oh-my-zsh -# Switch to deploy user for user-level installations +# Switch to the runtime user and pre-download pinned shell plugins USER ${USER} -# Install zinit and pre-download plugins -RUN ZINIT_HOME="/home/${USER}/.local/share/zinit" NO_EDIT=1 NO_TUTORIAL=1 \ - bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)" \ - && zsh -i -c 'zinit self-update && exit 0' || true +RUN zsh -i -c 'exit 0' \ + && test "$(git -C "/home/${USER}/.data/zinit/plugins/zsh-users---zsh-autosuggestions" rev-parse HEAD)" = "85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5" \ + && test "$(git -C "/home/${USER}/.data/zinit/plugins/zdharma-continuum---fast-syntax-highlighting" rev-parse HEAD)" = "3d574ccf48804b10dca52625df13da5edae7f553" WORKDIR /app diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09fc0a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Clovis Muneza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 35d6a14..e242671 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,17 @@ Custom [FrankenPHP](https://frankenphp.dev) Docker images with batteries include ```bash # Development (includes xdebug, dev tools) -docker pull ghcr.io/prvious/frankenphp:php8.4-dev +docker pull ghcr.io/prvious/frankenphp:php8.5-dev # Production -docker pull ghcr.io/prvious/frankenphp:php8.4 +docker pull ghcr.io/prvious/frankenphp:php8.5 ``` ## What's Included ### Base (both dev & prod) -- **PHP Extensions**: mysqli, pdo_mysql, pgsql, pdo_pgsql, bcmath, gd, imagick, imap, pcntl, zip, intl, exif, xml, sqlsrv, pdo_sqlsrv, sockets + +- **PHP Extensions**: mysqli, pdo_mysql, pgsql, pdo_pgsql, bcmath, gd, imagick, imap, pcntl, zip, intl, exif, ftp, xml, sqlsrv, pdo_sqlsrv, sockets - **Node.js**: v26 - **Package Managers**: Composer, pnpm v11 - **Database Clients**: PostgreSQL 17, MySQL @@ -24,22 +25,23 @@ docker pull ghcr.io/prvious/frankenphp:php8.4 - **Shell**: Zsh with zinit ### Dev Only + - **PHP**: Xdebug -- **Tools**: GitHub CLI, htop, nano, fzf, zoxide, eza +- **Tools**: GitHub CLI, OpenCode, htop, nano, fzf, zoxide, eza - **Shell**: Starship prompt, syntax highlighting, autosuggestions, fzf-tab ## Available Tags | Tag | Description | |-----|-------------| +| `php8.5` | PHP 8.5 production | +| `php8.5-dev` | PHP 8.5 development | | `php8.4` | PHP 8.4 production | | `php8.4-dev` | PHP 8.4 development | -| `php8.3` | PHP 8.3 production | -| `php8.3-dev` | PHP 8.3 development | -| `latest` | Latest PHP production | -| `latest-dev` | Latest PHP development | +| `latest` | Latest supported PHP production | +| `latest-dev` | Latest supported PHP development | -All images are multi-arch: `linux/amd64` and `linux/arm64`. +The workflow discovers and publishes the two newest stable PHP minor lines. Unqualified tags use Debian Trixie. For Debian Bookworm, insert `-bookworm` before the optional `-dev` suffix: `php8.5-bookworm`, `php8.5-bookworm-dev`, `latest-bookworm`, or `latest-bookworm-dev`. All images support `linux/amd64` and `linux/arm64`. ## Usage with Laravel @@ -47,7 +49,7 @@ All images are multi-arch: `linux/amd64` and `linux/arm64`. # docker-compose.yml services: app: - image: ghcr.io/prvious/frankenphp:php8.4-dev + image: ghcr.io/prvious/frankenphp:php8.5-dev ports: - "80:80" - "443:443" @@ -84,13 +86,16 @@ healthcheck-schedule # Check scheduler ```bash # Build dev image -docker build --build-arg VERSION=8.4 --target dev -t frankenphp:dev . +docker build --build-arg VERSION=8.5-trixie --target dev -t frankenphp:dev . # Build prod image -docker build --build-arg VERSION=8.4 --target prod -t frankenphp:prod . +docker build --build-arg VERSION=8.5-trixie --target prod -t frankenphp:prod . -# Build all variants with bake +# Build the default PHP 8.4/8.5 matrix docker buildx bake + +# Build an explicit version set and label it with the current commit +PHP_VERSION=8.4.23,8.5.8 SHA="$(git rev-parse HEAD)" LATEST=8.5.8 docker buildx bake ``` ## Configuration @@ -103,9 +108,10 @@ The container runs as user `deploy` (UID 1000). Key paths: - **Caddy config**: `/config/caddy` Environment variables: + - `SERVER_NAME=:80` - Caddy server name - `TZ=UTC` - Timezone ## License -MIT +[MIT](LICENSE) diff --git a/docker-bake.hcl b/docker-bake.hcl index 37e8e37..71c6e84 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -3,15 +3,18 @@ variable "IMAGE_NAME" { } variable "PHP_VERSION" { - description = "Comma-separated list of PHP versions to build, e.g. '8.1.0,8.2.0,8.3.0'." + description = "Comma-separated list of exact PHP versions to build, e.g. '8.4.23,8.5.8'." + default = "8.4.23,8.5.8" } variable "SHA" { - description = "The git commit SHA to use for the build." + description = "Git commit SHA for OCI revision labels; defaults to 'local' for local builds." + default = "local" } variable "LATEST" { description = "The latest PHP version to use for tagging the 'latest' tag." + default = "8.5.8" } function "clean_tag" { @@ -33,7 +36,7 @@ function "tag" { // Latest tags for the LATEST PHP version pv == LATEST ? ["${IMAGE_NAME}:latest-${os}${variant == "dev" ? "-dev" : ""}"] : [], // Semver tags with OS and variant (only if version is not empty and semver returns results) - [for v in (semver(version)) : "${IMAGE_NAME}:php${v}-${os}${variant == "dev" ? "-dev" : ""}"], + [for v in (semver(version)) : "${IMAGE_NAME}:php${v}-${os}${variant == "dev" ? "-dev" : ""}" if v != split(".", version)[0] || php_version == LATEST], ]) ])) } @@ -60,7 +63,7 @@ function "php_version" { function "_php_version" { params = [v, m] - result = "${m.major}.${m.minor}" == "8.4" ? [v, "${m.major}.${m.minor}", "${m.major}"] : [v, "${m.major}.${m.minor}"] + result = [v, "${m.major}.${m.minor}"] } target "default" { @@ -90,7 +93,7 @@ target "default" { } labels = { - "org.opencontainers.image.description" = variant == "dev" ? "FrankenPHP Docker images (${os}) with supervisor, fnm(node version manager), pnpm, sqlsrv, Xdebug, and a few other goodies." : "FrankenPHP Docker images (${os}) with supervisor, fnm(node version manager), pnpm, sqlsrv, and a few other goodies." + "org.opencontainers.image.description" = variant == "dev" ? "FrankenPHP Docker images (${os}) with supervisor, Node.js 26, pnpm, sqlsrv, Xdebug, and development tools." : "FrankenPHP Docker images (${os}) with supervisor, Node.js 26, pnpm, sqlsrv, and image tools." "org.opencontainers.image.created" = "${timestamp()}" "org.opencontainers.image.version" = variant == "dev" ? "${clean_tag(php_version)}-${os}-dev" : "${clean_tag(php_version)}-${os}" "org.opencontainers.image.revision" = SHA diff --git a/test.php b/test.php index c19e131..d62bcda 100644 --- a/test.php +++ b/test.php @@ -7,8 +7,6 @@ const PRODUCTION_EXTENSIONS = ['mysqli', 'pdo_mysql', 'pgsql', 'pdo_pgsql', 'bcmath', 'gd', 'imagick', 'imap', 'pcntl', 'zip', 'intl', 'exif', 'ftp', 'xml', 'pdo_sqlsrv', 'sqlsrv', 'sockets']; -const PRODUCTION_ONLY_EXTENSIONS = []; - const DEV_ONLY_EXTENSIONS = ['xdebug']; const DEV_EXTENSIONS = [ @@ -16,17 +14,40 @@ ...DEV_ONLY_EXTENSIONS, ]; -const PRODUCTION_BINARIES = ['php', 'composer', 'node', 'pnpm', 'jpegoptim', 'optipng', 'pngquant', 'gifsicle', 'ffmpeg', 'svgo', 'avifenc', 'zsh']; - -const PRODUCTION_ONLY_BINARIES = []; +const PRODUCTION_BINARIES = ['php', 'composer', 'node', 'pnpm', 'psql', 'mysql', 'supervisord', 'jpegoptim', 'optipng', 'pngquant', 'gifsicle', 'ffmpeg', 'svgo', 'avifenc', 'zsh']; -const DEV_ONLY_BINARIES = ['gh', 'eza', 'htop', 'nano', 'fzf', 'zoxide']; +const DEV_ONLY_BINARIES = ['gh', 'eza', 'htop', 'nano', 'fzf', 'zoxide', 'starship', 'opencode']; const DEV_BINARIES = [ ...PRODUCTION_BINARIES, ...DEV_ONLY_BINARIES, ]; +const PRODUCTION_BINARY_CHECKS = [ + 'composer' => 'composer --version', + 'psql' => 'psql --version', + 'mysql' => 'mysql --version', + 'supervisord' => 'supervisord --version', + 'jpegoptim' => 'jpegoptim --version', + 'optipng' => 'optipng -version', + 'pngquant' => 'pngquant --version', + 'gifsicle' => 'gifsicle --version', + 'svgo' => 'svgo --version', + 'avifenc' => 'avifenc --version', + 'zsh' => 'zsh --version', +]; + +const DEV_ONLY_BINARY_CHECKS = [ + 'gh' => 'gh --version', + 'eza' => 'eza --version', + 'htop' => 'htop --version', + 'nano' => 'nano --version', + 'fzf' => 'fzf --version', + 'zoxide' => 'zoxide --version', + 'starship' => 'starship --version', + 'opencode' => 'opencode --version', +]; + class Colors { // Control codes @@ -72,14 +93,6 @@ public function printHeader(string $emoji, string $title): void echo "{$envText} {$header}\n"; } - public function warn(string $message): void - { - $warnText = paint("[WARN]", Colors::BOLD . Colors::BRIGHT_YELLOW); - $messageText = paint($message, Colors::MAGENTA); - - echo "{$warnText} {$messageText}\n"; - } - public function check(string $name, bool $passed, ?string $hint = null): void { if ($passed) { @@ -116,6 +129,16 @@ public function commandExists(string $command): bool return is_string($output) && trim($output) !== ''; } + public function commandSucceeds(string $command): bool + { + $output = []; + $exitCode = 1; + + @exec("{$command} >/dev/null 2>&1", $output, $exitCode); + + return $exitCode === 0; + } + public function finish(): never { if ($this->failures > 0) { @@ -167,15 +190,11 @@ function extensions(Runner $runner): void if ($runner->environment === 'production') { foreach (DEV_ONLY_EXTENSIONS as $extension) { - if (extension_loaded($extension)) { - $runner->warn("extension:{$extension} present (unexpected in production)"); - } - } - } else { - foreach (PRODUCTION_ONLY_EXTENSIONS as $extension) { - if (extension_loaded($extension)) { - $runner->warn("extension:{$extension} present (unexpected in dev)"); - } + $runner->check( + "forbidden extension:{$extension}", + !extension_loaded($extension), + 'must not be present in production' + ); } } } @@ -190,17 +209,25 @@ function binaries(Runner $runner): void $runner->check("binary:{$binary}", $runner->commandExists($binary)); } + $binaryChecks = $runner->environment === 'dev' + ? [...PRODUCTION_BINARY_CHECKS, ...DEV_ONLY_BINARY_CHECKS] + : PRODUCTION_BINARY_CHECKS; + + foreach ($binaryChecks as $binary => $command) { + $runner->check( + "binary executable:{$binary}", + $runner->commandSucceeds($command), + "command failed: {$command}" + ); + } + if ($runner->environment === 'production') { foreach (DEV_ONLY_BINARIES as $binary) { - if ($runner->commandExists($binary)) { - $runner->warn("binary:{$binary} present (unexpected in production)"); - } - } - } else { - foreach (PRODUCTION_ONLY_BINARIES as $binary) { - if ($runner->commandExists($binary)) { - $runner->warn("binary:{$binary} present (unexpected in dev)"); - } + $runner->check( + "forbidden binary:{$binary}", + !$runner->commandExists($binary), + 'must not be present in production' + ); } } } @@ -226,14 +253,31 @@ function sanity(Runner $runner): void 'expected ' . PNPM_MAJOR_VERSION . ".x, got {$pnpmVersion}" ); - if ($runner->environment === 'dev') { - $pnpmStorePath = trim(@shell_exec('pnpm store path 2>/dev/null') ?? ''); - $expectedPath = '/home/deploy/.pnpm-store'; - $runner->check( - 'pnpm store path', - str_starts_with($pnpmStorePath, $expectedPath), - "expected {$expectedPath}, got {$pnpmStorePath}" - ); + $home = getenv('HOME') ?: '/home/deploy'; + $pnpmStorePath = trim(@shell_exec('pnpm store path 2>/dev/null') ?? ''); + $expectedPath = "{$home}/.pnpm-store"; + $runner->check( + 'pnpm store path', + str_starts_with($pnpmStorePath, $expectedPath), + "expected {$expectedPath}, got {$pnpmStorePath}" + ); + + $psyshPath = "{$home}/.config/psysh"; + $runner->check( + 'psysh config directory', + is_dir($psyshPath) && is_writable($psyshPath), + "expected writable directory {$psyshPath}" + ); + + $appProbe = @tempnam('/app', 'frankenphp-test-'); + $runner->check( + 'app directory writable', + is_string($appProbe), + 'expected the runtime user to write to /app' + ); + + if (is_string($appProbe)) { + @unlink($appProbe); } } From d3040a40df9b0ced2fcda89ce0db2dc72d0c0292 Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Thu, 30 Jul 2026 02:09:42 -0400 Subject: [PATCH 3/4] fix: address review feedback --- .zshrc | 6 +++--- Dockerfile | 2 +- README.md | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.zshrc b/.zshrc index 58f7392..7d0bd13 100644 --- a/.zshrc +++ b/.zshrc @@ -16,13 +16,13 @@ if [[ ! -r "$ZINIT_HOME/zinit.zsh" ]]; then fi source "${ZINIT_HOME}/zinit.zsh" +# Completions path +export FPATH="$HOME/.eza/completions/zsh:$FPATH" + # Initialize completion before sourcing Oh My Zsh plugins that register compdefs autoload -Uz compinit compinit -# Completions path -export FPATH="$HOME/.eza/completions/zsh:$FPATH" - # Essential plugins (loaded immediately) zinit ice ver"85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5" zinit light zsh-users/zsh-autosuggestions diff --git a/Dockerfile b/Dockerfile index 948524e..239139b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -226,7 +226,7 @@ COPY --from=binaries-builder --chown=0:0 /tmp/pnpm-store /usr/local/pnpm-store ENV PATH=/usr/local/node/bin:/usr/local/share/pnpm:/usr/local/share/pnpm/bin:/usr/local/share/pnpm-global/bin:$PATH # User creation and permissions -RUN groupadd --force -g "${WWWGROUP}" "${USER}" \ +RUN groupadd --non-unique -g "${WWWGROUP}" "${USER}" \ && useradd -m --no-user-group -o -g "${WWWGROUP}" -u "${WWWUSER}" -s /bin/zsh "${USER}" \ && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp \ && mkdir -p \ diff --git a/README.md b/README.md index e242671..73d5823 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ docker pull ghcr.io/prvious/frankenphp:php8.5 ## Available Tags +The entries below are current examples of the dynamically selected PHP minor tags: + | Tag | Description | |-----|-------------| | `php8.5` | PHP 8.5 production | From 3a96f675bd75fc50a5a53a2d3eec3b74eddec42a Mon Sep 17 00:00:00 2001 From: Clovis Muneza Date: Thu, 30 Jul 2026 02:12:37 -0400 Subject: [PATCH 4/4] fix: remove stale completion path --- .zshrc | 3 --- README.md | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.zshrc b/.zshrc index 7d0bd13..fa86117 100644 --- a/.zshrc +++ b/.zshrc @@ -16,9 +16,6 @@ if [[ ! -r "$ZINIT_HOME/zinit.zsh" ]]; then fi source "${ZINIT_HOME}/zinit.zsh" -# Completions path -export FPATH="$HOME/.eza/completions/zsh:$FPATH" - # Initialize completion before sourcing Oh My Zsh plugins that register compdefs autoload -Uz compinit compinit diff --git a/README.md b/README.md index 73d5823..c57129d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ docker pull ghcr.io/prvious/frankenphp:php8.5 ## Available Tags -The entries below are current examples of the dynamically selected PHP minor tags: +The entries below illustrate the dynamically selected PHP minor tag format: | Tag | Description | |-----|-------------|