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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- container-registries Installs to ~/.agents/skills/container-registries/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install container-registries The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project container-registries Installs to .agents/skills/container-registries/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/container-registries.zipunzip -o container-registries.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) container-registries PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You want smaller images rather than better registry hygiene: use docker-image-slimming
- You need SBOMs, SLSA provenance and signing policy across the pipeline: use supply-chain-security
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.
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
| Situation | Default |
|---|---|
| Code on GitHub, images public or for GitHub Actions | GHCR (ghcr.io), auth with GITHUB_TOKEN and packages: write |
| Workloads on AWS | ECR in the workload’s region, cross-account pulls via repository policy |
| Workloads on GCP | Artifact Registry (Container Registry is retired) |
| Workloads on Azure | ACR, pulls via managed identity |
| Air-gapped, on-prem, multi-tenant with replication and scanning | Harbor |
| Public distribution to many users | Docker 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
| Tag | Mutable? | Purpose |
|---|---|---|
sha-3f9c2e1 (git commit) | Never | Every CI build; traceability |
1.8.0 (semver release) | Never | Releases |
1.8, 1 | Moves to latest patch/minor | Convenience for consumers of public images |
main, staging | Moves | Human convenience only; never deploy from it |
latest | Moves | Avoid 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-credentialswith an IAM role trusted for GitHub OIDC, thenaws-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-versionsandmin-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_TOKENlogins; 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.yaml
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.