The image supply chain
Every image OpsCrew builds says what is in it and where it came from, and OpsCrew keeps the images and the tools behind them current. This page shows how to read that record, how to sign images, how updates reach you, and how deploys can be tagged and old images cleaned up.
What is in an image, and where it came from
Section titled “What is in an image, and where it came from”Each image is pushed to GitHub Container Registry as ghcr.io/<owner>/<repo>:<commit> together
with two records:
- an SBOM (software bill of materials): every package in the image, with its version;
- a provenance record: the workflow run that built the image, the repository and the commit.
Anyone who can pull the image can read both. For a private repository, log in first with
docker login ghcr.io. Then:
docker buildx imagetools inspect ghcr.io/<owner>/<repo>:<commit> --format '{{json .Provenance}}'docker buildx imagetools inspect ghcr.io/<owner>/<repo>:<commit> --format '{{json .SBOM}}'<owner>/<repo> is your repository in lower case, and <commit> the full commit hash. An image
another environment than prod built is tagged <commit>-<env> instead (for
example <commit>-staging), so two environments that build the same commit each keep their own
image. The deploy run’s summary shows the image’s tag and its digest; you can use the digest
instead of the tag (ghcr.io/<owner>/<repo>@sha256:…).
The provenance never holds a secret value or GitHub’s event data, such as commit messages or email addresses. Build values that are not secret can appear in it (see Values the build needs).
Signing images
Section titled “Signing images”Signing is off by default. When it is on, the deploy workflow signs each image with
cosign’s keyless signing: there is no key to keep, and the signature
is stored next to the image. Turn it on in the interview, or with supplyChain.sign: true in
opscrew/plan.yaml, then run npx opscrew provision.
For a private repository, signing makes its name public. Keyless signatures are recorded in Sigstore’s public transparency log, and each entry shows the repository’s name and the path of the deploy workflow. OpsCrew says so in the interview before you turn it on.
To check a signature, with cosign installed:
cosign verify ghcr.io/<owner>/<repo>@sha256:<digest> \ --certificate-identity "https://github.com/<Owner>/<Repo>/.github/workflows/opscrew-deploy.yml@refs/heads/main" \ --certificate-oidc-issuer https://token.actions.githubusercontent.comThe image name is in lower case, but the identity is not: write <Owner>/<Repo> exactly as GitHub
spells your repository, as in its address on github.com (for example Acme/Shop). The signature
records that spelling, and cosign compares it letter for letter. The identity is the deploy
workflow on your deploy branch: use your branch instead of main if it has another name, and the
path in cicd.workflowPath (in opscrew/plan.yaml) if you changed it. With several
environments, the identity is the workflow of the environment that built the
image, such as .github/workflows/opscrew-deploy-staging.yml: a promoted environment signs nothing
and checks its source’s signature. To accept any workflow of
your repository instead, replace --certificate-identity "…" with
--certificate-identity-regexp '^https://github.com/<Owner>/<Repo>/'.
Updates with Dependabot
Section titled “Updates with Dependabot”OpsCrew pins every image and tool it uses to an exact version, and GitHub’s Dependabot keeps most
of those pins current. When your repository has no .github/dependabot.yml, the pull request that
npx opscrew provision opens with the deployment setup adds one, together with the deploy workflow
and the Compose files it watches, so Dependabot’s first check finds them. It covers:
- your app’s packages (npm, pnpm and Yarn, Bun, pip, Poetry, Pipenv, uv, Bundler, Go modules, Composer, Cargo, NuGet, Maven or Gradle, whichever your app uses);
- the base images of the Dockerfile, when the image is built from one;
- the images in OpsCrew’s Compose files (
opscrew/runtime): Postgres, the cache, Caddy and the others; - the GitHub Actions in your workflows.
Dependabot checks weekly. Minor and patch updates come grouped, one pull request per ecosystem, so you get few of them. Major updates of your libraries come as separate pull requests. The Node.js, Python, Caddy and Postgres images get no major updates at all: a new major version can break your app’s libraries, and a new Postgres major cannot read the old one’s data. Those moves are yours (see Runtime versions).
When your repository already has a Dependabot configuration, OpsCrew leaves it as it is and lists
the entries it would add, in the output of npx opscrew provision and in that pull request.
When Dependabot updates a pin in a file OpsCrew generated, OpsCrew keeps the newer pin on the next
npx opscrew provision and does not put the old one back.
Dependabot cannot update the pins in the deploy workflow’s own build steps: the Buildpacks builder
and run images, Railpack’s frontend, the pack and Railpack versions, and Trivy. Those pins move with
OpsCrew’s releases: update OpsCrew and run npx opscrew provision to get the new ones.
Deploy tags
Section titled “Deploy tags”OpsCrew can tag each deploy that passes its health check. The tag is named after the time of the
deploy in UTC, such as deploy-20261018-142233, and points at the deployed commit, so you can see
what ran when and find the version to go back to. Optionally, each tag also gets a GitHub Release
that names the image’s digest. A deploy that fails gets neither.
Tags are off by default. Turn them on in the interview, or in opscrew/plan.yaml:
cicd: deployTags: enabled: true release: true # also a GitHub Release with the image digestthen run npx opscrew provision. Only the tagging step may write to your repository. Tags made by
the deploy start no other workflow, so do not build anything that waits for them.
Old images are cleaned up
Section titled “Old images are cleaned up”Each deploy adds an image to GitHub Container Registry. After each healthy deploy, OpsCrew keeps the newest 10 deployed images, with their SBOM, provenance and signature, and deletes older ones. It never deletes the image that is running or the one before it, so the previous version stays available. Images you tagged yourself are kept too. The intermediate images that Buildpacks and Railpack builds leave behind are always deleted. Each environment’s deploy deletes only the images its own builds made, so it never deletes another environment’s, and each environment keeps its newest 10 however many the others build. Leftover parts of images with no tag are deleted only once they are 30 minutes old, so an image another environment is pushing at that moment is never touched.
Each healthy deploy also labels the image its environment now runs
opscrew-<env> (opscrew-prod for prod) and the one before it opscrew-<env>-previous.
These tags only label the images; deploys always name the digest. Cleanup never deletes an image
with such a label, so every environment keeps what it runs and what it ran before, which
promotion and rollback use. Cleanup finds the app’s environments by
their deploy workflows in the repository: once an environment is removed and its workflow is gone,
its labels keep nothing, and neither does any other opscrew- tag that names no environment.
To keep more or fewer, set cicd.imageRetention.keep (2 to 100) in opscrew/plan.yaml. To keep
every deployed image, set cicd.imageRetention.enabled: false. Then run npx opscrew provision.