Skip to content
Laviyx Learn

Try

Docker and Kubernetes course menu

Build and Push Docker Images with GitHub Actions

Automate Docker builds in CI/CD: build on every commit, run tests in a container, push to GitHub Container Registry with commit tags, cache layers, scan images, and deploy.

Intermediate6 min readUpdated
On this page

In a real project, people do not build production images on their laptops. A CI pipeline builds, tests and publishes them automatically, so that each image can be traced to one commit.

The pipeline#

Text
git push --> build image --> run tests --> scan --> push to registry --> deploy

A GitHub Actions workflow#

This workflow builds on every pull request, and pushes to GitHub Container Registry when code reaches main. It needs no extra secret, because GitHub provides GITHUB_TOKEN:

.github/workflows/docker.yml (YAML)
name: Docker

on:
  push:
    branches: [main]
  pull_request:

env:
  IMAGE: ghcr.io/${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v7

      - uses: docker/setup-buildx-action@v4

      - name: Log in to the registry
        if: github.event_name == 'push'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: |
            ${{ env.IMAGE }}:latest
            ${{ env.IMAGE }}:sha-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The action versions change over time, so check each action's page for the latest one.

StepPurpose
setup-buildx-actionPrepares the modern builder
login-actionSigns in to the registry
push: ...Pull requests build only; main also pushes
sha-${{ github.sha }}Links the image to the exact commit
cache-from, cache-toReuses layers between runs, which makes builds much faster

Testing in the build#

A neat way to run tests is as a stage in the Dockerfile. If the tests fail, the build fails:

DOCKERFILE
FROM node:24-alpine AS test
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm test

FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

In CI, build the test stage first:

Terminal
docker build --target test -t app:test .

Testing with a database#

Compose can start the whole stack in CI:

YAML
      - run: docker compose up -d --build --wait
      - run: curl -f http://localhost:8000/health
      - run: docker compose logs
        if: failure()

--wait blocks until services are healthy.

Scanning#

Add a scan step, so that serious vulnerabilities stop the pipeline. Trivy provides an action, aquasecurity/trivy-action, with an option to fail on CRITICAL and HIGH results.

Deploying#

There are many ways to deploy an image. A simple one for a single server is SSH:

Terminal
ssh deploy@server "docker pull ghcr.io/your-name/app:sha-abc123 && docker compose up -d"

Other platforms pull the image for you: Kubernetes, AWS ECS, Google Cloud Run, Azure Container Apps, and many hosting services.

Rolling back#

Because each image has a unique tag, rolling back means deploying the previous tag. That is why you should not deploy latest.

Common mistakes#

Practice#

Practice exercise

In the workflow above, what stops a pull request from a fork pushing an image?

Show solution

The push value is true only for push events, so pull requests only build the image. The login step is also skipped, using if: github.event_name == 'push'.

Key takeaway#

Next: What Is Kubernetes?.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.