Build Artifacts and Docker Images in CI
Once tests pass, most pipelines produce something deployable, a compiled binary, a bundled frontend, or, very commonly today, a Docker image. This lesson builds and pushes one from CI.
Building and pushing an image
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to registry
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
run: |
docker build -t myregistry/web:${{ github.sha }} .
docker push myregistry/web:${{ github.sha }}
secrets.REGISTRY_USER/REGISTRY_TOKEN pull encrypted repository secrets into the workflow at runtime, never hardcode credentials directly in the YAML (covered in more depth in the secrets-management lesson).
Tagging strategy
Tagging every image build matters more than it looks: it's how you know exactly what's running, and how you roll back.
| Tag style | Example | Tradeoff |
|---|---|---|
| Commit SHA | web:a1b2c3d | Always unique, traces directly to exact code, not human-friendly |
| Semantic version | web:1.4.2 | Human-friendly, requires a versioning/release process |
latest | web:latest | Convenient, but ambiguous, "latest" changes meaning over time, avoid for production deploys |
A common, robust approach: always tag with the commit SHA (guaranteed unique, always traceable), and additionally tag stable releases with a semantic version for human readability.
Uploading build artifacts
Not every pipeline needs a Docker image, sometimes you just need to pass a build output (a compiled bundle, a test report) from one job to another:
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# in a later job:
- uses: actions/download-artifact@v4
with:
name: dist
NOTE
Artifacts uploaded with upload-artifact are how you pass files between jobs in the same workflow run (each job gets its own fresh runner with no shared filesystem), it's not for long-term storage, that's what a registry or object storage (like S3) is for.