Docker images for RollDev — a powerful, flexible Docker development environment for modern web applications.
RollDev (CLI command roll) spins up local development stacks for Magento 1 & 2,
Laravel, Symfony, TYPO3, Shopware, WordPress, Akeneo, and generic PHP projects.
This repository (epartment/images) is the build pipeline that produces every
service image RollDev pulls at runtime.
There is no application code here — each top-level directory is one service image
(its Dockerfile plus supporting config), and each has a matching GitHub Actions
workflow that builds it for linux/amd64 + linux/arm64 and publishes it to the
GitHub Container Registry.
New here? Jump to How builds work for the big picture, or Adding or changing a version if you just need to get a new version building.
All images are published to ghcr.io/<owner>/roll/<name>:<version>. The canonical
upstream namespace is ghcr.io/epartment/roll/; forks publish under their own
owner automatically (the namespace is derived from github.repository_owner).
| Image | Versions |
|---|---|
php-fpm and its variants (php-fpm-debug, php-fpm-magento1, php-fpm-magento2, php-fpm-magento2-debug, php-fpm-wordpress) |
per PHP × Node combination — see the PHP-FPM image graph |
nginx |
1.16, 1.25, 1.26, 1.27 + newer (auto) |
varnish |
6.0 (LTS), 6.6, 7.0–7.7 + newer (auto) |
mariadb |
10.3, 10.4, 10.6, 11.3, 11.4 + newer (auto) |
mysql |
5.5–5.7 (legacy), 8.0–8.4, 9.0–9.2 + newer (auto) |
mongo |
5, 6, 7 + newer (auto) |
redis |
5.0, 6.0, 6.2, 7.0, 7.2 + newer (auto) |
dragonfly |
1.3–1.17 + newer (auto) |
elasticsearch |
7.17, 8.4, 8.6, 8.11–8.13 + newer (auto) |
opensearch |
2.9–2.13, 2.19, 3.1–3.3 + newer (auto) |
rabbitmq |
3.7–3.13, 4.1 + newer (auto) |
magepack |
2.3–2.11 |
mailhog, dnsmasq, startpage |
latest (single image, no version matrix) |
Image tags mirror the upstream version string. The version-matrix images do not
get a latest tag; the single-image utilities (mailhog, dnsmasq, startpage)
are published only as :latest. "+ newer (auto)" means the workflow
discovers new upstream releases and starts building them on its own — see
Adding or changing a version.
CI is the build system — there is no local build or test harness. Images are
built and published exclusively by .github/workflows/docker-image-*.yml. Each
service workflow triggers on:
workflow_dispatch— manual run from the Actions tab (most workflows accept aversionsinput to build a specific set on demand),- a daily
schedulecron (0 6 * * *) — so images rebuild against fresh upstream bases, pushtomastertouching that service's directory or its workflow file.
CI runs on master only: push triggers are limited to master, there is no
pull-request trigger, and a manual run started from another branch skips all jobs.
Feature branches and pull requests therefore start no builds.
Every workflow has the same two-stage shape:
discover ──▶ build (matrix, one job per version, fail-fast: false)
- A
discoverjob lists the upstream tags and decides which versions to build (see below). - A build matrix builds each version as an independent job. Because
fail-fast: falseis set everywhere, one version failing never stops the others — each version succeeds or fails on its own.
This pipeline is designed so a single flaky build can never block publishing of everything else:
- Independent matrix jobs. Every version is its own job with
fail-fast: false, so a transient failure (a slow download, a registry hiccup) is isolated to that one version. - Resilient multi-arch merge (php-fpm). php-fpm builds each architecture
separately and then merges them into a multi-arch tag. The merge jobs run
even if some builds failed (
if: !cancelled()), publish every version that has both architectures built in this run, and skip any version missing one (it keeps its previous image) instead of failing the whole run. Each skip is listed in the job's step summary. Previously a single flaky combination skipped the merge and blocked every php-fpm tag — that is fixed. - EOL combinations are non-fatal. End-of-life or experimental PHP/Node
combinations are marked
continue-on-error, so a failure there can't fail the run. - Network downloads retry. In-Dockerfile downloads (the PHP extension installer,
magerun, wp-cli,
composer require,npm install) usecurl --retry/ retry loops instead of a single attempt that fails the build on one bad packet.
.trigger at the repo root is a no-op file listed in the paths: of many
workflows. Changing its contents (e.g. the UUID) and pushing rebuilds every image
that watches it, without having to touch a Dockerfile.
- Build the image locally with
docker buildx build, passing the same build args the workflow uses, or - Run a workflow locally with
act. The jobs only run for themasterref, so on a feature branch pass an event file containing"ref": "refs/heads/master"(act -e event.json). Registry logins, pushes and manifest merges are skipped underact.
Versions are discovered automatically from the upstream registry, on top of a small, explicit policy you control. The policy has the same shape everywhere:
| Knob | Meaning |
|---|---|
floor (MIN) |
Ignore anything older than this. |
pin (PIN) |
Always build these. Also the fallback list if discovery is unavailable. |
deny (DENY) |
Never build these, even if upstream still publishes them. |
The selection rule is: build the pinned list, plus any upstream version newer than the highest pinned one, minus the deny-list, above the floor. So:
- A brand-new upstream release (newer than your newest pin) starts building on its own — you usually do nothing.
- Intentionally-skipped older minors are not back-filled (only versions newer than your newest pin are auto-added), so curated lists stay curated.
- To guarantee a specific version builds, add it to the pin. To stop building one, add it to the deny-list (or raise the floor).
…for a simple service (redis, mysql, mariadb, mongo, nginx, elasticsearch, opensearch, varnish, dragonfly, rabbitmq)
Edit the discover job at the top of that service's
.github/workflows/docker-image-<service>.yml. The knobs are plain shell variables:
MIN="5.0" # floor
PIN="5.0 6.0 6.2 7.0 7.2" # always build (+ fallback)
DENY="" # never buildYou can also build an ad-hoc set without editing anything: run the workflow from the
Actions tab and fill in the versions input (e.g. 7.2, 7.4).
php-fpm has a richer matrix (PHP × Node × architecture × variant), so its policy
lives in .github/workflows/php-matrix/constants.php
instead of inline YAML:
const PHP_MIN_VERSION = '7.3'; // floor
const PHP_DENY_VERSIONS = []; // never build
const PHP_PIN_VERSIONS = ['7.3', ..., '8.4']; // always build (+ fallback)
// …and the same trio for Node (NODE_MIN_VERSION / NODE_DENY_VERSIONS / NODE_PIN_VERSIONS)The same file also holds:
PHP_VERSIONS_OS_RELEASE/NODE_VERSIONS_OS_RELEASE— pin the Debian base (bullseye/bookworm) for a version (a version not listed defaults to bullseye).EOL_PHP_VERSIONS/EOL_NODE_VERSIONS/EXPERIMENTAL_PHP_VERSIONS— mark a version so a flaky build of it can't fail the run (it becomescontinue-on-error).NOT_STABLE_XDEBUG_PHP_VERSIONS— which versions get the non-stable Xdebug.
The three generator scripts (php-generator.php, node-generator.php,
full-generator.php) read this config and emit the build matrix as JSON. You can run
them locally to preview the matrix — no CI needed:
php .github/workflows/php-matrix/full-generator.php | sed 's/^matrix=//' | jqTo build a specific set on demand, run the Docker Image PHP-FPM workflow from the
Actions tab and fill in the php_versions / node_versions inputs.
php-fpm is not a single image — it's a chain of layered images, each built FROM
the previous one. Every layer shares the single build context php-fpm/context/
but uses a different Dockerfile, and the build passes the previous stage's
published image in via the ENV_SOURCE_IMAGE + PHP_VERSION build-args so the
layers stack:
php (official)
└─ php-fpm/Dockerfile → roll/php-fpm (extensions, tools, composer, oh-my-zsh)
└─ php-fpm/node/Dockerfile → roll/php-fpm (+ Node / yarn / phantomjs, re-tagged same repo)
├─ php-fpm/xdebug3/Dockerfile → roll/php-fpm-debug
├─ php-fpm/magento1/Dockerfile → roll/php-fpm-magento1
├─ php-fpm/magento2/Dockerfile → roll/php-fpm-magento2 (+ magerun, mydumper, cache-clean)
│ └─ php-fpm/magento2/xdebug3/Dockerfile → roll/php-fpm-magento2-debug
└─ php-fpm/wordpress/Dockerfile → roll/php-fpm-wordpress
Version-conditional steps inside these Dockerfiles use shell sort -g / sort -V
comparisons against ${PHP_VERSION} (e.g. "install imagick only if PHP > 7.2 &&
< 8.3").
Each layer is built per-architecture separately on native amd64 and arm64
runners (no slow QEMU emulation). Each build pushes a per-arch tag,
<version>-arch-<x86|arm64> (e.g. php-fpm-magento2:8.3-node20-arch-arm64). The tag
carries the label org.epartment.roll.ci-run-id set to the id of the run that built it.
Separate merge-* jobs then run .github/scripts/merge-per-arch-tags.sh for every
version in the build matrix. For each version the script:
- reads each architecture's per-arch tag with
docker buildx imagetools inspect; - if both tags carry this run's id, runs
docker buildx imagetools createto assemble the multi-arch manifest:<version>from their digests; - otherwise skips the version, which keeps its previous image (see Robustness).
The run-id check means a failed leg can never pair an old image with a new one. The
-arch- tags are internal hand-off tags; use the plain :<version> tags. Because runs
share these tags, the workflow queues runs (concurrency, no cancelling). A push during
the daily build waits for it to finish rather than overwriting its tags halfway.
The hand-off deliberately does not use workflow artifacts. A run lists only about its first 1000 artifacts, and this workflow produces well over 1000 builds. Every digest past that cutoff was silently invisible to the merge jobs.
When adding a new layered image you must add both a build job (pushing labelled
per-arch tags) and a matching merge-* job (assembling the manifest) — they go together.
mirror-base-images.yml copies the
upstream bases php-fpm builds FROM (php, node, composer, phantomjs) into
ghcr.io/<owner>/base-images/*. This insulates the heavy php-fpm matrix from Docker
Hub pull-rate limits — a common cause of transient failures.
It is optional: php-fpm's discover job uses the mirror when it exists and
transparently falls back to Docker Hub when it doesn't. Run it once manually to
populate the mirror; a weekly cron keeps it fresh. (Currently only the php base layer
consumes the mirror; the node/composer/phantomjs layers still pull from Docker Hub —
wiring those through the mirror is a possible follow-up.)
php-fpm/context/docker-entrypoint runs on container start (not at build time) and
is RollDev-specific. It:
- recreates the
www-datauser with the host'sUSER_ID/GROUP_ID(for macOS file-permission parity), - renders
05-additions.inifrom a template viaenvsubst(Mailhog host/port), - trusts a mounted RollDev root CA certificate,
- proxies the host SSH agent socket into the container with
socat, - starts
cron, - selects the active Composer version from
COMPOSER_VERSION(1/2/2.2; three binaries are baked in ascomposer1/composer2/composer2lts), chowns the directories listed inCHOWN_DIR_LIST, and- installs any extra PHP extensions named in
ADD_PHP_EXT.
Keep these environment-variable contracts intact when editing the entrypoint — RollDev relies on them.
php-fpm/ base + layered PHP-FPM images and the shared build context/
context/ shared build context (entrypoint, php.d, profile.d)
node/ xdebug3/ magento1/ magento2/ wordpress/ layer Dockerfiles
nginx/ nginx image + per-framework vhost templates (available.d/)
varnish/ mariadb/ mysql/ mongo/ redis/ dragonfly/ ... one directory per service
.github/workflows/ one docker-image-*.yml per service + mirror-base-images.yml
.github/workflows/php-matrix/ php-fpm version policy (constants.php) + matrix generators
.trigger no-op file; bump to force a rebuild
See individual upstream projects for their licenses. RollDev itself is released under the MIT License.