advanced~4h

CI/CD with Docker: Jenkins, GitHub Actions & GitLab CI

CI/CD pipelines automate the journey from source code commit to a running container in production: build the image, test it, scan it, push it to a registry, and deploy it. Docker is the packaging form

CI/CD pipelines automate the journey from source code commit to a running container in production: build the image, test it, scan it, push it to a registry, and deploy it. Docker is the packaging format that makes each of these steps reproducible and environment-independent.

The three platforms covered here — Jenkins (self-hosted, highly extensible, the long-standing enterprise default), GitHub Actions (managed, YAML-based, tightly integrated with GitHub repos), and GitLab CI (managed or self-hosted, YAML-based, tightly integrated with GitLab) — share the same underlying Docker concepts even though their syntax differs.

The hardest recurring problem across all three platforms is the same: how does the CI runner itself get access to a Docker daemon to build images? The two standard answers are Docker-in-Docker (DinD) and socket mounting, each with different trade-offs.

  • A typical pipeline stage sequence: checkout source, build image (docker build), run unit/integration tests (often inside the built image or against a Compose stack), scan the image (Trivy/Grype, Chapter 7), tag and push to a registry, then trigger deployment (update a Kubernetes manifest, run a deploy script, or notify a GitOps tool).

  • Docker-in-Docker (DinD) runs an entirely separate, nested Docker daemon inside the CI job's own container, fully isolated from the host's daemon — cleaner isolation, but typically requires the job's container to run privileged, and loses any build cache from previous runs unless explicitly persisted via a registry cache.

  • Socket mounting (-v /var/run/docker.sock:/var/run/docker.sock) gives the CI job direct access to the host's existing Docker daemon — faster (shares image cache with the host) and avoids needing privileged mode, but means the build runs at the same trust level as the host's daemon, which is a real security consideration on shared/multi-tenant runners.

  • GitHub Actions provides a managed docker/build-push-action and native BuildKit cache integration via GitHub's Actions cache backend (cache-from/cache-to: type=gha), letting builds reuse layers across workflow runs without managing your own cache infrastructure.

  • GitLab CI's shared runners come with Docker pre-available either via the docker:dind service (classic DinD pattern) or via kaniko/buildah as rootless, daemon-less alternatives that avoid privileged-mode entirely.

  • Jenkins typically uses the Docker Pipeline plugin (docker.build(), docker.image().push()) or simply shells out to the docker CLI within a sh step, with the underlying daemon access pattern (socket-mounted agent, dedicated Docker-enabled build nodes, or Kubernetes-based dynamic agents) configured at the Jenkins infrastructure level rather than per-pipeline.

  • Developer pushes a commit or opens a pull request, triggering the configured pipeline webhook/trigger.

  • The pipeline checks out the source code onto the CI runner/agent.

  • The pipeline builds the Docker image using the repository's Dockerfile, tagging it with a unique, traceable identifier (commit SHA, build number) rather than a floating tag.

  • Automated tests run, either inside the built image directly or against a Compose stack spun up for integration testing, with the pipeline failing fast on any test failure.

  • The built image is scanned for known vulnerabilities (Chapter 7); the pipeline blocks progression on Critical/High severity findings per the team's policy.

  • On success, the image is pushed to the configured registry (ECR, GHCR, Docker Hub, Harbor) under its unique tag.

  • A deployment step (or a separate pipeline triggered by the registry push) updates the running environment — applying a new Kubernetes manifest, running docker compose pull && docker compose up -d, or notifying a GitOps controller (ArgoCD/Flux) of the new image tag to reconcile.

  • A Spring Boot microservices repo where every PR triggers a build, test, and scan pipeline, and merges to main additionally push to the registry and trigger a staging deployment automatically.

  • Multi-architecture image builds (amd64 + arm64) using docker buildx in CI for teams supporting both x86 cloud instances and ARM-based developer machines or ARM cloud instances (e.g., AWS Graviton).

  • Using GitHub Actions' built-in cache (type=gha) or GitLab's registry-based cache to make Docker builds in CI nearly as fast as local incremental builds, despite each job starting from a clean runner.

  • Jenkins pipelines orchestrating builds across many internal microservices repos using a shared, templated Jenkinsfile library, with Docker build/push/scan steps standardized centrally.

  • Always tag images by an immutable identifier (git commit SHA, build number) for anything that gets deployed — latest is for local convenience only, never for traceable production deployments.

  • Make the security scan step a blocking gate in the pipeline, not an informational-only report that nobody acts on.

  • Use registry-based or platform-native (GitHub Actions cache, GitLab's cache) layer caching so CI build times don't regress to full from-scratch builds on every single run.

  • Prefer daemonless build tools (Kaniko, Buildah) on shared/multi-tenant CI infrastructure to avoid the security implications of privileged Docker-in-Docker or host socket mounting.

  • Building and tagging only latest in CI, making it impossible to know which exact commit is running in any given environment, and making rollbacks ambiguous.

  • Mounting the host's Docker socket into CI jobs on shared runners without understanding this grants build jobs root-equivalent access to that host and to every other job's containers running on it.

  • Treating a failed security scan as a warning to silence rather than a blocking signal, eventually shipping known-critical vulnerabilities to production because the pipeline allowed it to continue anyway.

  • Not caching dependencies/layers between CI runs, causing every single pipeline execution to pay the full cost of a from-scratch image build.

  • Use multi-stage builds combined with CI-level caching so only the changed application layer rebuilds on most commits, mirroring the local development cache behavior from Chapter 3.

  • Parallelize independent pipeline stages (e.g., running unit tests and linting concurrently with the image build) where the CI platform supports it, rather than forcing a fully linear pipeline.

  • For multi-architecture builds, use docker buildx with QEMU emulation only when native ARM runners aren't available — native ARM CI runners are meaningfully faster than emulated cross-builds.

  • Adopt GitOps-style deployment (ArgoCD/Flux watching a Git repo of Kubernetes manifests) so the CI pipeline's job ends at 'push image and update manifest', with the actual cluster reconciliation handled by a dedicated, auditable controller rather than ad hoc deploy scripts.

  • Sign images (cosign/Notary) as part of the pipeline and verify signatures at deploy time, so only images that went through your actual CI pipeline can be deployed, closing a supply-chain gap.

  • Maintain separate, explicit promotion steps between environments (build once, promote the same image digest through dev, staging, and production) rather than rebuilding the image separately for each environment, which risks subtle drift between what was tested and what is deployed.

  • Write a GitHub Actions workflow that builds, scans (Trivy), and pushes an image tagged by github.sha, using type=gha caching.

  • Convert a socket-mounted CI build to use Kaniko instead, and compare the resulting security posture.

  • Add a blocking security-scan stage to an existing pipeline (Jenkins, GitHub Actions, or GitLab CI) and verify it actually fails the build on a known-vulnerable test image.

  • Set up registry-based BuildKit caching (--cache-from/--cache-to type=registry) for a multi-stage Dockerfile and measure the build time improvement on a no-op rerun.

  • CI/CD pipelines wrap docker build, test, scan, push, deploy into automated, repeatable stages.

  • Two Docker access patterns for CI runners: socket mounting (fast, host-trust) vs Docker-in-Docker (isolated, needs privileged mode) vs daemonless tools (Kaniko/Buildah, best for shared runners).

  • Always tag by immutable identifier (git SHA, build number) for anything deployed — never deploy by 'latest'.

  • Make security scanning a blocking CI gate, not an informational report.

  • Use platform-native or registry-based layer caching to keep CI build times fast across ephemeral runners.

Want a visual for this concept?

Generate a diagram tailored to “CI/CD with Docker: Jenkins, GitHub Actions & GitLab CI” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to From Docker to Kubernetes: What Actually Changes← Back to all Docker chapters