Getting Started

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.

Terminal
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:

Terminal
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:

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

scripts/tests/image/web-server.sh
requires healthcheck web_server

setup() {
    start_container
}

test_serves_php() {
    expect_body / "serversideup-php-ok:${php_version}"
}
  • requires skips 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.
  • setup starts 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 in scripts/tests/image/lib.sh.
  • Mounted files are in scripts/tests/image/fixtures/: public/ is the web root, laravel/ is a stand-in app whose artisan records each command it receives, and entrypoint.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:

Terminal
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:

CheckWhat it covers
actionlintGitHub Actions workflow syntax
zizmorWorkflow security, such as unpinned actions and risky triggers
shellcheckShell scripts in scripts/ and src/
hadolintDockerfiles
scriptsThe build script tests, and that the environment variable and command docs match src/
Terminal
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/bin in docs/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.sh when you change a build script it covers, such as scripts/build-summary.sh. CI runs it with an older jq and yq than most laptops have, so a green CI run is the one that counts.
  • New linters go in scripts/lint.sh and as a job in .github/workflows/service_lint.yml.

Open a pull request

  1. Build the variations your change touches and run scripts/test-image.sh against each.
  2. Add a test in scripts/tests/image/ for new behavior.
  3. Run the linters above.
  4. 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