Contributing
Thanks for your interest in contributing to this project! Please use read this entire guide before submitting a pull request.
Improve the docs
Improving the docs is very easy. If you find a simple mistake, look at the bottom of the Table Of Contents section and click "Edit this page".
If you'd like to contribute bigger documentation changes, take a look at the /docs directory. Our entire site is available in that directory and you can see the process to easily install this on your machine by reading /docs/README.md.
Project dependencies
You must have these installed on your system.
- Docker (container engine): https://www.docker.com/products/docker-desktop
How things work
- All files are stored in the
/srcfolder - GitHub Actions will automatically build and deploy the images
Running things locally
To run a build, simply run ./scripts/dev.sh (with Docker Desktop running). This will show you a help menu with all the available options.
Example: Build a FPM-NGINX image running PHP 8.2.12 on Debian Bookworm
bash scripts/dev.sh --variation fpm-nginx --version 8.2.12 --os bookworm
This will build serversideup/php:8.2.12-fpm-nginx-bookworm locally on your machine for testing and inspection.
Published Beta Images
We also have beta images that are published to our Docker Hub and GitHub Packages repositories.
| ⚙️ Variation | 🚀 Version |
|---|---|
| cli | Debian Based Alpine Based |
| fpm | Debian Based Alpine Based |
| fpm-apache | Debian Based |
| fpm-nginx | Debian Based Alpine Based |
| frankenphp | Debian Based Alpine Based |
Running a test web server:
Sometimes you need to just run a test web server locally to see if your changes work. Below is a good example on how to quickly do this.
docker run --rm -v $(pwd):/var/www/html -p 80:8080 -p 443:8443 serversideup/php:8.4-fpm-nginx
How PHP Versions are selected for distribution
We use the official PHP versions as our base image. To identify which versions should be built, we use a file called scripts/conf/php-versions-base-config.yml to explicitly select what versions should be built and any special rules/settings for each version (like base OS, default versions, etc).
We then use a scripts/get-php-versions.sh script to download the latest active releases from PHP and merge them into a final file called scripts/conf/php-versions.yml.
The php-versions.yml file will include all final versions for tagging and building.
We generate our tags with a file called scripts/assemble-docker-tags.sh which handles all the advanced logic of compiling our tags together.
All the scripts above are designed to run locally and in GitHub Actions. Feel free to execute these scripts to see the help menus and how they work.
GitHub Actions
We use GitHub Actions exclusively to publish all of our releases. If the image exists from DockerHub or GitHub Packages, it will never be published from a local machine.
See .github/workflows/action_publish-images-beta.yml for an example of how we publish our beta images.
Linting
Every pull request starts with a lint group that runs before any image is built: actionlint for the workflows, ShellCheck for the scripts, and hadolint for the Dockerfiles, plus fixture tests for the helper scripts in scripts/ that CI relies on. Run the same checks locally before you push:
docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color
docker run --rm -v "$PWD:/repo" -w /repo koalaman/shellcheck:stable --severity=warning \
scripts/*.sh scripts/tests/*.sh src/common/usr/local/bin/* src/s6/usr/local/bin/* $(find src -path '*entrypoint.d*' -name '*.sh')
docker run --rm -v "$PWD:/repo" -w /repo hadolint/hadolint:latest hadolint src/variations/*/Dockerfile
bash scripts/tests/run.sh
The script tests live in one file, scripts/tests/run.sh, as plain assertions over a small set of sample images. CI runs the same file on a GitHub runner, whose jq and yq versions can differ from your machine, so a green run there is the one that counts.
The Depot runner labels we use are listed in .github/actionlint.yaml so actionlint recognizes them. New linters belong in .github/workflows/service_lint.yml.
What happens when you open a pull request
Every pull request that touches src/, scripts/, or the workflows builds the full image matrix for both amd64 and arm64. Jobs are grouped by variation, so you can see at a glance whether cli, fpm, fpm-apache, fpm-nginx, and frankenphp all build.
- Pull requests from forks are built on isolated builders and never leave them. Each build job also loads its amd64 image and runs the image tests against it, so a green check means your change builds everywhere and passes the tests on amd64.
- Pull requests from this repository go through the same three phases as a release: build, test, publish. Images are published to
serversideup/php-devwith your PR number as the tag prefix (for exampleserversideup/php-dev:699-8.4-fpm-nginx-alpine) only after the image tests pass. A comment on the PR shows the status and the pull commands. - Every run ends with a summary table listing each image with its compressed size per architecture, so a size regression is visible before merge.
- Nothing is published until it passes the image tests. Every build is saved to a private registry first. Every image is pulled from there on native amd64 and arm64 runners and checked: it starts, runs unprivileged, reports the right PHP version, loads the default extensions, becomes healthy, and serves a PHP file through its web server. Only then is the exact same image promoted to Docker Hub, and the published digest is checked against the tested one, so what you pull is what was tested. If any image fails to build or fails its test, nothing from that run is published. You can run the same check locally with
bash scripts/test-image.sh serversideup/php-dev:<tag>. - Transient failures heal themselves. If a few jobs fail because a registry or mirror hiccupped, CI re-runs just those jobs once. If a release or weekly rebuild is still red after that, CI opens an issue for the maintainers with the failed jobs and their errors.
- Maintainers can publish a fork's images for testing by running the "Docker Publish (PR Images)" workflow manually with the PR number.
NGINX Versions
We use the official NGINX repos to install the latest version of NGINX for each OS. The version to install is set by a build argument, which is loaded from the scripts/conf/php-versions-base-config.yml file.
To view the current NGINX versions, run the following command:
View NGINX versions
./scripts/get-nginx-versions.sh
This script will look at the official NGINX repos to find the latest version of NGINX for each OS. If you want to update the version, you can run the script with the --write flag.
Update NGINX versions
./scripts/get-nginx-versions.sh --write
NGINX repository key verification
- Debian (APT): We import the official NGINX GPG key from
https://nginx.org/keys/nginx_signing.keyand verify it against a pinned fingerprint via theSIGNING_FINGERPRINTbuild arg. - Alpine (APK): APK uses a raw RSA public key (
nginx_signing.rsa.pub). We verify this key by pinning the SHA‑256 of the DER‑encoded public key via theSIGNING_ALPINE_RSA_PUB_SHA256build arg. You can provide multiple comma‑separated hashes to support key rotation.
Compute the Alpine key hash when updating:
curl -sS https://nginx.org/keys/nginx_signing.rsa.pub -o /tmp/nginx_signing.rsa.pub
# macOS
openssl rsa -pubin -in /tmp/nginx_signing.rsa.pub -outform DER 2>/dev/null | shasum -a 256 | awk '{print $1}'
# Linux
openssl rsa -pubin -in /tmp/nginx_signing.rsa.pub -outform DER 2>/dev/null | sha256sum | awk '{print $1}'
Build with the new hash (optionally include the old hash during rotation)
docker build \
--build-arg SIGNING_ALPINE_RSA_PUB_SHA256="<new-hash>,<old-hash>" \
-f src/variations/fpm-nginx/Dockerfile .
Reference: Installing NGINX Open Source → Alpine packages.
Why allow multiple hashes? This is optional, but useful during a short rotation window:
- Ensure CI builds across branches/runners succeed while the upstream key change propagates.
- Avoid flakes from CDN/caching delays where some environments still see the old key.
- Let you pre-stage the new value before the official switch, then remove the old afterwards.
If you control all builds centrally and can update quickly, pass a single hash.
Helping out
If you're really eager to help out, here are a few places to get started:
- Help answer questions on our GitHub Discussions and our Discord
- Chime in on issues labeled "Help Wanted"
- Open a feature request and tell us how we can improve
- Run performance tests and share your results
- Say good things and tag us on X (formerly Twitter): @serversideup, @danpastori, @jaydrogers