CI/CD Pipeline with Docker & Actions
Build a CI/CD pipeline using Docker and GitHub Actions. This lesson walks through creating automated builds, tests, and deployments with containerized workflows in a hands-on exercise.
Focus: build ci/cd pipeline with docker and github actions
You have a working Dockerized application, but every deployment still starts with a manual docker build on your local machine. That fragile workflow breaks on busy release days, introduces configuration drift, and erodes trust in your release process. It is time to automate—building a CI/CD pipeline with Docker and GitHub Actions eliminates human error from image builds and deliveries, turns your repository into a deployment authority, and gives you a single pane of glass for every release.
The problem this lesson solves
Manual container builds waste time and introduce risk. You forget a tag, build against a stale base image, or push a broken image to production. Without automation, teams fall back to arcane scripts, undocumented build steps, and ad-hoc testing. The result is 'works on my machine' amplified across every environment.
A CI/CD pipeline fixes this by codifying your build, test, and deploy steps inside a source-controlled workflow file. GitHub Actions provides the orchestration; Docker provides the portable build and runtime environment. Combined, they give you repeatable, auditable, and fast container releases.
Core concept / mental model
Think of your CI/CD pipeline as an assembly line for your Docker images. Your code is raw material that enters the line at a commit trigger. Each station runs a specific job:
- Source — GitHub triggers the workflow on
pushorpull_request. - Build station — The Action checks out code, logs into a container registry, and runs
docker build. - Test station — The Action runs unit tests or integration tests inside a container.
- Ship station — The Action pushes the tagged image to Docker Hub or GitHub Container Registry.
- Deploy station (optional) — The Action updates a staging or production environment (e.g., via SSH or Kubernetes manifest update).
Pro tip: The key insight: GitHub Actions runs inside a VM that has Docker installed. Your workflow file becomes the blueprint for the entire assembly line.
How it works step by step
1. Workflow file structure
A workflow is a YAML file stored at .github/workflows/. Each workflow has:
- Name — Human readable label.
- On — Trigger event (e.g.,
push,pull_request). - Jobs — One or more units of work.
- Steps — Individual commands inside a job.
2. Job anatomy for Docker builds
A Docker build job typically:
- Uses
actions/checkout@v4to fetch your source. - Uses
docker/login-actionto authenticate to your registry. - Builds the image with a dynamic tag (e.g., commit SHA or Git tag).
- Pushes the image to the registry.
The build step can use raw docker build commands or the docker/build-push-action for extra features like caching.
3. Testing inside the pipeline
You can run tests in two ways:
- Inline tests — Use
docker runto execute tests against your built image. - Separate test job — Build the image, push it as a test artifact, and have a test job pull and run it.
4. Registry authentication
Store your Docker Hub or GitHub Container Registry credentials as GitHub Secrets (DOCKER_USERNAME, DOCKER_PASSWORD) and reference them in the login step.
Pro tip: For GitHub Container Registry (
ghcr.io), useGITHUB_TOKENinstead of password—no secret management needed.
Hands-on walkthrough
Let's build a real pipeline for a Python Flask app. This example assumes your repository has a Dockerfile at root.
Example 1: Basic pipeline — build and push
Create .github/workflows/docker-build.yml:
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: mydockerhubuser/myapp:latest
Expected output: On every push to main, GitHub Actions checks out code, logs into Docker Hub, builds the image, and pushes mydockerhubuser/myapp:latest.
Example 2: Add testing with dynamic tags
Extend the workflow to run tests and tag by commit SHA:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build test image
run: docker build -t myapp-test .
- name: Run tests
run: docker run myapp-test pytest tests/
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: mydockerhubuser/myapp
tags: |
type=sha,format=short
type=ref,event=branch
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expected output: Tests run first. If they pass and the push is to main, the image gets tagged with the short commit SHA, branch name, and latest.
Example 3: Multi-stage build with cache
Improve build performance by caching layers:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build and push with cache
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: mydockerhubuser/myapp:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
Expected output: Second build on the same runner uses cached layers—significantly faster than a cold build.
Compare options / when to choose what
| Feature | docker/build-push-action |
Raw docker commands |
docker/compose-action |
|---|---|---|---|
| Ease of use | One step, handles tags & secrets | Manual step breakdown | Good for multi-container tests |
| Caching | Built-in (type=gha or local) | Manual cache management | Limited |
| Multi-platform | Supports platforms input |
Requires Buildx setup | Not supported |
| When to use | Standard single-image builds | Complex or custom build logic | Integration tests with Compose |
Pro tip: For most CI/CD pipelines,
docker/build-push-actionis the best choice—it handles authentication, tagging, and caching out of the box.
Troubleshooting & edge cases
Workflow doesn't trigger
Symptom: Push doesn't start the workflow.
Fix: Check your branch filters in on.push.branches. Also ensure the file is in .github/workflows/ and named with .yml extension.
Docker login fails
Symptom: Error: Username and password required.
Fix: Verify your GitHub Secrets (repo → Settings → Secrets and variables → Actions). Ensure the secret names match exactly case-sensitive.
Build succeeds but push fails
Symptom: Image built locally but not pushed.
Fix: Ensure push: true is set in docker/build-push-action. If using Docker Hub free plan, check you haven't exceeded pull rate limits.
Cache not working
Symptom: Second build doesn't use cache.
Fix: Make sure cache-from and cache-to point to the same path and the cache key includes a stable value like branch name, not just commit SHA.
What you learned & what's next
You now know how to build a CI/CD pipeline with Docker and GitHub Actions — from defining triggers and jobs to running tests, caching layers, and pushing images. You can explain the core idea: automate every container build and push inside a version-controlled workflow file. You completed a practical exercise that builds, tests, and delivers a Docker image on every commit.
Next, you will explore deploying your containerized application to a cloud provider or introducing environment-specific configurations using GitHub Actions environments. Your assembly line is ready—now it needs a destination.
Practice recap
Open your project with a Dockerfile. Create .github/workflows/ci.yml with a simple build-and-push workflow. Add a test job that runs your unit tests. Push to a branch on GitHub and watch the Action run. Then add a latest and commit-sha tag using docker/metadata-action.
Common mistakes
- Forgetting to add
actions/checkout@v4before building — the build context is empty without checkout. - Hardcoding credentials in workflow YAML instead of using GitHub Secrets.
- Pushing a
latesttag on every commit without a unique identifier like commit SHA, making rollbacks impossible.
Variations
- Use
docker/compose-actionfor workflows that need to spin up multiple dependent services during integration tests. - Deploy to a Kubernetes cluster by adding a
kubectlstep after pushing the image. - Trigger the pipeline on scheduled intervals (e.g., weekly rebuilds) using
on.schedulewith cron syntax.
Real-world use cases
- A team rebuilds and redeploys a microservice to AWS ECS on every PR merge, ensuring staging always matches main.
- An open-source project uses GitHub Actions + Docker to build and push multi-architecture images (amd64, arm64) for every release tag.
- A security team scans every new image with Trivy inside the pipeline before pushing to a private registry.
Key takeaways
- A CI/CD pipeline with Docker and GitHub Actions automates build, test, and push steps via a YAML workflow file.
- Always use
actions/checkout@v4first to fetch your source code. - Leverage
docker/build-push-actionfor built-in caching, tagging, and registry login. - Store registry credentials as GitHub Secrets; use
GITHUB_TOKENfor ghcr.io. - Test your code in a separate job or inside the build job using
docker run. - Cache Docker layers across workflow runs to dramatically reduce build times.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.