Docker From PR to Production
Tracking a Docker Image Journey
In a [recent article]({{ < ref “post/2025/04/trivy-codespaces-ci/index.md” > }}), I wrote about creating a CodeSpaces container, using a Trivy-powered process to assess security.
At the end, I mentioned flaws - I was building an image in the PR, running the checks, and then in the deployment and tagging workflows, I was building the image again.
Sure, it’s the same source. It should be the same.
“Should” is my favourite word in IT.
Can we do better? Why not take the same image we built in the PR, and promote that to Docker Hub?
Further, when we’re ready to release, let’s take the appropriate SHA, and make sure that’s the version that gets tagged on GitHub.
PR Builds: One Image, tracked from the Start
Every pull request triggers a GitHub Actions workflow that:
- Builds the image, tagging it with both the commit SHA and the PR number.
- Runs a Trivy scan, posting comments on the PR (which I wrote about in my last article).
- Pushes to GHCR, so it’s available for later workflows.
- Cleans up old images: anything SHA-tagged from previous builds of the same PR gets removed.
It’s similar to the workflow I wrote about in my [last article]({{ < ref “post/2025/04/trivy-codespaces-ci/index.md” > }}), and you can see it here:
az-pwsh-terraform/.github/workflows/docker-pr-build-and-scan.yml
Promotion: Deploy Means Promote, Not Rebuild
Once the PR is merged onto main, the deploy workflow steps in.
Here’s the flow:
- The workflow uses the PR number associated with the merge commit to find and pull the PR-tagged image from GHCR.
|
|
- We scan it again with Trivy and upload results to GitHub’s code scanning UI.
|
|
It retags it as latest and pushes to Docker Hub.
Then we clean up the SHA-tagged version, keeping the staging (GHCR) registry lean.
Crucially, you’ll note that we do not rebuild the image. Due to how GitHub Actions handles PR builds, the commit SHA during the PR isn’t guaranteed to match the final merge commit — that’s why we associate images via PR number instead.
Link to workflow: /.github/workflows/docker-deploy.yml
Release Tags: Same Image, Just Versioned
Tagging releases provides a clear signal that an image is production ready. Combined with healthy use of semantic versioning, it makes it safer for consumers than always pulling ’latest'.
When we push a release tag (e.g., v1.2.3):
- The same SHA-tagged image is pulled again.
- It gets version-tagged and re-pushed to Docker Hub.
- And once again, we clean up the SHA version.
At no point does anything get rebuilt after the PR.
Workflow: /.github/workflows/docker-tag-release.yml
Why This Matters
In CI/CD, trust is earned through consistency. Building once, promoting with confidence — that’s how ‘should’ becomes ‘is’.
By building the image once in the PR and reusing it across every stage, we ensure that what gets reviewed is exactly what gets deployed.
From the moment a PR is opened to the moment a new version is released, the same Docker image is carried through every stage, ensuring consistency and reliability.
Want to check it out?
If you’re curious about the details, you can check out all the workflows on GitHub.
The pre-built container is also available on Docker Hub.
The scripts that do the tidy up were a bit curly, so I made separate actions for them.
How do you manage Docker images in your CI/CD pipelines?
I’ve focused here on promoting a single build for maximum traceability — but I’m always interested to hear other approaches. Let me know your thoughts in the comments!