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.
