CI/CD Pipelines with GitHub Actions and Docker

Team 6 min read

#ci-cd

#docker

#github-actions

#devops

Introduction

Continuous Integration and Continuous Deployment (CI/CD) are core rituals in modern software delivery. When paired with Docker, you can ensure that every code change runs in a consistent, isolated environment, producing reproducible build artifacts and reliable deployments. This post walks through how to architect a CI/CD pipeline using GitHub Actions and Docker, from building Docker images to running tests and deploying to production.

In this guide, you’ll see practical patterns, sample workflow files, and considerations for security, reliability, and maintainability. Whether you’re shipping a web app, an API service, or a microservices stack, the core ideas translate across stacks.

Why GitHub Actions and Docker

  • Consistency across environments: Docker ensures the same runtime environment across CI, staging, and production.
  • Reproducible builds: Versioned images capture the exact dependencies and runtimes used to build and run your app.
  • Parallelism and speed: GitHub Actions can run build, test, and deploy steps in parallel or in sequence, speeding up feedback.
  • Easier collaboration: Centrally managed workflows live with your codebase, making CI/CD audit trails transparent.

The combination of GitHub Actions for automation and Docker for containerization provides a powerful, portable workflow that scales from small teams to large organizations.

Building Docker images in CI

A typical pipeline builds a Docker image from your application’s source, runs tests inside a container, and pushes the image to a registry. From there, deployments pull the image and rollout updates in your target environment.

Key patterns:

  • Use multi-stage Docker builds to minimize image size.
  • Pin dependencies by lockfiles (package-lock.json, Pipfile.lock, etc.).
  • Use Buildx for multi-arch builds and cache management.
  • Keep the Dockerfile lean and avoid shipping secrets in images.

Example Dockerfile (simple Node.js app):

# Use a small base image
FROM node:18-alpine

# Create app directory and copy code
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Expose port and run the app
EXPOSE 3000
CMD ["npm", "start"]

This Dockerfile demonstrates a straightforward approach: install only production dependencies, copy the application, and start the process. In production pipelines, you may choose to build using a multi-stage approach to separate build-time dependencies from runtime.

The CI/CD workflow at a glance

A robust CI/CD workflow typically includes:

  • Checkout and setup: Retrieve code, set up QEMU and Buildx if you need cross-architecture builds.
  • Build and test: Build the Docker image, run unit/integration tests inside containers.
  • Scan and verify: Optional security and dependency checks.
  • Publish: Push the image to a registry (GitHub Packages, Docker Hub, or a private registry).
  • Deploy: Update the target environment (staging/production) with the new image.
  • Observability: Emit logs and metrics, and ensure rollback mechanisms exist.

Below is a concrete GitHub Actions workflow outline that implements these steps in a generic fashion.

GitHub Actions workflow: a concrete example

This example shows a single workflow that builds a Docker image, runs tests, pushes the image to a registry, and deploys to a Kubernetes cluster. Adapt the steps to your stack (your registry, tests, and deployment targets).

Code: .github/workflows/ci-cd.yml

name: CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up QEMU for cross-arch (optional)
        uses: docker/setup-qemu-action@v2
        if: matrix.arch != null

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

      - name: Run unit tests inside container
        run: |
          docker run --rm ghcr.io/${{ github.repository }}:latest sh -c "npm test"
        env:
          CI: true

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v3
        with:
          version: 'v1.28.0'

      - name: Configure kubeconfig
        env:
          KUBECONFIG: ${{ secrets.KUBECONFIG }}
        run: |
          mkdir -p ~/.kube
          echo "$KUBECONFIG" > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: Deploy to Kubernetes
        run: |
          kubectl rollout status deployment/my-app -n production
          kubectl set image deployment/my-app my-app=ghcr.io/${{ github.repository }}:latest -n production

Notes:

  • Replace the registry and tags with your actual registry (GitHub Packages, Docker Hub, etc.).
  • Secrets such as GITHUB_TOKEN and KUBECONFIG must be configured in your repository settings.
  • The deploy step assumes you’re deploying to a Kubernetes cluster; adapt to your target (e.g., Docker Swarm, ECS, or plain servers with SSH).

Docker best practices in CI/CD

  • Use multi-stage builds to reduce final image size and surface area.
  • Leverage image caching by reusing layers when possible; pin dependencies via lockfiles.
  • Run tests inside the built image to validate the exact runtime environment.
  • Scan base images and dependencies for known vulnerabilities; fail the pipeline on critical findings.
  • Tag images with meaningful identifiers (e.g., version numbers, commit SHAs) rather than relying solely on “latest.”
  • Separate build and deploy privileges: the CI runner should have just enough permissions for the intended steps.

Deploy strategies and environments

  • Staging can mirror production to catch issues before release. Deploy new images to staging and run end-to-end tests.
  • Canaries and blue/green deployments reduce risk by gradually shifting traffic to updated versions.
  • Rollbacks should be automated: if the deployment fails health checks or end-to-end tests, revert to the previous image.
  • Feature flags can decouple release from code changes, enabling safer rollouts.

Observability and feedback

  • Include health checks in the container and monitor readiness and liveness probes in production.
  • Emit build and deployment metrics: duration, artifact sizes, success/failure rates.
  • Store build metadata (image digest, tag, build number) for traceability.
  • Use logs to diagnose failures quickly and reproduce issues locally.

Security considerations

  • Do not bake secrets into images. Use secret management and runtime injection (e.g., environment variables supplied at deploy time).
  • Regularly rotate credentials used in CI/CD workflows.
  • Keep your dependencies up to date and scan for vulnerabilities in base images and application layers.
  • Use minimal base images to limit attack surface.

Conclusion

GitHub Actions and Docker together offer a practical and scalable path to modern CI/CD. By building reproducible images, running tests inside controlled containers, and automating deployments to your chosen environment, you can reduce drift between environments and speed up reliable releases. Start with a lean workflow, protect secrets, and iterate on the pipeline by adding tests, security scans, and deployment strategies as your project grows.

If you’re new to this setup, start with the basics: a simple Dockerfile, a straightforward GitHub Actions workflow to build and push the image, and a basic deploy step. Then layer on security checks, multi-arch builds, canary deployments, and robust rollback procedures to mature your pipeline over time.