Building Custom Images
Scion agents run inside container images that bundle an LLM harness (Claude, Gemini, etc.) with the Scion toolchain. By default, Scion uses pre-built images from the upstream registry. This guide shows how to build your own images and configure Scion to use them.
Why Build Custom Images?
Section titled “Why Build Custom Images?”- Self-hosted registries: Push images to a registry you control (GHCR, Artifact Registry, ECR, etc.).
- Pinned versions: Tag and version images to match your deployment lifecycle.
- Custom modifications: Add tools, certificates, or configurations to the base images.
Image Hierarchy
Section titled “Image Hierarchy”Scion images are built in layers:
core-base System dependencies on node:24-trixie-slim (Go, Node, Python, Git, CLI tools) └── scion-base Scion CLI, sciontool binary, scion user, entrypoint ├── scion-claude Claude Code harness ├── scion-gemini-cli Gemini CLI harness ├── scion-opencode OpenCode harness ├── scion-codex Codex harness └── scion-hub Scion hub serverThe core-base layer changes infrequently, but needs to be built at least once as it is a prerequisite for all other layers. Most rebuilds only need scion-base, the harness layers, and the hub layer (the common build target).
Besides the language runtimes, core-base installs the command-line tools agents commonly use, through image-build/lib/install-core-toolchain.sh: gh, gcloud, gcsfuse, kubectl, golangci-lint, Chromium, and helm, plus the npm tools chrome-devtools-mcp and @playwright/cli. helm is pinned to an exact version, v3.17.3, the version used by the chart’s CI, so agents that work on the deploy/helm/scion-hub chart render the same output as CI without downloading helm at run time. To change the version, set the HELM_VERSION, HELM_SHA256_AMD64, and HELM_SHA256_ARM64 build args together; the build fails if the downloaded archive does not match the checksum for the build architecture. The thick-prep layer installs the same pinned helm.
Non-Root Requirement
Section titled “Non-Root Requirement”For security and compatibility across runtimes (especially Kubernetes), Scion agents are required to run as a non-root user.
- User: The base images create a
scionuser. - UID: The user must have UID
1000. - Permissions: Ensure your custom images do not require root privileges at runtime and that any added files or directories are accessible by the
scionuser. Home directory structure (/home/scion) and environmental variables (HOME,USER,LOGNAME) are automatically injected by the runtime.
Building Behind a Corporate Proxy (npm Registry)
Section titled “Building Behind a Corporate Proxy (npm Registry)”Corporate networks that block registry.npmjs.org can point the build at an internal npm mirror (Artifactory, Nexus, Verdaccio, etc.) using two environment variables:
| Variable | Purpose | Default |
|---|---|---|
NPM_REGISTRY |
Registry URL passed as a Docker build arg. Sets NPM_CONFIG_REGISTRY inside the image so that all npm install commands during the build — and agents at runtime — use the mirror. |
https://registry.npmjs.org/ |
NPM_CONFIG_FILE |
Host path to an .npmrc file. Mounted as a BuildKit secret (id=npmrc) so authentication tokens are available during RUN steps but never written to an image layer. |
— (unauthenticated) |
# Unauthenticated mirrorNPM_REGISTRY=https://npm.corp.example.com/ \ image-build/scripts/build-images.sh --target all
# Authenticated mirror (token stays out of the image)NPM_REGISTRY=https://npm.corp.example.com/ \NPM_CONFIG_FILE=~/.npmrc \ image-build/scripts/build-images.sh --target all --push --registry ghcr.io/myorgNPM_REGISTRY is threaded as a build arg into the core-base layer and inherited by every downstream image. NPM_CONFIG_FILE is forwarded by the local-docker and local-podman builders; cloud-build does not support BuildKit secrets — use a private Artifact Registry npm repository with IAM-based auth instead.
Building Behind a Corporate Proxy (Python Package Index)
Section titled “Building Behind a Corporate Proxy (Python Package Index)”The same pattern applies to Python packages. Corporate networks that block pypi.org can point the build at an internal PyPI mirror (Artifactory, Nexus, devpi, etc.) using two environment variables:
| Variable | Purpose | Default |
|---|---|---|
PIP_INDEX_URL |
Registry URL passed as a Docker build arg. Sets PIP_INDEX_URL inside the image so that all pip install commands during the build — and agents at runtime — use the mirror. |
https://pypi.org/simple |
PIP_CONFIG_FILE |
Host path to a pip.conf file. Mounted as a BuildKit secret (id=pipconf) so authentication tokens are available during RUN steps but never written to an image layer. |
— (unauthenticated) |
# Unauthenticated mirrorPIP_INDEX_URL=https://pypi.corp.example.com/simple/ \ image-build/scripts/build-images.sh --target all
# Authenticated mirror (credentials stay out of the image)PIP_INDEX_URL=https://pypi.corp.example.com/simple/ \PIP_CONFIG_FILE=~/.pip/pip.conf \ image-build/scripts/build-images.sh --target all --push --registry ghcr.io/myorgPIP_INDEX_URL is threaded as a build arg into the core-base layer and inherited by every downstream image. PIP_CONFIG_FILE is forwarded by the local-docker and local-podman builders; cloud-build does not support BuildKit secrets — use a private Artifact Registry PyPI repository with IAM-based auth instead.
How the Build Tooling Is Organized
Section titled “How the Build Tooling Is Organized”A single orchestrator script — image-build/scripts/build-images.sh — owns the build DAG (which images depend on which, in what order, with which tags). The execution backend is selected with --builder. Three backends ship today:
| Builder | Backend | Multi-arch | Push behavior |
|---|---|---|---|
local-docker (default) |
docker buildx |
yes (auto-promotes to --push) |
honors --push; --load otherwise |
local-podman |
podman build |
single-arch by default; multi-arch errors out (manual QEMU setup required) | honors --push; built images live in the local store automatically |
cloud-build |
gcloud builds submit against a static cloudbuild-*.yaml (group targets) or a config generated on the fly (individual harness targets) |
always linux/amd64 + linux/arm64 (server-side) |
always pushes |
The orchestrator computes tags, threads BASE_IMAGE between layers, and dispatches to the selected builder. Switching backends is purely a --builder flag change — target names and other flags are uniform.
Quick Start
Section titled “Quick Start”Option 1: Local Docker Build
Section titled “Option 1: Local Docker Build”Build all images locally. Once core-base has been built, rebuilds can often use the default common build target.
# Build all layers locally without pushing — bare tags# (scion-claude:latest, etc.) land in your local docker engine.image-build/scripts/build-images.sh --target all
# Or build and push to your registryimage-build/scripts/build-images.sh --registry ghcr.io/myorg --push --target all
# When pushing, configure Scion to use themscion config set image_registry ghcr.io/myorg--registry is optional for local-only builds. Omit it and the orchestrator tags images with bare names that stay in your local image store. Supply it (without --push) if you’d rather have fully-qualified tags locally — nothing leaves the machine until you docker push separately. --registry becomes required as soon as you pass --push or --builder cloud-build.
Option 2: Local Podman Build
Section titled “Option 2: Local Podman Build”# Single-arch local build, no registry neededimage-build/scripts/build-images.sh --builder local-podman --target all
# Or push to a registryimage-build/scripts/build-images.sh \ --builder local-podman \ --registry quay.io/myorg \ --pushMulti-arch Podman builds require manual QEMU binfmt setup. Until that is in place, passing --platform linux/amd64,linux/arm64 to local-podman exits with an actionable error.
Option 3: GitHub Actions (GHCR)
Section titled “Option 3: GitHub Actions (GHCR)”If your project is hosted on GitHub:
- Fork the repo (or use it as a template).
- Go to Actions > Build Scion Images > Run workflow.
- Enter
ghcr.io/<your-username>as the registry. - Wait for the build to complete.
- Configure Scion:
Terminal window scion config set image_registry ghcr.io/<your-username>
The workflow shells out to build-images.sh --builder local-docker after docker/setup-buildx-action, so it shares all the orchestration logic with local builds. It is also available as a reusable workflow via workflow_call for downstream repos.
Option 4: Google Cloud Build
Section titled “Option 4: Google Cloud Build”For GCP-based workflows:
# One-time setup: enable APIs, create Artifact Registry repo, grant permissionsimage-build/scripts/setup-cloud-build.sh --project my-gcp-project
# Submit a buildimage-build/scripts/build-images.sh \ --builder cloud-build \ --registry us-central1-docker.pkg.dev/my-gcp-project/scionThen point Scion at the registry:
scion config set image_registry us-central1-docker.pkg.dev/my-gcp-project/scionConfiguring Scion: image_registry
Section titled “Configuring Scion: image_registry”The image_registry setting tells Scion to pull images from your registry instead of the upstream default. It rewrites the registry prefix of all standard harness images (those named scion-<harness>) while preserving the image name and tag.
How It Works
Section titled “How It Works”When image_registry is set, Scion transforms the default image reference:
| Default Image | image_registry |
Resolved Image |
|---|---|---|
us-central1-docker.pkg.dev/.../scion-claude:latest |
ghcr.io/myorg |
ghcr.io/myorg/scion-claude:latest |
us-central1-docker.pkg.dev/.../scion-gemini-cli:latest |
ghcr.io/myorg |
ghcr.io/myorg/scion-gemini-cli:latest |
Setting It
Section titled “Setting It”Globally (applies to all projects):
scion config set image_registry ghcr.io/myorgOr edit ~/.scion/settings.yaml directly:
schema_version: "1"image_registry: "ghcr.io/myorg"Per-profile (different registries for different environments):
profiles: local: runtime: docker image_registry: "ghcr.io/myorg" staging: runtime: kubernetes image_registry: "us-central1-docker.pkg.dev/myproject/staging"Profile-level image_registry takes precedence over the top-level setting of the same source.
Across sources the order is: the Runtime Broker’s SCION_IMAGE_REGISTRY, then values saved in the
Hub (profile, then top level), then the settings file (profile, then top level). So a registry
saved in the Hub, for example through the setup wizard, beats a settings-file profile value; save
the profile value in the Hub to keep it. See Image registry order. The profile used is
the one the agent was provisioned with, recorded broker-side, on every start and restart; see
Settings precedence.
Override Precedence
Section titled “Override Precedence”The image_registry setting is the lowest-priority way to configure images. Explicit overrides always win (summary; see the full chain linked below, which also covers inline config and settings harness_configs.<name>.image):
- CLI
--imageflag (highest priority) - Profile
harness_overridesimage field, when explicitly set - Agent inline config (
--config) image field - Settings
harness_configs.<name>.image, then the harness-config’s ownconfig.yamlimage image_registryrewrite (lowest priority)
A template’s image field is ignored with a warning: a template describes the agent’s job, and the image comes from the harness-config and settings.
Each time a higher-priority source replaces another source’s image, the agent start logs it at Info. See Settings precedence for the full chain.
If any higher-priority override specifies a full image path, image_registry does not apply to that agent.
Build Script Reference
Section titled “Build Script Reference”The image-build/scripts/build-images.sh orchestrator supports the following options:
| Flag | Description | Default |
|---|---|---|
--registry <path> |
Target registry path (e.g., ghcr.io/myorg). Required when --push is set or with --builder cloud-build. When omitted for a local-only build, images are tagged with bare names (e.g., scion-claude:latest) and stay in the local store. |
— |
--builder <name> |
Backend: local-docker, local-podman, or cloud-build. |
local-docker |
--target <target> |
Build target (see below). | common |
--tag <tag> |
Mutable image tag. The :<short-sha> tag is always added when in a git repo. |
latest |
--platform <plat> |
Target platform(s). Use all for linux/amd64,linux/arm64. Ignored by cloud-build. |
builder’s native arch |
--push |
Push images after building. Auto-enabled for multi-arch local builds. Ignored by cloud-build (always pushes). |
build only |
--dry-run |
Print the resolved steps and the exact builder commands without executing. | off |
Build Targets
Section titled “Build Targets”Targets resolve to an ordered list of step IDs (one step per image):
| Target | What It Builds | Notes |
|---|---|---|
core-base |
core-base |
Foundation tools layer. |
scion-base |
scion-base |
Adds sciontool. Reuses existing core-base:<tag>. |
harnesses |
All catalog harness images with a harnesses/<name>/Dockerfile |
Reuses existing scion-base:<tag>. |
hub |
scion-hub |
Hub server image. Reuses existing scion-base:<tag>. |
common (default) |
scion-base + harnesses + hub |
Skips core-base. Most common rebuild. |
all |
Full DAG | Rebuilds everything from core-base. |
Tagging
Section titled “Tagging”Every image is tagged with both :<tag> (controlled by --tag, defaults to latest) and :<short-sha> (computed once from git rev-parse --short HEAD). When no SHA is available (e.g. running outside a git working tree), only the mutable tag is emitted.
When two steps in the same run depend on each other, the orchestrator threads BASE_IMAGE=...:<short-sha> so chained builds are immune to concurrent overwrites of :latest. Standalone targets (e.g. --target harnesses on its own) reference the parent image as :<tag>.
Build Provenance
Section titled “Build Provenance”When building scion-base, the orchestrator stamps scion and sciontool with the git commit and, when HEAD is on an exact git tag, the version. Inside an agent container, sciontool version reports both. scion-base also carries the OCI org.opencontainers.image.revision label, which every image built FROM scion-base (harnesses, hub) inherits. To check which commit an image was built from without running it:
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' scion-claude:latestIf a build uses scion-base but does not rebuild it in the same run (for example, --target harnesses), the orchestrator prints a warning. The resulting images inherit whatever sciontool is already baked into the existing scion-base:<tag>. The warning also shows that image’s revision when a local copy exists. Include scion-base in the build (for example, --target common) so sciontool fixes reach your agents.
Authentication
Section titled “Authentication”The orchestrator and builders assume the caller is already authenticated to the target registry (via docker login, podman login, gcloud auth configure-docker, etc.) and to any required cloud APIs. No login steps are performed inside the script.
Examples
Section titled “Examples”# Full rebuild for all platforms, pushed to GHCRimage-build/scripts/build-images.sh \ --registry ghcr.io/myorg \ --target all \ --platform all \ --push
# Build only harness images with a specific tagimage-build/scripts/build-images.sh \ --registry ghcr.io/myorg \ --target harnesses \ --tag v1.2.0 \ --push
# Local build for testing (no push, current architecture only, bare tags)image-build/scripts/build-images.sh --target all
# Preview what would run, without executing anythingimage-build/scripts/build-images.sh \ --registry ghcr.io/myorg \ --target all \ --platform all \ --dry-run
# Submit the same target DAG to Cloud Buildimage-build/scripts/build-images.sh \ --builder cloud-build \ --registry us-central1-docker.pkg.dev/myproject/scion \ --target allGitHub Actions Workflow
Section titled “GitHub Actions Workflow”The workflow at .github/workflows/build-images.yml can be used in two ways:
Manual Trigger (workflow_dispatch)
Section titled “Manual Trigger (workflow_dispatch)”Run it from the GitHub Actions UI with inputs for registry, target, tag, and platform.
Reusable Workflow (workflow_call)
Section titled “Reusable Workflow (workflow_call)”Call it from your own workflows in downstream repos:
jobs: build-images: uses: google/scion/.github/workflows/build-images.yml@main with: registry: ghcr.io/myorg target: common tag: latest platform: allThe workflow is a runner, not a builder — it shells out to build-images.sh --builder local-docker and shares the same Dockerfiles and orchestration as a local build.
Google Cloud Build Configs
Section titled “Google Cloud Build Configs”The cloud-build builder maps each group --target to a static YAML file in image-build/:
| Target | Config file |
|---|---|
all |
cloudbuild.yaml |
common |
cloudbuild-common.yaml |
core-base |
cloudbuild-core-base.yaml |
scion-base |
cloudbuild-scion-base.yaml |
harnesses |
cloudbuild-harnesses.yaml |
hub |
cloudbuild-hub.yaml |
An individual harness step ID (scion-claude, scion-muse-code, etc.) is also
a valid --target under cloud-build. Rather than a static file, the builder
generates a one-step config for just that harness — mirroring the matching
step in cloudbuild-harnesses.yaml — submits it, and removes it afterward.
This rebuilds one harness on Cloud Build without resubmitting the whole
harnesses group.
These YAMLs reference $_TAG, $_SHORT_SHA, $_COMMIT_SHA, $_REGISTRY, and (in the five that build scion-base: all, common, scion-base, thick/thick-prep, omni) $_VERSION, all forwarded by the orchestrator. _TAG defaults to latest in every YAML’s substitutions: block; _VERSION defaults to '' in the YAMLs that declare it, preserving the prior behavior when --tag or a version tag is omitted. The orchestrator only forwards a non-empty _VERSION when HEAD is on an exact git tag; off-tag, it relies on that default.
Initial Setup
Section titled “Initial Setup”Run the one-time setup script to configure your GCP project:
image-build/scripts/setup-cloud-build.sh --project my-gcp-projectThis script:
- Enables the Cloud Build and Artifact Registry APIs.
- Creates an Artifact Registry repository named
scion. - Grants Cloud Build the necessary IAM permissions.