On the telco agent apps at DPL, releases were where things went wrong. Deployments failed, took too long, and sometimes shipped issues that had never appeared in testing and only surfaced at runtime in production. I automated the pipeline with Docker and Jenkins, and the change that mattered most was simple to state: the image that passes UAT is the image that goes to production. Deployment errors fell by 99%.
Rebuilding is the bug
If every environment gets its own build, every environment gets its own artifact. Even from the same commit, a rebuild can pull a newer base image, resolve a dependency differently, or bake in a different setting. The build that UAT signed off and the build that reached production were never the same thing, and "it worked in PreProd" stopped meaning anything.
Make PreProd a replica of production
Promoting an image only works if the place it was tested looks like the place it's going. Our PreProd was a replica of production, so a UAT sign-off there was a sign-off for production too. If PreProd drifts, UAT is testing a different system, and promotion just moves the surprise later.
Promote, don't rebuild
The pipeline builds once and pushes the image with an identity that never changes. After UAT is signed off on PreProd, promotion is only a new tag on that same image:
# Build stage: build and push once, tagged with the build number
docker build -t registry.example.com/agent-api:build-$BUILD_NUMBER .
docker push registry.example.com/agent-api:build-$BUILD_NUMBER
# Promotion stage, after UAT sign-off on PreProd: no build, just a new tag
docker pull registry.example.com/agent-api:build-$BUILD_NUMBER
docker tag registry.example.com/agent-api:build-$BUILD_NUMBER \
registry.example.com/agent-api:release-$RELEASE_VERSION
docker push registry.example.com/agent-api:release-$RELEASE_VERSION
Production then deploys release-$RELEASE_VERSION, which points at exactly the bytes UAT tested. Nothing is compiled between sign-off and release, so nothing new can sneak in.
Tags are the release record
- One immutable identity per build. The build tag is written once and never moved.
- Release tags are names, not new builds. They make it obvious what's running, and rolling back is deploying the previous release tag.
- Never reuse a tag. Moving a tag to a different image quietly rewrites history, which is the problem this whole setup exists to prevent.
Keep configuration out of the image
Build-once has one precondition: nothing environment-specific can be baked into the image. Connection strings, endpoints, and feature settings come from the environment at deploy time. If a setting forces a rebuild per environment, you're back to shipping untested artifacts.
What changed
Deployment errors dropped by 99%. Releases got faster, because there was nothing left to build at release time. And the runtime issues that used to appear only in production mostly stopped, because production was finally running the thing we had tested.