GovernanceMode · Docker build

How the GovernanceMode images are built

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.

host ubuntu@84.8.125.207 stack dir ~/datamode/docker-stack compose project governance-mode as of 2026-10-26

The image chain

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.

local/cybermode-grpc-ext:1.84.0-php8.3
Holds only the compiled gRPC extension, at /grpc/grpc.so (15.5 MB, FROM scratch). Compiling it takes 15–45 minutes, so it is built once and reused.
Dockerfile: /tmp/grpc-ext/Dockerfile (temporary, see below)
↓ copied in as GRPC_EXT_IMAGE
gm/php-base:mongo1 · :mongo2
Runtime base: php:8.3-cli-bookworm plus grpc, mongodb, gd, ldap, zip, bcmath, sockets, pcntl, opcache; RoadRunner rr; composer; supervisor; user app.
base/Dockerfile · target runtime
gm/php-build:mongo1 · :mongo2
The same image plus git and unzip. Used only for the composer stage, so it never ships.
base/Dockerfile · target build
↓ FROM gm/php-build (builder) and gm/php-base (runtime)
gm/<service>:local × 13
Two stages per service: composer install + app code in the builder stage, then the finished /app, .rr.yaml, supervisord, entrypoint and healthcheck copied into the runtime base.
services/<Repo>/Dockerfile

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.

Where the files are

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

The grpc.so image

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
That file lives in /tmp

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.

Why there are two flavours

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.

Flavourext-mongodbServices
mongo11.21.9Business-Continuity, Workflow-Engine, Enterprise-Risk, Dashboard, Messaging, Task-Management, Import-Export, Reporting
mongo22.5.2User-Service, Notification, Regulator, Governance, Operations

Building

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

Before moving this to Jenkins or another host