Contributing
Thanks for helping out. This page covers how to build an image locally, test it, and run the checks CI runs, so your pull request passes on the first try. Read it before you open one.
Fix the docs
For a small fix, click "Edit this page" at the bottom of the table of contents. For bigger changes, the whole site lives in docs/. docs/README.md explains how to run it locally. Follow docs/AGENTS.md for style.
What you need
- Docker with
buildx yq, which the build and test scripts use to read the version config- Bash. The build scripts in
scripts/work on macOS, Linux, and WSL2.
Image source lives in src/. Each variation has one Dockerfile in src/variations/, and the other folders hold shared scripts and config. Scripts in src/ run inside containers on Debian and Alpine. They must be POSIX sh with no Bash syntax.
Build an image
scripts/dev.sh builds one image. Run it with no options to see them all.
bash scripts/dev.sh --variation fpm-nginx --version 8.5 --os trixie
This builds serversideup/php:8.5-fpm-nginx-trixie on your machine.
Local builds start from the official PHP image on Docker Hub, which is fast. CI compiles PHP from source instead (see How images are built). To build exactly what CI builds, add --build-php-base with a full patch version, such as --version 8.5.12. Compiling PHP takes several minutes. It needs Docker's default builder and doesn't work with --push.
Test it
scripts/test-image.sh runs the same image tests CI runs before it publishes anything:
bash scripts/test-image.sh serversideup/php:8.5-fpm-nginx-trixie
# Run one feature area
TEST_ONLY=web-server bash scripts/test-image.sh serversideup/php:8.5-fpm-nginx-trixie
Every test runs even after one fails. The summary lists each failure with its output. To check a startup change by hand, run the image with a shell:
docker run --rm -it serversideup/php:8.5-fpm-nginx-trixie sh
Test entrypoint changes on both a Debian and an Alpine build.
Add a test for new behavior
Tests live in scripts/tests/image/, one file per feature area (php.sh, entrypoint.sh, web-server.sh, ssl.sh, and so on). Add a test_ function to the file for your area. A new area gets a new file. Never add checks to scripts/test-image.sh itself.
requires healthcheck web_server
setup() {
start_container
}
test_serves_php() {
expect_body / "serversideup-php-ok:${php_version}"
}
requiresskips the file, or one test when called inside it, unless the image has the feature (healthcheck,web_server,https,frankenphp,fpm). Features come from the image's environment variables, not its name, so tests stay generic across variations.setupstarts one container that every test in the file shares.- A test fails when a command fails or it calls
fail "message".skip "reason"skips it. - Helpers (
start_container,run_php,expect_body) and image facts (php_version,http_port,container) are inscripts/tests/image/lib.sh. - Mounted files are in
scripts/tests/image/fixtures/:public/is the web root,laravel/is a stand-in app whoseartisanrecords each command it receives, andentrypoint.d/holds sample startup scripts.
Run the linters
CI runs the same checks on every pull request before it builds any image. Run them all from the repository root:
bash scripts/lint.sh
Each check runs in Docker at the version CI uses, and the summary lists every failure. To run one, name it:
| Check | What it covers |
|---|---|
actionlint | GitHub Actions workflow syntax |
zizmor | Workflow security, such as unpinned actions and risky triggers |
shellcheck | Shell scripts in scripts/ and src/ |
hadolint | Dockerfiles |
scripts | The build script tests, and that the environment variable and command docs match src/ |
bash scripts/lint.sh shellcheck hadolint
- Document every environment variable and command in the same change that adds or changes it: variables in
docs/content/data/env-vars.yml, commands in/usr/local/binindocs/content/data/commands.yml. The check scripts fail when one is missing, or when a default or variation list doesn't match the Dockerfiles. Keep both files sorted A–Z. - Pin every GitHub Action to a full commit SHA with its version in a comment, such as
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1. zizmor fails on unpinned actions. - Record lint exceptions with a reason: zizmor in
.github/zizmor.yml, hadolint in.hadolint.yaml. - Add an assertion to
scripts/tests/run.shwhen you change a build script it covers, such asscripts/build-summary.sh. CI runs it with an olderjqandyqthan most laptops have, so a green CI run is the one that counts. - New linters go in
scripts/lint.shand as a job in.github/workflows/service_lint.yml.
Open a pull request
- Build the variations your change touches and run
scripts/test-image.shagainst each. - Add a test in
scripts/tests/image/for new behavior. - Run the linters above.
- Update the docs in the same pull request if your change affects users.
A pull request that touches src/, scripts/, or the image workflows builds and tests every image for amd64 and arm64. A fork's images are built and tested on amd64 but never published. A maintainer can publish them to serversideup/php-dev for you to try. How images are built explains each step.
Other ways to help
- Answer questions in GitHub Discussions and on Discord.
- Pick up an issue labeled "Help Wanted".
- Request a feature and tell us what you need.
- Run performance tests and share your results.