Skip to content

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.

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 + capabilities

This short example shows the matching commands before the detailed ECR walkthrough. Authenticate once with GitHub Container Registry:

Terminal window
printf '%s' "$GHCR_TOKEN" \
| docker login ghcr.io --username "$GITHUB_USER" --password-stdin

Build and publish the application image in the normal Docker lane:

Terminal window
docker build --tag ghcr.io/yourorg/crm-agent:1.2.0 .
docker push ghcr.io/yourorg/crm-agent:1.2.0
docker pull ghcr.io/yourorg/crm-agent:1.2.0

Build and publish the tracked agent capabilities in the Tuff lane:

Terminal window
tuff pack build --name crm-integration --version 1.2.0
tuff pack push \
tuff-dist/crm-integration-1.2.0.tuffpack \
ghcr.io/yourorg/crm-integration:1.2.0
tuff pack pull \
ghcr.io/yourorg/crm-integration:1.2.0 \
--output build/crm-integration-1.2.0.tuffpack
tuff pack extract \
build/crm-integration-1.2.0.tuffpack \
-a open-agents \
--output build/tuff-runtime

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

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
  • jq if 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.

Terminal window
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 the repository once. Skip this command when your platform team already provisions it.

Terminal window
aws ecr create-repository \
--region "$AWS_REGION" \
--repository-name "$PACK_REPOSITORY" \
--image-tag-mutability IMMUTABLE

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

tuff-pack-ecr-policy.json
{
"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.

Request an ECR token and give it to Docker through standard input:

Terminal window
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 locally verify the deterministic artifact:

Terminal window
tuff pack build --name crm-integration --version 1.2.0
tuff pack verify tuff-dist/crm-integration-1.2.0.tuffpack

Publish it under the explicit ECR tag:

Terminal window
tuff pack push \
tuff-dist/crm-integration-1.2.0.tuffpack \
"$PACK_TAG_REFERENCE" \
--json

The 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:

Terminal window
mkdir -p build
tuff 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.

The consumer authenticates to ECR in the same way, receives the digest reference from the publisher, and writes to fresh output paths:

Terminal window
aws ecr get-login-password --region "$AWS_REGION" \
| docker login \
--username AWS \
--password-stdin "$ECR_REGISTRY"
mkdir -p build
tuff 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-runtime

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

Dockerfile
# syntax=docker/dockerfile:1
FROM 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:

Terminal window
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:

Terminal window
docker run --rm --entrypoint find example-agent:1.2.0 \
/workspace/.agents -maxdepth 4 -type f

BuildKit documents local named contexts and COPY --from=<context> in Build context and the buildx build reference.

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.

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.

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.