DEV Community

jasonmills94
jasonmills94

Posted on

Docker CI Releases Need a Verifiable AWS Receipt

A green deployment is not the same thing as a provable deployment. When a Docker release reaches Kubernetes or an ECS service, I want more than a successful job badge. I want a small receipt that answers: which source revision built the image, which digest was tested, and which environment received it?

This became especially useful when our release pipeline also exercised email flows. A test may create a temp mail inbox, trigger a notification, and then deploy an image. If the notification check fails later, a plain CI log makes it hard to tell whether the application, the container, or the fixture was responsible. A receipt gives the incident a fixed starting point.

The pattern below uses Docker, CI/CD, and an S3 bucket in AWS. It is intentionally simple. The receipt is a JSON object written once by the pipeline, then verified before promotion. It is not a replacement for deployment observability; it is the minimum evidence that makes a release replayable.

Why a successful deploy is not enough

Container tags are convenient, but a mutable tag such as latest is a weak audit record. Two builds can use the same tag while pointing at different image layers. A job log can also say “push complete” while a later step deploys an older digest from a cached variable.

The first lesson were simple: record the immutable values at the boundary where they are known. That means the Git commit, Docker image digest, test result, and deployment target should travel together. If the release is promoted again, the next receipt should have a new id rather than overwriting the old one.

I also keep test fixture details separate from credentials. For example, an engineer may type temp gamil com while searching for a disposable test mailbox. That text can appear in a test description, but it should never become a secret, an image tag, or a production configuration value.

The receipt contract

My release receipt has a narrow schema:

{
  "release_id": "2026-09-24T23:21:00Z-7f3c",
  "git_sha": "7f3c2d1",
  "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/orders@sha256:...",
  "tests": "passed",
  "environment": "staging",
  "deployed_at": "2026-09-24T23:22:00Z"
}
Enter fullscreen mode Exit fullscreen mode

There are no secrets in it. The image uses an ECR digest, not only a tag. release_id is unique and can be used to find the CI run, Docker build logs, and AWS deployment event. The tests field can stay small at first; I usually add a link to the full report in a separate field rather than copying a large log into S3.

This contract is similar to keeping replayable email checks for automation separate from the system that sends mail. The useful part is the boundary: inputs and outputs are named before a retry starts. For worker-backed notifications, outbox leases for reliable workers apply the same idea to ownership and retries.

Write the receipt from Docker CI

The build job should capture the digest after pushing the image. Here is a shortened GitHub Actions example:

- name: Build and push image
  id: image
  env:
    IMAGE: ${{ steps.login-ecr.outputs.registry }}/orders
  run: |
    docker build --tag "$IMAGE:${GITHUB_SHA}" .
    docker push "$IMAGE:${GITHUB_SHA}"
    DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE:${GITHUB_SHA}")"
    echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"

- name: Write release receipt
  env:
    IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
    RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
  run: |
    jq -n \
      --arg release_id "$RELEASE_ID" \
      --arg git_sha "$GITHUB_SHA" \
      --arg image "$IMAGE_DIGEST" \
      '{release_id: $release_id, git_sha: $git_sha,
        image: $image, tests: "passed", environment: "staging",
        deployed_at: (now | todate)}' > release-receipt.json
    aws s3 cp release-receipt.json \
      "s3://my-release-receipts/$RELEASE_ID.json" \
      --content-type application/json
Enter fullscreen mode Exit fullscreen mode

The command is illustrative: authenticate Docker and AWS using the CI provider's identity mechanism, not long-lived keys in repository secrets. The S3 bucket should block public access, enable versioning, and use a lifecycle rule that matches the retention policy. Logs is not a retention policy by itself.

Store and verify it in AWS

I give the pipeline permission to write only to a prefix such as releases/, while the promotion job can read that prefix. A separate incident role can read receipts without being able to change them. Bucket versioning helps with accidental replacement, but it does not turn an overly broad IAM policy into a safe one.

Before promotion, the job reads the receipt and compares its digest with the digest in the deployment manifest:

aws s3 cp "s3://my-release-receipts/$RELEASE_ID.json" receipt.json
test "$(jq -r .image receipt.json)" = "$EXPECTED_IMAGE_DIGEST"
test "$(jq -r .tests receipt.json)" = "passed"
Enter fullscreen mode Exit fullscreen mode

If the comparison fails, stop the release. Do not patch the receipt manually and continue. That is the whole point of having evidence. A little more clear failure is cheaper than a fast promotion with the wrong image.

Failure modes worth monitoring

The common failures are predictable:

  • Missing digest: the push or metadata step did not complete, so promotion must not guess.
  • Mismatched digest: a cached tag, wrong registry, or stale manifest is being used.
  • Missing receipt: the writer job failed or the S3 prefix is wrong.
  • Passed tests, bad deployment: the receipt proves the artifact, but the runtime configuration or rollout is unhealthy.
  • Repeated release id: a retry reused an identifier and made the audit trail confusing.

I alert on the first three as pipeline failures. Runtime health belongs to service monitoring, but the receipt lets me correlate the two with much less manual work. When a job fail in the middle of a retry, the original receipt should remain available for comparison.

Q&A

Do I need S3 for this?

No. A durable CI artifact store works too. S3 is useful when AWS already owns the deployment boundary and IAM can enforce read/write separation.

Should I include email addresses in the receipt?

Only if there is a strong operational reason. For test mail, record an opaque fixture id or a redacted address. A free disposable email or temp mail inbox can be part of a test run without becoming a permanent release record.

What if the image is rebuilt during promotion?

Treat that as a new release. Create a new digest and a new receipt; do not keep the old release_id. Immutable evidence is more usefull than a tidy-looking dashboard.

The payoff is modest but real: when a Docker release goes wrong, I can identify the exact artifact and test boundary before opening five different logs. That makes AWS operations calmer, and it makes CI/CD retries something we can explain.

Top comments (0)