Skip to content

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

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

feat(aloft): first-party official docker images and supporting utilities#2004
iamdadmin wants to merge 9 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'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
Contributor Author

Choose a reason for hiding this comment

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

Answering in turn

  1. We now have aloft:publish or they can be built from aloft:build and optionally pass —with-php-extensions to add more extensions. It’s still distroless. We just now have some vanity commands wrapped around it and it’s significantly simpler. I really went overboard on the docs too.
  2. I’ve gotten it down to 263MB versus FrankenPHP’s Alpine at ~180MB (which doesn’t have any of our extensions which takes up a chunk of that) versus FrankenPHP’s Debian at 622MB.
  3. There’s a debug version with busybox. We’d be able to provide instructions to people asking them to volume map specific configs or log files whether or not they use debug in theory. This is a bit of a ‘how long is a piece of string’ but we have options. I can create some internal troubleshooting guides that can live in a GitHub-only readme file if needed too.

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.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FrankenPHP would have to be built from source for this I think? It’s above 1024 so best to handle this in port mapping anyway.

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?

Copy link
Contributor Author

@iamdadmin iamdadmin Mar 4, 2026

Choose a reason for hiding this comment

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

Yes, if you want to route to the container from another within the docker network, in my case for example I have the docker image on a DMZ docker network with cloudflare tunnel, so I can reverse proxy over that, then it’s required.

When running as host with -p, then it won’t hurt to have it as anything on the network could in theory connect via host anyway. But we could in theory hide these behind ENV or ARG in some way just so they’re off unless needed on, if you have that preference?

if it helps, FrankenPHP exposes them by default also, so we're not breaking new ground.

# 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They exist as ARGs with a default so we can use it in CI/CD. It also gives our users the ability to choose their own versions and we can update the stubs from time to time also. Possibly have the CI/CD write a change to the file to keep the defaults updated when a new version is published, dependabot style but doing a replace in file from a bash script.

@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.

@iamdadmin
Copy link
Contributor Author

iamdadmin commented Mar 4, 2026

How about RedHat's UBI?

I am not sure that RedHat’s FOSS strategy works best for us, I used to use CentOS for example… while it still exists I’m out of the loop on its state and fitness for purpose. So it makes me a little wary that we base on that and they change their minds.

As I mentioned in another reply I managed to get file size down to within spitting distance of Alpine under Debian and it’s almost a default for docker containers to support Debian and Alpine (although not Alpine for us because of MUSL ofc).

It’s about ready for another check when you can Aidan, I will go back over the last comments above and be sure to reply explaining how I’ve dealt with them, some I haven’t fully addressed yet and will need another commit, and we will need to figure out CI/CD and a repo at some point too.

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