GitHub Actions Docker Integration

Docker packages your application and all its dependencies into a portable container image. GitHub Actions integrates with Docker at every step — building images, running containers, publishing to registries, and deploying containerized applications. This topic covers the most common Docker workflows teams use in production.

Why Use Docker with GitHub Actions

Without Docker, deploying an application means ensuring the target server has the right version of Node.js, Python, system libraries, and dozens of other dependencies. Docker packages everything the app needs into a single image that runs identically everywhere.

Traditional deployment:
  Server needs Node 20 → install it
  Server needs PostgreSQL client → install it
  Server needs specific system libs → install them
  App works on developer machine → might fail on server

Docker deployment:
  Build image (contains everything app needs)
  Push image to registry
  Server pulls image → runs it
  Same image = same behavior everywhere

Building a Docker Image in GitHub Actions

The standard workflow uses Docker's official GitHub Actions to build and push an image:

name: Build and Push Docker Image

on:
  push:
    branches: [main]

jobs:
  docker-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            myorg/my-app:latest
            myorg/my-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

What each action does

docker/setup-buildx-action  → Enables advanced Docker build features
docker/login-action         → Authenticates with a container registry
docker/build-push-action    → Builds the image and pushes it to the registry

Tagging Strategies

The tags field accepts multiple image tags. Teams commonly apply both a stable label and a unique identifier:

tags: |
  myorg/my-app:latest           ← Always points to newest build
  myorg/my-app:${{ github.sha }}← Unique tag per commit (immutable)

Using the commit SHA as a tag makes every image version traceable back to an exact code state. This is critical for debugging and rollback — you know exactly which code is running in any container.

Using GitHub Container Registry (GHCR)

GitHub provides its own container registry called GHCR. It integrates directly with your repository's permissions and costs nothing for public repositories.

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

- name: Build and push to GHCR
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

The GITHUB_TOKEN secret is automatic — GitHub creates it for every workflow run. No manual setup is required for GHCR authentication.

Docker Layer Caching

Docker builds are slow without caching. The cache-from and cache-to options in docker/build-push-action enable GitHub Actions-native build caching:

uses: docker/build-push-action@v6
with:
  cache-from: type=gha
  cache-to: type=gha,mode=max
Without cache:
  First build  → 4 minutes (downloads all base layers)
  Second build → 4 minutes (downloads same layers again)

With cache:
  First build  → 4 minutes (downloads layers, saves cache)
  Second build → 45 seconds (restores layers from cache)

Running Containers in a Workflow

You can run a Docker container as a service alongside your workflow steps — useful for testing against a real database or API:

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: runner
          POSTGRES_PASSWORD: password
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://runner:password@localhost:5432/testdb

The services block starts the PostgreSQL container before any steps run. The health check options tell GitHub to wait until the database is ready before proceeding. Steps connect to the service using localhost.

Building Multi-Platform Images

Modern applications often need to run on both x86 servers and ARM processors (like AWS Graviton or Apple Silicon). Build for both platforms in one step:

- name: Set up QEMU for cross-platform builds
  uses: docker/setup-qemu-action@v3

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

- name: Build multi-platform image
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: myorg/my-app:latest

Automatic Image Tagging with Metadata

The docker/metadata-action generates tags and labels automatically based on your Git state:

- name: Extract image metadata
  id: meta
  uses: docker/metadata-action@v5
  with:
    images: myorg/my-app
    tags: |
      type=ref,event=branch
      type=semver,pattern={{version}}
      type=sha

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}

This automatically creates tags like main, v2.3.1, and sha-abc1234 based on the current branch and any Git tags present — without writing tag logic manually.

Leave a Comment

Your email address will not be published. Required fields are marked *