OCI Registries and Container Images
This guide follows one pack from source to Amazon Elastic Container Registry (ECR), then into a runnable container image. The same Tuff commands work with other OCI-compatible registries; only repository provisioning and login differ.
First: understand the two layers
Section titled “First: understand the two layers”The word “layer” refers to two different things in this workflow:
| Layer | Contains | Created by | Runnable by Docker? |
|---|---|---|---|
| Tuff OCI artifact layer | The exact .tuffpack bytes |
tuff pack push |
No |
| Container image filesystem layer | Extracted .agents/... and other harness-native files |
Dockerfile COPY |
Yes, as part of the image |
A Tuff pack uses an OCI manifest so a registry can store and transport it, but it is not a container image. It has an empty OCI configuration and a Tuff-specific layer media type rather than an operating system, architecture, command, or root filesystem. Do not use docker pull or FROM with a Tuff pack reference.
Container images and Tuff packs use the same registry transport, but they remain different artifacts. The two delivery lanes meet only after the verified capability target is copied into the image build context:
Agent application lane Agent capability lane────────────────────── ─────────────────────application source tracked skills, tools, hooks, MCP servers │ │ ▼ ▼docker build tuff pack build --name ... │ │ ▼ ▼container image .tuffpack artifact │ │ ▼ ▼docker push / docker pull tuff pack push / tuff pack pull │ ▼ tuff pack extract │ ┌────────────────────────┘ ▼ Dockerfile COPY │ ▼ runnable image = application + capabilitiesQuick example with GHCR
Section titled “Quick example with GHCR”This short example shows the matching commands before the detailed ECR walkthrough. Authenticate once with GitHub Container Registry:
printf '%s' "$GHCR_TOKEN" \ | docker login ghcr.io --username "$GITHUB_USER" --password-stdinBuild and publish the application image in the normal Docker lane:
docker build --tag ghcr.io/yourorg/crm-agent:1.2.0 .docker push ghcr.io/yourorg/crm-agent:1.2.0docker pull ghcr.io/yourorg/crm-agent:1.2.0Build and publish the tracked agent capabilities in the Tuff lane:
tuff pack build --name crm-integration --version 1.2.0tuff pack push \ tuff-dist/crm-integration-1.2.0.tuffpack \ ghcr.io/yourorg/crm-integration:1.2.0tuff pack pull \ ghcr.io/yourorg/crm-integration:1.2.0 \ --output build/crm-integration-1.2.0.tuffpacktuff pack extract \ build/crm-integration-1.2.0.tuffpack \ -a open-agents \ --output build/tuff-runtimeThe final Dockerfile and BuildKit commands later in this guide copy build/tuff-runtime into the application image. docker pull cannot pull the Tuff pack because its OCI layer has a Tuff media type; use tuff pack pull for that lane.
Prerequisites
Section titled “Prerequisites”You need:
- Tuff installed;
- AWS CLI v2 configured for the target account and Region;
- permission to create or use an ECR private repository;
- Docker with Buildx/BuildKit; and
jqif you want to capture Tuff’s JSON result automatically in a shell or CI job.
This guide uses account 111122223333, Region eu-west-2, ECR repository tuff/crm-integration, and pack version 1.2.0. Replace them with your values.
export AWS_ACCOUNT_ID="111122223333"export AWS_REGION="eu-west-2"export PACK_REPOSITORY="tuff/crm-integration"export PACK_TAG="1.2.0"export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"export PACK_TAG_REFERENCE="${ECR_REGISTRY}/${PACK_REPOSITORY}:${PACK_TAG}"OCI references do not include https://. Tuff uses HTTPS by default.
Create an immutable ECR repository
Section titled “Create an immutable ECR repository”Create the repository once. Skip this command when your platform team already provisions it.
aws ecr create-repository \ --region "$AWS_REGION" \ --repository-name "$PACK_REPOSITORY" \ --image-tag-mutability IMMUTABLEECR tag immutability complements Tuff’s safe-tag default. Repeating a push of the same Tuff manifest still reports unchanged because Tuff detects that before publishing. A different manifest is refused by Tuff without --force, and an immutable ECR repository also rejects an attempted forced tag move. Use a new version tag instead of --force for releases.
Amazon ECR private repositories support OCI-compatible artifacts. See the ECR repository documentation and create-repository reference.
Grant the publisher and consumer permissions
Section titled “Grant the publisher and consumer permissions”The following combined policy covers login, Tuff’s safe-tag read, blob publication, and later pull. Replace the account, Region, and repository in Resource. Repository creation is intentionally separate and requires ecr:CreateRepository for the provisioning identity.
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AuthenticateToEcr", "Effect": "Allow", "Action": "ecr:GetAuthorizationToken", "Resource": "*" }, { "Sid": "PublishAndPullTuffPacks", "Effect": "Allow", "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:BatchGetImage", "ecr:CompleteLayerUpload", "ecr:GetDownloadUrlForLayer", "ecr:InitiateLayerUpload", "ecr:PutImage", "ecr:UploadLayerPart" ], "Resource": "arn:aws:ecr:eu-west-2:111122223333:repository/tuff/crm-integration" } ]}For separate roles, the consumer needs GetAuthorizationToken, BatchGetImage, and GetDownloadUrlForLayer; the publisher needs the remaining upload actions plus BatchGetImage for Tuff’s existing-tag check. AWS documents the standard repository-scoped publisher policy in IAM permissions for pushing to ECR.
Authenticate Tuff to ECR
Section titled “Authenticate Tuff to ECR”Request an ECR token and give it to Docker through standard input:
aws ecr get-login-password --region "$AWS_REGION" \ | docker login \ --username AWS \ --password-stdin "$ECR_REGISTRY"docker login stores the credential in Docker’s configured credential store or helper. Tuff reads that existing Docker configuration, so there is no separate tuff login command and the token is not included in the pack. ECR authorization tokens expire; authenticate again at the start of each publisher or consumer job. See AWS’s get-login-password example.
Build and publish the pack
Section titled “Build and publish the pack”Build and locally verify the deterministic artifact:
tuff pack build --name crm-integration --version 1.2.0tuff pack verify tuff-dist/crm-integration-1.2.0.tuffpackPublish it under the explicit ECR tag:
tuff pack push \ tuff-dist/crm-integration-1.2.0.tuffpack \ "$PACK_TAG_REFERENCE" \ --jsonThe result contains both digests and an immutable reference:
{ "status": "pushed", "name": "crm-integration", "version": "1.2.0", "artifactDigest": "sha256:<tuffpack-digest>", "manifestDigest": "sha256:<oci-manifest-digest>", "tagReference": "111122223333.dkr.ecr.eu-west-2.amazonaws.com/tuff/crm-integration:1.2.0", "reference": "111122223333.dkr.ecr.eu-west-2.amazonaws.com/tuff/crm-integration@sha256:<oci-manifest-digest>"}The reference field is the release identity to pass to deployment jobs. A tag is readable but mutable in the general OCI model; the manifest digest identifies the exact registry object that was reviewed.
To capture it in CI:
mkdir -p buildtuff pack push \ tuff-dist/crm-integration-1.2.0.tuffpack \ "$PACK_TAG_REFERENCE" \ --json > build/tuff-pack-push.json
export PACK_DIGEST_REFERENCE="$(jq -r '.reference' build/tuff-pack-push.json)"printf '%s\n' "$PACK_DIGEST_REFERENCE"Persist PACK_DIGEST_REFERENCE as deployment metadata or a downstream CI output. Do not reconstruct it from the tag later.
Pull and extract the runtime target
Section titled “Pull and extract the runtime target”The consumer authenticates to ECR in the same way, receives the digest reference from the publisher, and writes to fresh output paths:
aws ecr get-login-password --region "$AWS_REGION" \ | docker login \ --username AWS \ --password-stdin "$ECR_REGISTRY"
mkdir -p buildtuff pack pull \ "$PACK_DIGEST_REFERENCE" \ --output build/crm-integration-1.2.0.tuffpack
tuff pack extract \ build/crm-integration-1.2.0.tuffpack \ -a open-agents \ --output build/tuff-runtimepack pull verifies the OCI manifest, layer size and digest, complete Tuff artifact, stored files, and metadata annotations before it creates the output file. pack extract verifies the artifact again and writes the pre-rendered open-agents target. Both commands refuse to overwrite existing output, so CI should use a fresh workspace or new paths.
For open-agents, the extracted root contains paths such as .agents/skills/..., .agents/tools/..., and any shared harness configuration emitted by the adapter. Other --harness values produce that adapter’s native layout.
Add the extracted target to a container image
Section titled “Add the extracted target to a container image”Pass the extracted directory as a named BuildKit context. This keeps ECR authentication and Tuff outside the Docker build and prevents the raw .tuffpack from becoming runtime baggage.
# syntax=docker/dockerfile:1FROM debian:bookworm-slim
WORKDIR /workspace
# Copies the verified harness-native tree, including hidden .agents paths.COPY --from=tuff-runtime / /workspace/
# Install the application or agent runtime after this line.CMD ["sh"]Build the image and load it into the local Docker image store:
docker buildx build \ --build-context tuff-runtime=./build/tuff-runtime \ --tag example-agent:1.2.0 \ --load \ .Docker treats the COPY result as ordinary image filesystem content. The image now contains /workspace/.agents/...; it does not contain registry credentials unless some unrelated Dockerfile instruction adds them.
For a base image that contains find, inspect the result with:
docker run --rm --entrypoint find example-agent:1.2.0 \ /workspace/.agents -maxdepth 4 -type fBuildKit documents local named contexts and COPY --from=<context> in Build context and the buildx build reference.
Why not pull inside the Dockerfile?
Section titled “Why not pull inside the Dockerfile?”Pulling before docker build keeps cloud login, registry tokens, Tuff, and provider CLIs out of the build definition and image history. It also creates a clear verification boundary: only an extracted target derived from the approved digest enters the build context.
An advanced BuildKit build could mount secrets and run Tuff inside a build stage, but that adds token handling, networking, tool installation, and cache behavior without improving the resulting image. Tuff standardizes on pre-pull and pre-extract for this workflow.
Copying only the raw .tuffpack into an image is also possible, but the runtime would then need Tuff and an extraction step during startup. That delays verification and mutation until runtime, so it is not the recommended immutable-image workflow.
Other compatible registries
Section titled “Other compatible registries”After login, the Tuff build, push, pull, digest, extract, and Docker steps remain unchanged.
| Registry | Configure reusable credentials | Reference shape |
|---|---|---|
| GitHub Container Registry | printf '%s' "$GHCR_TOKEN" | docker login ghcr.io --username "$GITHUB_USER" --password-stdin |
ghcr.io/yourorg/crm-integration:1.2.0 |
| Google Artifact Registry | gcloud auth configure-docker europe-west2-docker.pkg.dev |
europe-west2-docker.pkg.dev/project/repository/crm-integration:1.2.0 |
| Azure Container Registry | az acr login --name myregistry |
myregistry.azurecr.io/team/crm-integration:1.2.0 |
| Self-hosted OCI registry | docker login registry.example.com |
registry.example.com/team/crm-integration:1.2.0 |
Tuff checks Docker credentials first and Podman credentials second. For a self-hosted registry with a private certificate authority, the login client must trust that CA according to its own configuration, and each Tuff push or pull must receive --ca-file company-ca.pem; Tuff’s flag does not change Docker or Podman trust settings. Use --plain-http only with an isolated disposable development registry.
Common failures
Section titled “Common failures”| Error | Likely cause | Resolution |
|---|---|---|
Authentication or 401 Unauthorized |
Missing, expired, or wrong-Region ECR login | Run get-login-password again with the repository’s Region and correct registry hostname. |
| Repository or manifest not found | Repository was not provisioned, or reference uses the wrong account, Region, repository, tag, or digest | Check the ECR repository and use the exact reference printed by Tuff. |
refusing to move existing OCI tag |
The tag already identifies different content | Publish a new release tag; do not force an immutable ECR tag. |
refusing to overwrite |
Pull or extract output already exists | Use a fresh CI workspace or choose a new output path. |
Docker cannot use the Tuff reference in FROM |
A Tuff pack is a generic OCI artifact, not a runnable image | Pull with Tuff, extract a target, and pass it as a local named build context. |