The stack builds in three layers. A small image holds the compiled gRPC extension. A shared PHP base is built on top of it, and each of the 13 backend services builds on that base.
Read top to bottom: each layer is built from the one above it. The slow work (compiling PHP extensions) happens once in the upper layers. A service build only runs composer install and copies the code, which takes 10–30 seconds instead of 5–6 minutes.
/grpc/grpc.so (15.5 MB, FROM scratch). Compiling it takes 15–45 minutes, so it is built once and reused.php:8.3-cli-bookworm plus grpc, mongodb, gd, ldap, zip, bcmath, sockets, pcntl, opcache; RoadRunner rr; composer; supervisor; user app.git and unzip. Used only for the composer stage, so it never ships.composer install + app code in the builder stage, then the finished /app, .rr.yaml, supervisord, entrypoint and healthcheck copied into the runtime base.Two images sit outside this chain. gm/api-gateway is a Go service built from the Dockerfile in the api-gateway repo. gm/web is the Vite SPA, served by nginx from web/Dockerfile.
# on ubuntu@84.8.125.207
~/datamode/docker-stack/
├── base/Dockerfile # grpc-ext-build → ext-builder → runtime / build
├── services/<Repo>/Dockerfile # 13 services, generated (see below)
├── web/Dockerfile web/nginx.conf # front SPA
├── build.sh # builds everything as gm/*:local
├── docker-compose.yml # project governance-mode, web on :80
├── make-base-dockerfiles.py # docker-fixed/* → services/*
├── make-jenkinsfiles.py
├── jenkins/ # Jenkinsfile.php-base + one per service
├── restore-mongo.sh
├── WORKAROUNDS.md
└── DEPLOYMENT-STATE.txt
~/datamode/docker-khaled-original/ # Khaled's feature/docker-deployment files, untouched
~/datamode/docker-fixed/ # the same files with the grpc/PECL fixes
~/datamode/api-gateway/Dockerfile # gateway, from its own repo
/tmp/grpc-ext/Dockerfile # what built local/cybermode-grpc-ext
Don't edit the service Dockerfiles in place. They are generated from ~/datamode/docker-fixed/<Repo>/Dockerfile by make-base-dockerfiles.py, which swaps the extension-compiling stages for FROM gm/php-build / FROM gm/php-base. Edit the source in docker-fixed/ and regenerate.
Khaled's Dockerfiles default to GRPC_EXT_IMAGE=ghcr.io/pkksa/cybermode-grpc-ext, which is private and returns unauthorized from this host. So a local replacement was compiled once on 2026-10-21 from this file:
# /tmp/grpc-ext/Dockerfile → local/cybermode-grpc-ext:1.84.0-php8.3
# syntax=docker/dockerfile:1
FROM php:8.3-cli-bookworm AS build
WORKDIR /var/www/html
COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath exif gd intl ldap pcntl sockets zip redis grpc \
&& apt-get update && apt-get install -y --no-install-recommends git unzip \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /grpc && cp "$(php-config --extension-dir)/grpc.so" /grpc/grpc.so \
&& strip --strip-debug /grpc/grpc.so
FROM scratch
COPY --from=build /grpc /grpc
A reboot can delete it. You don't need it to rebuild, though: base/Dockerfile has its own grpc-ext-build stage that compiles grpc 1.84.0 the same way, retrying because pecl.php.net often answers HTTP 500. That stage runs whenever GRPC_EXT_IMAGE=grpc-ext-build is passed or the prebuilt image is missing.
The services' composer.lock files pin different MongoDB PHP libraries, and each library needs a matching ext-mongodb. The mongodb extension is downloaded from its GitHub release instead of PECL. Each service picks its flavour with ARG BASE_FLAVOUR.
| Flavour | ext-mongodb | Services |
|---|---|---|
| mongo1 | 1.21.9 | Business-Continuity, Workflow-Engine, Enterprise-Risk, Dashboard, Messaging, Task-Management, Import-Export, Reporting |
| mongo2 | 2.5.2 | User-Service, Notification, Regulator, Governance, Operations |
cd ~/datamode/docker-stack
./build.sh # base (if missing) + 13 services + api-gateway + web
./build.sh base # rebuild the 4 base images (PHP or extension change)
./build.sh user web # just these targets
FORCE=1 ./build.sh user # rebuild even if source + Dockerfiles are unchanged
GRPC_EXT_IMAGE=grpc-ext-build ./build.sh base # compile grpc.so instead of reusing the image
GITHUB_TOKEN=… ./build.sh # authenticated composer downloads (avoids GitHub 429)
docker compose -p governance-mode up -d
origin/pre-develop. Notification uses feature/docker-deployment because pre-develop has no gRPC code. A repo without pre-develop builds from its current checkout..build/ and removed afterwards.gm/* and local/* tags exist only on 84.8.125.207. Push gm/php-base and gm/php-build (both flavours) to registry.servers.advancedcontrols.tech first; jenkins/Jenkinsfile.php-base is the draft for that.GRPC_EXT_IMAGE=grpc-ext-build./tmp/grpc-ext/Dockerfile into the stack (for example base/grpc-ext/Dockerfile) if you want to keep a standalone copy.