Container Registries skill for Codex

Immutable tags, digest deploys, safe cleanup. Codex sets a tagging scheme where every build is traceable to a commit, enables tag immutability and pins deployments by digest. It pushes from CI with OIDC instead of stored passwords, adds pull-through caches against Docker Hub rate limits, copies multi-arch images with crane or skopeo, and writes retention policies that are previewed against running workloads before you approve any deletion.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- container-registries

Installs to ~/.agents/skills/container-registries/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $container-registries, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Runs container registries well - GHCR, Amazon ECR, Google Artifact Registry, Azure ACR, Docker Hub and Harbor - with immutable tagging conventions, deploy-by-digest, OIDC push credentials from CI, retention and cleanup policies that never delete running or signed images, pull-through caches against Docker Hub rate limits, multi-arch copies with crane or skopeo, and cosign signing. Use when choosing a registry, defining image tags, registry storage costs grow, CI hits toomanyrequests, images must move between registries or accounts, or a cleanup policy needs writing safely.

Use it when

  • Defining image tags and how deployments reference them
  • Registry storage costs grow with no retention policy
  • CI or clusters hit Docker Hub rate limits
  • Images must move between registries, accounts or regions

Use something else when

How to check the result

Deployed manifests reference digests that match crane digest output, re-pushing a release tag fails, and the cleanup preview lists no running image or recent rollback target.

The mistake it prevents

Copying images with docker pull, tag and push, which silently drops every platform except the local one and changes the digest.

~/.agents/skills/container-registries/SKILL.md

--- name: container-registries description: Runs container registries well - GHCR, Amazon ECR, Google Artifact Registry, Azure ACR, Docker Hub and Harbor - with immutable tagging conventions,… license: MIT ---

Done well means: every deployed image is referenced by digest and traceable to a commit, tags that releases depend on cannot be overwritten, CI pushes with short-lived credentials, old images are cleaned up by a policy that was previewed first, and nothing running in any environment is ever deleted.

Inspect first

docker buildx imagetools inspect ghcr.io/acme/api:1.8.0          # manifest list, platforms, digest
crane digest ghcr.io/acme/api:1.8.0                               # digest only (go-containerregistry)
crane ls ghcr.io/acme/api | tail                                   # tags
skopeo inspect --raw docker://ghcr.io/acme/api:1.8.0 | head       # raw manifest
aws ecr describe-repositories --query 'repositories[].[repositoryName,imageTagMutability]' --output table
aws ecr get-lifecycle-policy --repository-name api 2>/dev/null

All need network and registry credentials; in a Codex sandbox ask before running them. Read CI workflows to see how images are built, tagged and pushed, and the deploy manifests to see how they are referenced. crane and skopeo are separate installs.

Choose the registry

SituationDefault
Code on GitHub, images public or for GitHub ActionsGHCR (ghcr.io), auth with GITHUB_TOKEN and packages: write
Workloads on AWSECR in the workload’s region, cross-account pulls via repository policy
Workloads on GCPArtifact Registry (Container Registry is retired)
Workloads on AzureACR, pulls via managed identity
Air-gapped, on-prem, multi-tenant with replication and scanningHarbor
Public distribution to many usersDocker Hub or GHCR public, plus a mirror

Keep images in the same cloud and region as the clusters that pull them: cross-region and internet egress costs add up and pulls get slower.

Tagging and referencing

TagMutable?Purpose
sha-3f9c2e1 (git commit)NeverEvery CI build; traceability
1.8.0 (semver release)NeverReleases
1.8, 1Moves to latest patch/minorConvenience for consumers of public images
main, stagingMovesHuman convenience only; never deploy from it
latestMovesAvoid for anything deployed

Deploy by digest: ghcr.io/acme/api:1.8.0@sha256:... (tag for humans, digest for the runtime). GitOps tools and Kustomize images: with digest: support this. A tag can be re-pushed; a digest cannot change content.

Enable tag immutability where the registry supports it (ECR IMMUTABLE, Artifact Registry immutable tags, Harbor immutability rules). If you keep moving tags like main, use a registry or rules that exclude only those tags from immutability.

Add OCI labels at build time (org.opencontainers.image.source, .revision, .created); GHCR uses source to link packages to the repo.

Pushing from CI without stored passwords

# GitHub Actions -> GHCR
permissions: { contents: read, packages: write, id-token: write }
steps:
  - uses: docker/login-action@v3
    with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
  • ECR: aws-actions/configure-aws-credentials with an IAM role trusted for GitHub OIDC, then aws-actions/amazon-ecr-login. Locally: aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin <acct>.dkr.ecr.eu-west-1.amazonaws.com.
  • Artifact Registry: Workload Identity Federation, then gcloud auth configure-docker europe-docker.pkg.dev.
  • Push roles are separate from pull roles; clusters and runtime identities only pull.
  • Pin third-party actions by commit SHA (see $github-actions-security).

Docker Hub limits and mirrors

Docker Hub rate-limits pulls, with much lower limits for anonymous pulls, counted per IP. Shared CI runners and NAT gateways hit toomanyrequests first. Docker announced stricter limits in 2025 and then postponed them, so check the current numbers in Docker’s docs rather than hard-coding them.

  • Authenticate every Docker Hub pull in CI and clusters (a paid or org account if volume is high).
  • Use a pull-through cache: ECR pull-through cache rules, Artifact Registry remote repositories, ACR cache rules, or Harbor proxy projects. Point base images at the cache (<acct>.dkr.ecr.<region>.amazonaws.com/docker-hub/library/node:22-slim).
  • Better still, copy the few base images you depend on into your own registry on a schedule.

Copying images between registries

crane copy ghcr.io/acme/api:1.8.0 123456789012.dkr.ecr.eu-west-1.amazonaws.com/api:1.8.0
skopeo copy --all docker://ghcr.io/acme/api:1.8.0 docker://europe-docker.pkg.dev/acme/apps/api:1.8.0

docker pull + docker tag + docker push copies only your machine’s platform and breaks multi-arch images; crane copy and skopeo copy --all keep the full index and the digest. Confirm the digest is identical at both ends. Signatures and attestations are separate artifacts; copy them too (cosign copy) if you verify them at the destination.

Retention and cleanup

Unbounded registries grow forever. Write cleanup policies, but deleting images is destructive: preview the effect, check what is running, and get the user’s confirmation before applying a policy or deleting anything.

ECR lifecycle policy (lower rulePriority is evaluated first):

{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Keep last 50 release images",
      "selection": { "tagStatus": "tagged", "tagPatternList": ["v*", "*.*.*"], "countType": "imageCountMoreThan", "countNumber": 50 },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 2,
      "description": "Expire untagged after 14 days",
      "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14 },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 3,
      "description": "Expire CI build tags after 30 days",
      "selection": { "tagStatus": "tagged", "tagPatternList": ["sha-*"], "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 30 },
      "action": { "type": "expire" }
    }
  ]
}
aws ecr start-lifecycle-policy-preview --repository-name api --lifecycle-policy-text file://policy.json
aws ecr get-lifecycle-policy-preview --repository-name api --query 'previewResults[].imageTags'
  • Artifact Registry: cleanup policies support a dry-run mode (gcloud artifacts repositories set-cleanup-policies api --location=europe --policy=policy.json --dry-run); keep it in dry run until the logged deletions look right.
  • GHCR has no built-in retention; use a scheduled workflow with actions/delete-package-versions and min-versions-to-keep.
  • Before any cleanup, list images that are running everywhere and protect them: kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u. Rollback targets (the last few releases) must survive too.
  • Untagged does not mean unused: platform-specific manifests inside a multi-arch index and some signature or attestation artifacts can appear untagged. Confirm in the preview that indexes you keep still resolve on every platform afterwards.

Signing

# Keyless signing in CI (OIDC identity from the CI provider)
cosign sign --yes ghcr.io/acme/api@sha256:<digest>
cosign verify ghcr.io/acme/api@sha256:<digest> \
  --certificate-identity-regexp 'https://github.com/acme/api/.github/workflows/release.yml@refs/tags/v.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Sign the digest, never a tag. Enforce verification at admission (see $kubernetes-security); signing without verification only adds artifacts. SBOMs, provenance and the wider supply chain belong to $supply-chain-security.

Verification

  • The deployed manifest references a digest, and crane digest <tag> matches it.
  • Pushing an existing release tag again fails (immutability works).
  • CI logs show OIDC or GITHUB_TOKEN logins; no long-lived registry passwords in secrets.
  • Cleanup preview lists no image currently running and no recent rollback target.
  • After a copy, digests match at source and destination and every platform pulls.

Report template

**Registry:** ECR eu-west-1 (api, worker, migrations), pull-through cache for docker-hub
**Tags:** sha-<commit> per build, semver per release; IMMUTABLE enabled
**Deploys:** Argo CD manifests pinned by digest via Kustomize images[].digest
**Retention:** lifecycle policy previewed: 1,412 images expire (2.1 TB), 0 running, last 50 releases kept
**Applied:** pending your confirmation
**Signing:** cosign keyless in release.yml; verification policy in Warn mode

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Container Registries"
  short_description: "Immutable tags, digest deploys, safe cleanup"
  default_prompt: "$container-registries Write an ECR lifecycle policy that keeps releases and cleans up CI images, preview it first"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc