/docker-workflow

internal
Category:
Internal Skills

What it is

Multi-stage Dockerfile patterns, Docker Compose for local development, and image security scanning with vulnerability triage.

Called by

  • /gaia-dev-story (when Docker is in the stack)
  • TypeScript, Angular, Java, Python, and mobile stack dev agents

What it does

Provides multi-stage Dockerfile patterns for different stacks (Node.js, Java, Python) with build and runtime separation.

Documents Docker Compose patterns for local development environments.

Covers image security scanning and vulnerability triage workflows.

When you will see it surface

  • When a story involves Docker changes, the dev agent loads these patterns for guidance.

Technical notes

  • Separate build dependencies from runtime -- smaller images, reduced attack surface.
  • Run containers as non-root users.
  • Use .dockerignore to exclude unnecessary files.

The bundled gaia-tools deterministic-tools image

GAIA ships a single OCI image (ghcr.io/gaiastudio-ai/gaia-tools) that bundles every Tier-2 brownfield scanner with pinned versions and a freshly-warmed Grype vuln-DB. When brownfield.tools.runner: docker is set in .gaia/config/project-config.yaml, the brownfield Phase-3 adapters dispatch through this image instead of resolving binaries from $PATH — so operators on a stock machine need only Docker, not brew + pip + npm + Go SDK + Java SDK + .NET Sarif.Multitool.

Bundled tools (v0.1.4 / 2026-06-02)

ToolTierVersionPurpose
grype20.79.5CVE scanner (any stack)
syft21.4.1SBOM producer (any stack)
osv-scanner11.7.4OSV CVE scan
spotbugs24.8.4JVM dead-code + bug finder
vulture12.13Python dead-code
pip-audit12.7.3Python CVE
cyclonedx-py14.4.3Python SBOM
cdxgen110.11.0Node SBOM
yamllint11.35.1YAML workflow lint
yq (Mike Farah)4.44.1YAML query
Sarif.Multitool5.0.2Phase-7 SARIF merge (package-name + input-path mapping ensure merged output contains real findings)

Enabling the docker runner

Set the runner switch via the canonical config editor:

/gaia-config-brownfield set tools.runner docker
/gaia-config-brownfield set tools.image ghcr.io/gaiastudio-ai/gaia-tools:0.1.4-2026-06-02

Or hand-edit .gaia/config/project-config.yaml:

brownfield:
  deterministic_tools: true
  tools:
    runner: docker          # default: native
    image:  ghcr.io/gaiastudio-ai/gaia-tools:0.1.4-2026-06-02

Image override env var: for one-off scans against a non-default image without touching config, export GAIA_TOOLS_IMAGE before invoking the brownfield run. The env-var beats the config key. Useful for testing a not-yet-published image tag.

Pulling / building the image

CI publishes the image on push to main and on a monthly cron for vuln-DB freshness — see .github/workflows/gaia-tools-image.yml. The canonical pull path:

/gaia-doctor --install --docker

If docker pull fails with denied (registry visibility issue) or the published image lags the in-tree Dockerfile (the monthly cron has not yet republished), self-build locally:

docker build -t ghcr.io/gaiastudio-ai/gaia-tools:latest plugins/gaia/tools/gaia-tools/

That tag matches the default docker-runner.sh resolves when brownfield.tools.image isn't set, so the brownfield adapters route through the local image without further config.

Build prerequisites: the Dockerfile itself installs everything it needs; the HOST building the image needs ONLY Docker (with buildkit if you want multi-arch via docker buildx). The image's runtime stage bundles the .NET 8 SDK (for Microsoft.Sarif.Multitool) and the native runtime dependency libicu72 — both are installed inside the image, not on the host. Multi-arch builds (linux/amd64 + linux/arm64) work from any Docker host with QEMU emulation enabled (Docker Desktop ships this on by default; on a bare Linux host run docker run --privileged --rm tonistiigi/binfmt --install all once).

History of build-blocker classes: earlier image revisions resolved several Dockerfile blockers and baked in GRYPE_DB_VALIDATE_AGE=false. Adding the .NET SDK layer for Sarif.Multitool briefly introduced three new blockers — a missing libicu72 runtime dependency; a wrong NuGet package name (Microsoft.Sarif.Multitool, where the actual tool package is Sarif.Multitool); and an unset DOTNET_ROOT — all since fixed. A fresh build from the current branch succeeds end-to-end, and the sarif --version probe at the end of the .NET layer verifies the install before the build accepts the image.

What runs through the runner

Every Tier-2 and applicable Tier-1 brownfield step routes through the docker runner when tools.runner=docker:

  • grype — CVE scan (via scripts/adapters/grype/adapter.sh).
  • syft — SBOM producer (Phase-3 SBOM step in plugins/gaia/skills/gaia-brownfield/SKILL.md).
  • python-vulture — Python dead-code (scripts/adapters/dead-code/python-vulture/adapter.sh).
  • spotbugs — JVM dead-code (scripts/adapters/dead-code/jvm-spotbugs/adapter.sh).
  • Sarif.Multitool — Phase-7 SARIF merge (scripts/adapters/brownfield/sarif-merge.sh).
  • go-deadcode — probes the runner first; bundled image does not yet ship the binary (Go toolchain off-policy), so it falls through to host-PATH with a clear INFO log when neither is present.

Mount + network contract

The runner (plugins/gaia/scripts/lib/docker-runner.sh) dispatches with:

docker run --rm --network=none \
  -v "$PROJECT_ROOT:/workspace:ro" \
  -v "$ADAPTER_OUT_DIR:/out" \
  -w /workspace \
  $TOOLS_IMAGE  $SUBCMD  $ARGS

Inside the container, the project source is read-only at /workspace, SARIF + JSON output lands at /out, and --network=none prevents accidental phone-home. The bundled grype's bypass of the vuln-DB age check is what lets it scan without network — pre-warmed DB is good for the lifetime of the image tag.