Docker in CI/CD Pipelines
The same container that runs a build, the same container that runs the tests, the same container that ships to production — Docker turns "it works on my machine" into an artifact everyone actually agrees on.
Continuous integration and continuous delivery are built around a simple promise: every change gets automatically built, tested, and made ready to ship, with as little manual intervention as possible. That promise only holds up if the environment doing the building and testing genuinely matches the environment the application will actually run in later. Without containers, CI pipelines have historically struggled with exactly this — a build agent’s installed tool versions, operating system quirks, or leftover state from a previous job can all cause a pipeline to behave differently than a developer’s own machine, or differently than production. Docker addresses this directly: instead of running builds and tests loosely on a shared CI machine, the pipeline builds and runs everything inside a container defined by the project itself, and that same container image can be pushed onward, unchanged, all the way to production. This guide covers what that actually looks like in practice, how a typical pipeline is structured around Docker, and the patterns that keep it fast, reliable, and secure.
ACore Concepts
The core idea: the image is the artifact
In a Docker-centered pipeline, the thing that gets built, tested, and ultimately deployed is not a loose collection of files — it is a single, versioned container image. That image becomes the pipeline’s actual output, the same way a compiled binary or a packaged application might be in a more traditional build process.
Why this solves the “works on my machine” problem
Because the image bundles the application together with its exact runtime environment — language version, system libraries, installed dependencies — the environment used to test it in CI is not just similar to the environment it will run in later, it can be made functionally identical. This removes an entire category of failures that only show up because two environments quietly differed in some small way nobody accounted for.
Build once, promote everywhere
A well-designed Docker-based pipeline builds a single image early on, then reuses that exact same image through every later stage — testing, staging, production — rather than rebuilding a fresh image at each stage. This “build once, promote everywhere” principle is what actually delivers on the promise that what was tested is what gets deployed.
Think of a traditional, non-containerized pipeline like baking a fresh cake separately for the taste test, the photo shoot, and the actual wedding — three different bakes that hopefully turn out the same, but sometimes do not. A Docker-based pipeline is like baking one single cake, tasting a slice of that exact cake, photographing that exact cake, and then serving that exact cake at the wedding. Nothing was ever recreated from scratch along the way, so there is no chance of the final version differing from what was already approved.
BArchitecture & Components
Where the pipeline actually executes
The machine or ephemeral environment that runs pipeline steps, which itself needs the ability to build and run Docker containers as part of its job.
The build definition
The same Dockerfile a developer might use locally, now serving as the authoritative, version-controlled definition of exactly what gets built in the pipeline.
Where the image comes to life
The pipeline step that runs the actual image build, producing a tagged, versioned image that later stages will reuse without rebuilding it.
Where the image proves itself
The step that runs the application’s test suite inside a container started from the freshly built image, verifying correctness in an environment matching what will actually ship.
Where the artifact is stored
A central location the built, tested image is pushed to, and later pulled from by whatever deployment step or environment needs to run it.
Where the artifact ships
The final step that pulls the specific, already-tested image from the registry and runs it in staging or production, without rebuilding anything at this point.
The relationship between these pieces is what gives the whole pipeline its integrity: the build stage produces one image, every later stage refers back to that exact same image by its specific tag, and nothing downstream ever silently swaps in a different build.
CInternal Working
When a change is pushed to the project’s source repository, the CI system detects it and starts a new pipeline run on a runner. The build stage checks out the code and runs a Docker build using the project’s Dockerfile, producing a new image tagged in a way that uniquely identifies this specific run — often using the source commit’s identifier, so the image can always be traced back to the exact code that produced it.
How the CI runner gets the ability to build and run Docker itself
Because the runner needs to build images and start containers as part of the pipeline, it needs its own access to a Docker engine. This is commonly achieved either by giving the runner access to the host machine’s own Docker engine, or by running a separate, isolated Docker engine specifically for that pipeline job — an approach generally referred to as Docker-in-Docker. Each approach comes with different trade-offs around isolation and security, covered further in a later chapter.
How testing and promotion actually happen
Once the image is built, the test stage starts one or more containers from that exact image and runs the test suite inside them, capturing the results. If the tests pass, the pipeline pushes that same image to a registry under its permanent tag. Later stages — a staging deployment, a production deployment — pull that specific image by its exact tag rather than building anything new, which is what guarantees the artifact being deployed is truly identical to the one that was tested.
A pipeline that rebuilds a fresh image at every stage only ever verifies that a similar image passes each check — it never actually guarantees the deployed image is the same one that was tested. Build once and promote that exact image through every stage to close that gap entirely.
DData Flow & Lifecycle
flowchart TD
A["Developer pushes commit abc123"] --> B["CI pipeline triggers automatically"]
B --> C["Build stage: docker build, tag image as app:abc123"]
C --> D["Test stage: container started from app:abc123 runs test suite"]
D --> E{"Tests pass?"}
E -->|"No"| F["Pipeline fails, image not promoted further"]
E -->|"Yes"| G["Image app:abc123 pushed to registry"]
G --> H["Staging deployment pulls app:abc123 from registry"]
H --> I["Staging verification passes"]
I --> J["Production deployment pulls the same app:abc123"]
J --> K["Production runs the exact image that was built and tested"]
Fig. 1 — One image, built once and tagged to the commit that produced it, moves through every later stage unchanged — nothing is rebuilt between testing and production.
Notice that the commit identifier travels with the image the entire way through the diagram. If a problem is ever discovered in production, tracing it back to the exact source code that produced that image is immediate — there is no ambiguity about which version of the code is actually running, because the image tag says so directly.
Rolling back becomes trivial
Because every previously deployed image remains available in the registry under its own permanent tag, rolling back a bad deployment is often as simple as redeploying the previous known-good tag, with no rebuild required and no risk of accidentally deploying a slightly different version than what was actually running before.
EAdvantages, Disadvantages & Trade-offs
Strengths
- Eliminates most environment-mismatch failures between testing and production by using the same container both places
- Produces a single, traceable, versioned artifact that can be rolled back to instantly without rebuilding
- Keeps CI runners themselves clean and general-purpose, since the actual build environment lives inside the Dockerfile, not on the runner
Limitations
- Building images adds time to every pipeline run unless layer caching is set up deliberately and correctly
- Giving CI runners the ability to build and run Docker containers introduces real security considerations, especially around privileged access
- Registry storage and image transfer add operational overhead that a non-containerized pipeline would not have
The trade-off in one sentence: Docker-based pipelines trade some added build infrastructure and security surface for a dramatic increase in confidence that what was tested is genuinely what gets deployed.
FSecurity
A CI runner that has full, unrestricted access to build and run Docker containers effectively has a great deal of power over the host it runs on, particularly in setups where the runner shares the host’s own Docker engine directly. If a malicious or compromised pipeline step can build and run arbitrary containers with elevated privileges, it can potentially reach far beyond the intended scope of that one pipeline job.
Running an isolated Docker engine inside a pipeline job (Docker-in-Docker) commonly requires privileged access, which grants that job significantly broader system capabilities than it would otherwise need. Teams that rely on this approach should treat it as a meaningful security trade-off, and many organizations prefer rootless or more tightly scoped alternatives specifically to avoid running pipeline jobs with full privileged access by default.
Beyond runner privileges, secrets used during the build (private registry credentials, internal package tokens) need to be injected carefully so they never end up baked into an image layer that gets pushed to a registry, and every image, once built, benefits from an automated vulnerability scan before it is trusted for deployment — catching known, fixable issues in dependencies before they ever reach production. Treating the pipeline itself as part of the application’s attack surface, not just a background convenience, is the mindset that keeps a Docker-based CI/CD setup genuinely safe.
GDesign Patterns & Anti-patterns
Pattern
Tagging every built image with something permanent and traceable, most commonly the source commit identifier, rather than only a generic, reused tag.
Why it works
It makes every deployed image immediately traceable back to the exact code that produced it, which is invaluable during incident investigation and makes rollback to a specific known-good version straightforward.
Pattern
Using multi-stage Dockerfile builds so the final, deployed image contains only the compiled application and its runtime dependencies, not the full build toolchain used to produce it.
Why it works
It keeps the production image smaller, faster to pull and start, and free of build-time tools that offer no runtime value but do expand the image’s potential attack surface.
Anti-pattern
Rebuilding a fresh image at every pipeline stage instead of building once and promoting that same image forward.
Why it causes problems
It reintroduces exactly the environment-mismatch risk Docker was meant to eliminate — a rebuild at deploy time could differ subtly from the one that actually passed testing, even from the same source code, if anything about the build environment changed in between.
Anti-pattern
Deploying images only by a floating, reused tag such as “latest,” with no permanent, specific version tag tracked anywhere.
Why it causes problems
It makes it impossible to know with certainty which exact build is currently running in a given environment, and it makes a clean rollback to a specific prior version far harder, since “latest” always points at whatever was pushed most recently.
HBest Practices & Common Mistakes
Set up build layer caching deliberately
Structuring a Dockerfile so rarely-changing steps (installing dependencies) happen before frequently-changing steps (copying source code) lets CI reuse cached layers and build noticeably faster.
Scan images for known vulnerabilities automatically
Running an automated scan as a standard pipeline step catches known, fixable dependency issues before an image is trusted for deployment.
Keep the deployed image minimal
A smaller image pulls faster, starts faster, and offers less surface area for something to go wrong or be exploited.
Granting broad, unrestricted Docker access to every pipeline job
Scope Docker access to only the jobs that genuinely need to build or run containers, rather than making it a default, blanket capability.
Baking build-time secrets into the final image
Credentials used only to fetch private dependencies during the build should never persist into the shipped image layers — use build-time secret mechanisms designed to avoid this.
Skipping test stages “to save time”
A fast pipeline that skips testing an image directly defeats the entire point of building the image in the first place — speed gains belong in caching and parallelization, not in skipped verification.
IReal-World & Industry Examples
Popular CI platforms
Widely used continuous integration platforms including GitHub Actions, GitLab CI, and Jenkins all provide first-class, well-documented support for building and pushing Docker images as a standard pipeline step, reflecting how central this pattern has become.
Microservice-heavy organizations
Companies running many independently deployed services, including well-known technology companies with large-scale microservice architectures, rely heavily on this exact build-once-promote-everywhere pattern to keep dozens or hundreds of services deployable independently and reliably.
Open-source project release pipelines
Many open-source projects automatically build and publish a tagged Docker image for every release directly from their CI pipeline, giving downstream users a consistent, verifiable way to run the exact version that was tested and released.
Regulated industries requiring traceability
Organizations in finance and healthcare, where audit trails matter, often specifically value the traceable, commit-tagged image pattern because it provides a clear, provable link between deployed software and the exact source code that produced it.
JFrequently Asked Questions
KSummary and Key Takeaways
The essentials, in one place
- The image is the pipeline’s actual artifact, not a loose collection of files or scripts.
- Build once, promote everywhere — the same image tested in CI should be the exact image that reaches production.
- Traceable, permanent tags (like a commit identifier) make deployed software instantly traceable back to its source and simplify rollback.
- Giving CI runners Docker capability is a real security decision, especially around privileged Docker-in-Docker setups.
- Layer caching and multi-stage builds are what keep Docker-based pipelines fast and images lean, not brute-force rebuilding.
- Secrets used during the build must never persist into the final image’s layers.
- Docker eliminates environment mismatch failures, but it does not replace the need for genuinely thorough application-level testing.