Skip to content

feat(aloft): first-party official docker images and supporting utilities#2004

Draft
iamdadmin wants to merge 4 commits intotempestphp:3.xfrom
iamdadmin:3.x-aloft
Draft

feat(aloft): first-party official docker images and supporting utilities#2004
iamdadmin wants to merge 4 commits intotempestphp:3.xfrom
iamdadmin:3.x-aloft

Conversation

@iamdadmin
Copy link
Contributor

Closes #1983

Introducing tempestphp/aloft

Or, at least, if you like the name! 'aloft' was my idea for this pseudo-package because it invokes imagery of things in the sky ... among the clouds ... and docker images are very 'cloudy'.

I'll write out some Q&A in another reply, probably tomorrow now, so you can understand my choices in the design and production of this image, including the answers to "why not alpine?", "why rootless?" and "why distroless?".

Initial draft PR includes

  • Refactored and relocated ServeCommand and associated router.php file to support ./tempest serve --aloft to pull and run the docker image to serve a dev server, alongside php dev server remaining primary
  • Custom Caddyfile based on FrankenPHP official with extra settings
  • Custom distroless and rootless Dockerfile based on GoogleContainerTools Debian Trixie, with a 'debug' version containing BusyBox
  • Custom dependencies support script which is injected in the build
  • Version pinning (i.e. frankenphp release-to-php release "1.11.2-8.5.3") as well as "latest" images

Things to be checked

  • composer.json probably isn't going to be wired correctly
  • tempest/aloft may need to be 'required' in some other composer.jsons depending how ServeCommand was called

Still to be done

  • Clarify CI/CD pipeline (may just be a Docs task)
  • Docs
  • Tests
  • Are any additional ENVs a good idea?
  • Where will the images be pushed to? Dockerhub is obvious option but ghcr and others, of course.

Future roadmap

  • When worker mode is ready, we'll need to iterate

@iamdadmin
Copy link
Contributor Author

  • CADDY_ prefix missing on some ENV in Caddyfile
  • Redundant COPY statement in Dockerfile removed
  • Added initial .dockerignore, it's mostly just copy-pasted from various so will need more thought but it's a starting point
  • moved ServeCommand back to packages/router/ because I see aloft perhaps not being something pushed by default otherwise, since the primary consumption will be from a public registry

@iamdadmin
Copy link
Contributor Author

While this is a working start, there's more to be done on making a fixed bill-of-materials, pinning and tracing versions in each build. At the moment we're just inheriting frankenphp and plopping it in a distroless image which works, but we can do better.

Alpine is out for this reason https://frankenphp.dev/docs/performance/#dont-use-musl

Don’t Use musl

The Alpine Linux variant of the official Docker images and the default binaries we provide are using the musl libc.

PHP is known to be slower when using this alternative C library instead of the traditional GNU library, especially when compiled in ZTS mode (thread-safe), which is required for FrankenPHP. The difference can be significant in a heavily threaded environment.

Also, some bugs only happen when using musl.

In production environments, we recommend using FrankenPHP linked against glibc, compiled with an appropriate optimization level.

This can be achieved by using the Debian Docker images, using our maintainers .deb, .rpm, or .apk packages, or by compiling FrankenPHP from sources.

For leaner or more secure containers, you may want to consider a hardened Debian image rather than Alpine.

So that brings us to options.

  1. Fork googlecontainertools distroless and maintain a PHP layer on top, there's an existing fork which does this but it's 4 years behind commits and also limited to PHP8.3 at the moment.

Downsides - php-zts packages aren't in Sury repo and will have to be built from source, Bazel really wants to deploy from repos, and it's a fairly big fork to maintain

  1. Custom mmdebstrap/debootstrap wrapper with declarative package manifests.

Downsides - The tool is lower-level but can make fixed-file systems from package lists. We'll have to do some work to wrap a file-pinning BOM around it, and maintain that. Again, Sury repo doesn't do php-zts so we'd have to build PHP from source. We'd have to make and maintain it, breaking fairly fresh ground.

  1. Use Ubuntu as base, using Chisel.

Downsides - Adopting Canonical may not be the wisest due to their not entirely FOSS-friendly choices at times (i.e. insisting on Snap instead of using Flatpak) and Chisel cannot work with non-Ubuntu repos. So we'd have to build php-zts and frankenphp from source and manage all those deps, which is a bit less ideal. That means no single BOM.

  1. NixOS container build. They already have php-zts and frankenphp, and it's updated fairly promptly. Builds are driven by a BOM so it is pinned and documented inherently.

Downsides - relying on community maintainers to bump the php-zts and frankenphp packages, although granted, we could potentially submit PRs if we needed to. Learning curve, I'm willing to dive in, but it is it's own whole thing.

My suggestion

Given all of the above, my feeling is that NixOS is probably actually the best way to go. We can inject our Caddyfile, and Nix can build an OCI-compatible image that we can just import to Docker or podman locally, or push to a registry.

We'd produce a flake file for each build, and then as with composer, a flake.lock file is generated with a fixed package list. If we get a security advisory in dependencies, we can bump individual files and re-build.

When frankenphp and/or php gets a bump, aka our 'primary' packages, we could either manage with git tags so we can inspect point in time, or we can literally duplicate the original flake file side-by-side in another folder or something.

…evelopment, fixed issue in stage-files under amd64
@iamdadmin
Copy link
Contributor Author

Okay, I've done a test with Nix, and there's some constraints that I think rule it out.

  1. it's comparatively much slower than the build I've already done which translates into real wasted CI/CD minutes.
  2. There's complications with Mac, under which it doesn't run and has to be QEMU'd or cross-compiled with MUSL with which are trying to avoid.
  3. Building a single-stage multi-arch image means cross-compiling under emulator and appears to be quite slow, so you have to build separate arch and combine them after.
  4. The images are nowhere near as small, which does defeat the point rather.

On to the next one!

Copy link
Member

@aidan-casey aidan-casey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, @iamdadmin! I apologize for only just now getting around to this, there's a lot to review here. 🙂

First off, I appreciate the work you have put into this. Thank you!

Generally, as I look at this PR, I'm less concerned with the substance of it than I am of the complexity of it.

For example, let's envision the ideal user experience as something like:

# Publishes the Docker stubs to the root of our project.
tempest aloft:install

# Starts a dev environment.
docker compose up -d

# Builds a production image.
docker build .

If we are publishing the stubs to the root of our project, is the average user going to be able to figure out how to add a PHP extension easily? Will they be able to debug a distroless container with little to no logs?

I'm thinking we might need to simplify this further, which might mean publishing a base build to the Docker registry and pulling from that for our stub. That would create an issue with distroless, however.

Dunno, I'm open to discussion on all this, just spitballing as I think about the dev experience.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd scratch this file, personally. Let's assume the end user with Docker knows how to run this with Docker and provide a sensible docker-compose default.


{$CADDY_EXTRA_CONFIG}

{$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTP_PORT:8000}, {$CADDY_SERVER_NAME:localhost}:{$CADDY_HTTPS_PORT:8443} {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can leave off ports here because they are already defined globally above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut is that this adds a complexity that we're not after here and we either need to extract this out to a separate build process and publish to a registry or simplify.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going back and forth on the distroless. I really like the idea in concept, but I have a few concerns:

  1. I think the ideal world is that these are stubs we are publishing into the project for someone to use/customize. These would be a lot of stubs to publish and I'm guessing the average user is going to get overwhelmed and/or not understand what is going on here.
  2. The resulting image is still pretty large. Knowing the issues with MUSL, I'd be curious if we could get the same/less size using RedHat UBI or something.
  3. I am just imagining a situation where someone submits a bug report about something with the Docker image and we want logs or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to track the env variables somewhere as there are quite a few at play here.


RUN curl -sSLf \
-o /usr/local/bin/install-php-extensions \
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur with previous comments about latest. We should probably pin things like this and verify the hash.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EXPOSE 8000
EXPOSE 8443
EXPOSE 8443/udp
EXPOSE 2019
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any real reason to expose this?

# 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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be half tempted to pin the version and reduce flexibility here.

@aidan-casey
Copy link
Member

Okay, I've done a test with Nix, and there's some constraints that I think rule it out.

  1. it's comparatively much slower than the build I've already done which translates into real wasted CI/CD minutes.
  2. There's complications with Mac, under which it doesn't run and has to be QEMU'd or cross-compiled with MUSL with which are trying to avoid.
  3. Building a single-stage multi-arch image means cross-compiling under emulator and appears to be quite slow, so you have to build separate arch and combine them after.
  4. The images are nowhere near as small, which does defeat the point rather.

On to the next one!

How about RedHat's UBI?

@iamdadmin
Copy link
Contributor Author

iamdadmin commented Feb 27, 2026

Hey, @iamdadmin! I apologize for only just now getting around to this, there's a lot to review here. 🙂

First off, I appreciate the work you have put into this. Thank you!

No worries at all, it helps me as much as it helps the community as I intend to use it myself. Equally, if I am not going in the direction wanted for Tempest, that's also fine, I'm not going to try to force it on you or anything and if what I want doesn't match I will just make my own package and use it :) So please, don't hold anything back or worry about wording, I won't be offended.

Generally, as I look at this PR, I'm less concerned with the substance of it than I am of the complexity of it.

I agree, this implementation is more fragile than I was aiming for.

For example, let's envision the ideal user experience as something like:

# Publishes the Docker stubs to the root of our project.
tempest aloft:install

# Starts a dev environment.
docker compose up -d

# Builds a production image.
docker build .

If we are publishing the stubs to the root of our project, is the average user going to be able to figure out how to add a PHP extension easily? Will they be able to debug a distroless container with little to no logs?

You're right that the PHP extensions issue is big. On the distroless side, the debug container has a shell, and we can figure out tweaks like logging configs, or even make sure all the logs go into /var/logs and we make that a container path so they're literally written in /logs/aloft/, or they can be viewed on the shell of course. We already set environment in .env so that can be utilised, or we can just turn all logs on in debug with a 7 day rotation/retention and all off in the distroless PROD image.

I think the goal to aim for is that both images are in the registry for those who want no changes, or extendable locally.

I'm thinking we might need to simplify this further, which might mean publishing a base build to the Docker registry and pulling from that for our stub. That would create an issue with distroless, however.

Dunno, I'm open to discussion on all this, just spitballing as I think about the dev experience.

I agree that this won't work in it's current iteration. My next test is:

  1. Writing a DTO class around a json package file, not entirely unlike composer.json in format since we're all used to it.
  2. Using the Dagger.io PHP-SDK to programmatically construct an OCI image based on these packages dependencies example.

What this means is that one will be able to do

./tempest aloft:build --with-phpExtensions "yml,other-extension,another-one,whatever" --compose

Internally the --compose flag will just run ./tempest aloft:compose which can be used independently with the registry images.

What this will do is automate the docker-build, locally, and with the --compose flag also build a docker-compose for those who need the full stack.

For the end user? One or two simple commands.

For us? We can use it in CI/CD to make SBOM-traceable images for each version pairing of php/frankenphp. We should probably envision some kind of version strategy for example for when Tempest itself moves past PHP8.5 to PHP8.6 - at this point maybe we archive all older versions from the registry itself, excepting the very last PHP8.5.x and frankenphp image, while retaining the SBOMs in the github repo in case people need to build them locally for their own needs, to keep storage costs managed etc.

Looking forward, we should probably also tie in a package security check and actively remove images with known defects, I'm not sure on that mechanism yet and suspect best to put that in a second release as this commit will just get huge otherwise.

I'm working on the Dagger workflow at the moment, given I have a working Dockerfile it hopefully won't take too much effort to convert it into Dagger commands, and test. Will keep you posted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create FrankenPHP Dockerfile

2 participants