Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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:

  1. Source — GitHub triggers the workflow on push or pull_request.
  2. Build station — The Action checks out code, logs into a container registry, and runs docker build.
  3. Test station — The Action runs unit tests or integration tests inside a container.
  4. Ship station — The Action pushes the tagged image to Docker Hub or GitHub Container Registry.
  5. 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@v4 to fetch your source.
  • Uses docker/login-action to 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 run to 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), use GITHUB_TOKEN instead 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-action is 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@v4 before building — the build context is empty without checkout.
  • Hardcoding credentials in workflow YAML instead of using GitHub Secrets.
  • Pushing a latest tag on every commit without a unique identifier like commit SHA, making rollbacks impossible.

Variations

  1. Use docker/compose-action for workflows that need to spin up multiple dependent services during integration tests.
  2. Deploy to a Kubernetes cluster by adding a kubectl step after pushing the image.
  3. Trigger the pipeline on scheduled intervals (e.g., weekly rebuilds) using on.schedule with 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@v4 first to fetch your source code.
  • Leverage docker/build-push-action for built-in caching, tagging, and registry login.
  • Store registry credentials as GitHub Secrets; use GITHUB_TOKEN for 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.