YourBOX Verification Baseline
Purpose
Section titled “Purpose”The first work item on YourBOX is making the suite runnable and the image trustworthy.
The 2020 setup pinned three different PHP versions across mise.toml, the Dockerfile and
the CI test job, shipped an Apache mod_php image with a baked secrets file, and never
passed hadolint or Trivy. None of the modernization ADRs can self verify until this
baseline is in place.
Toolchain Alignment
Section titled “Toolchain Alignment”YourBOX runs on PHP 8.5. The version is pinned in three coordinated places:
mise.tomlselects PHP 8.5 throughubi:adwinying/php, plus composer, ffmpeg, yamllint, hadolint, trivy, sops, age, and pre-commit.- The runtime Dockerfile uses
docker.io/library/php:8.5-fpm-alpinewith its current digest pinned per ADR-0009. - The GitLab CI test job uses
docker.io/library/php:8.5-cli-alpinewith its current digest pinned the same way.
The pipe operator already used in YourBOX/src/inc/functions.php requires PHP 8.4 or
above. PHP 8.5 keeps the alignment forward compatible.
Multi Stage Container Image
Section titled “Multi Stage Container Image”The runtime image is a two stage build: a composer:2 stage installs production
dependencies for both YourBOX/composer.json (legacy tree) and
app/composer.json (Symfony 8 skeleton from plan 003) into two side-by-side
vendor directories, and a php:8.5-fpm-alpine runtime stage carries nginx,
php-fpm, ffmpeg, tini, both code trees and the pre-installed vendors. nginx
roots itself at app/public/ so the Symfony front controller handles every
request and the strangler bundle includes legacy PHP files internally.
FROM docker.io/library/composer:2@sha256:<digest> AS depsWORKDIR /appCOPY YourBOX/composer.json YourBOX/composer.lock ./yourbox/COPY app/composer.json app/composer.lock ./symfony/RUN --mount=type=cache,target=/tmp/composer-cache \ COMPOSER_CACHE_DIR=/tmp/composer-cache \ composer install --working-dir=/app/yourbox --no-dev --no-interaction --no-progress --prefer-dist --no-scripts \ && composer install --working-dir=/app/symfony --no-dev --no-interaction --no-progress --prefer-dist --no-scripts
FROM docker.io/library/php:8.5-fpm-alpine@sha256:<digest> AS runtimeRUN apk add --no-cache nginx ffmpeg tini wget \ && docker-php-ext-install pdo_mysql \ && addgroup --system --gid 10001 appgroup \ && adduser --system --uid 10001 --ingroup appgroup appuserWORKDIR /var/www/htmlCOPY --from=deps /app/yourbox/vendor ./YourBOX/vendorCOPY --from=deps /app/symfony/vendor ./app/vendorCOPY YourBOX/ ./YourBOX/COPY app/ ./app/COPY infra/docker/nginx.conf /etc/nginx/nginx.confCOPY infra/docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-app.confCOPY infra/docker/entrypoint.sh /usr/local/bin/entrypoint.shRUN chmod +x /usr/local/bin/entrypoint.sh \ && mkdir -p /run/nginx /var/lib/nginx/tmp /var/log/nginx /var/cache/nginx app/var \ && chown -R appuser:appgroup /var/www/html /run/nginx /var/lib/nginx /var/log/nginx /var/cache/nginxUSER 10001RUN php app/bin/console cache:warmup --env=prod --no-debug || trueEXPOSE 8080HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget -qO- http://127.0.0.1:8080/health || exit 1ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"]The image runs as UID 10001, exposes 8080, ships a healthcheck, warms the Symfony prod cache at build time and never embeds secrets. BuildKit secret mounts are available for any private package source that requires authentication during build:
docker buildx build \ --secret id=composer_auth,src=secrets/composer_auth.json \ -t registry.yourbox.site/yourbox:0.1.0 .The sidecar files live under infra/docker/:
nginx.conflistens on 8080, setsroot /var/www/html/app/public, fastcgi passes to127.0.0.1:9000with the Symfony fallback (try_files $uri /index.php?$query_string), and routes every request through the Symfony front controller (which in turn lets the strangler bundle include legacy PHP).php-fpm.confruns as theappuseraccount with a dynamic pool sized for an alpine container.entrypoint.shstartsphp-fpm -Dand execsnginx -g 'daemon off;'under tini.
Hadolint And Trivy Gates
Section titled “Hadolint And Trivy Gates”Hadolint runs in the pre-commit hook and in the lint stage of the pipeline. The
Dockerfile passes with zero warnings. Rules disabled globally are declared in
.hadolint.yaml with a justification; no inline # hadolint ignore= comments are
needed or used.
Trivy scans the image locally via mise run image:scan and in CI. The gate is
HIGH,CRITICAL with --exit-code 1. A failure blocks the pipeline. Unfixable findings
are tracked as exceptions in infra/security/trivy-allowlist.yaml with a CVE identifier,
a justification and an expiry date.
mise run image:lint # hadolint Dockerfilemise run image:scan # trivy image --severity HIGH,CRITICAL --exit-code 1 yourbox:devLocal Task Entrypoints
Section titled “Local Task Entrypoints”All local workflows go through mise run. The bootstrap entrypoint is make init;
every other task uses the following mise tasks:
| Task | Description |
|---|---|
mise run test |
Run unit and integration suites against the compose test stack |
mise run test:unit |
Unit tests only (no database needed) |
mise run test:integration |
Integration tests against the compose MySQL service |
mise run lint:yaml |
yamllint with strict rules from .yamllint |
mise run image:lint |
hadolint on the Dockerfile, rules from .hadolint.yaml |
mise run image:build |
Build the YourBOX runtime image as yourbox:dev |
mise run image:scan |
Trivy HIGH and CRITICAL scan on the locally built image |
mise run image:smoke |
Build the image, run it, hit /health, tear down |
The test tasks source credentials from dev.env (committed, placeholder
values) which mise loads automatically via _.file. The compose.test.yml
MySQL service uses inline test fixtures. mise run image:smoke consumes the
same dev.env through docker --env-file, overriding APP_ENV=prod /
APP_DEBUG=0 so the smoke test exercises the prod kernel cache path.
Plumber Compliant Image References
Section titled “Plumber Compliant Image References”Every container image reference in YourBOX uses the immutable form
<registry>/<image>:<tag>@sha256:<digest> as defined by ADR-0009 and enforced by the
Plumber configuration in .plumber.yaml.
The digest is resolved with:
docker buildx imagetools inspect docker.io/library/php:8.5-fpm-alpine \ --format '{{.Manifest.Digest}}'The Plumber controls active for YourBOX are:
containerImageMustNotUseForbiddenTagsrejectslatest,dev,staging,main,master.containerImagesMustBePinnedByDigestrequires the@sha256:suffix.containerImageMustComeFromAuthorizedSourcesenforces the registry allowlist.branchMustBeProtectedandincludesMustBeUpToDateare enabled but will report non-compliance until plan 013 wires up branch protection and CI template components.
Test Suite
Section titled “Test Suite”The PHPUnit setup unifies under a root phpunit.xml.dist that covers two suites:
unit under tests/unit/ and integration under tests/integration/. Composer
scripts wrap each suite:
composer test # all suitescomposer test:unitcomposer test:integrationThe integration suite connects to a MySQL 8.4 container spun up by compose.test.yml.
Local credentials come from dev.env; CI uses hard coded test-only values. The DSN
uses the correct lowercase dbname= key; the earlier DB_NAME= typo is fixed.
The minimum coverage gate and functional tests are deferred to later plans pending Symfony and Postgres migration.
The CI test job uses pinned digest images and a dedicated composer:install pre-stage
to avoid executing unverified remote scripts:
composer:install: stage: .pre image: docker.io/library/composer:2@sha256:<digest> script: - composer install --no-interaction --no-progress --prefer-dist --working-dir=YourBOX
test:integration: image: docker.io/library/php:8.5-cli-alpine@sha256:<digest> services: - name: docker.io/library/mysql:8.4@sha256:<digest> alias: mysql variables: DB_DSN: "mysql://root:test@mysql:3306/YourBOX"Pre Commit Stack
Section titled “Pre Commit Stack”The pre-commit configuration combines gitleaks, yamllint, hadolint, and the conventional-pre-commit hook that enforces Conventional Commits on every commit message:
---default_install_hook_types: [pre-commit, commit-msg]
repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: - id: gitleaks - repo: https://github.com/adrienverge/yamllint rev: v1.37.1 hooks: - id: yamllint - repo: https://github.com/hadolint/hadolint rev: v2.14.0 hooks: - id: hadolint-docker - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.6.0 hooks: - id: conventional-pre-commit stages: [commit-msg] args: [--strict, --force-scope, build, chore, ci, docs, feat, fix, ...]A push that bypasses pre-commit is caught in CI through the same hooks executed by the lint stage.
Verification Checklist
Section titled “Verification Checklist”The baseline is considered met when every item below holds. The CI pipeline encodes each step.
mise installreports PHP 8.5 and pinned tooling versions.mise run image:lintexits 0 with zero warnings.mise run image:buildproduces an image andmise run image:scanexits 0.mise run image:smokeexits 0 confirming the health endpoint responds.mise run testexits 0 across unit and integration suites.grep -nE 'image: ?[^@]+$' .gitlab-ci/ Dockerfilereturns no matches.grep -nE 'FROM [^@]+$' Dockerfilereturns no matches.pre-commit run --all-filesexits 0.