CodingNic

CI/CD Fundamentals

Building and Publishing a Docker Image

CI/CD Fundamentals 20 min read

Building and Publishing a Docker Image

Objectives

By the end of this lesson, you should be able to:

  • Extend a CI workflow with a second job that only runs after tests pass
  • Build and push a Docker image to a container registry automatically
  • Provide a workflow with credentials safely, using repository secrets

💡 Why this matters: Lesson 2 automated running Course 3’s tests. This lesson automates the next step, Module 3’s Dockerfile, built and published automatically, but only for code that already passed those tests, this is continuous deployment, built directly on top of continuous integration.

⚠️ A note on verification: as in the last lesson, GitHub Actions can’t execute inside this course’s own sandboxed tooling. The YAML below was validated with a real YAML parser for correctness, its actual execution reflects GitHub Actions’ stable, current, documented behavior. Push this to a real GitHub repository, with a real container registry, to see it run.

Extending the Workflow with a Second Job

text
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  build-and-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

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

needs and if, Controlling When a Job Runs

needs: test means build-and-push only starts once the test job has finished successfully, if tests fail, this job never runs at all, a broken build is never published. if: github.ref == 'refs/heads/main' restricts it further, only running on pushes directly to main, not on every pull request, publishing an image for every single draft pull request would be wasteful, and often not what’s actually wanted.

Where the Credentials Come From

${{ secrets.GITHUB_TOKEN }} is a token GitHub provides to every workflow run automatically, scoped to that repository, no manual setup needed for pushing to GitHub’s own container registry (ghcr.io). For a different registry, Docker Hub, for instance, a real secret would need to be added under the repository’s Settings → Secrets, then referenced the exact same way, ${{ secrets.DOCKERHUB_TOKEN }}, never hardcoded into the workflow file itself, exactly Module 1’s secrets-management principle, applied to a CI/CD pipeline instead of an application.

Tagging the Image with the Commit SHA

tags: ghcr.io/${{ github.repository }}:${{ github.sha }}, every build is tagged with the exact commit it was built from, github.sha, this makes every published image traceable back to precisely the code that produced it, useful the moment something needs to be rolled back to a known-good, specific version, rather than an ambiguous “latest.”

What This Achieves End to End

A push to main now triggers, automatically: tests run, if they fail, nothing else happens, if they pass, a Docker image is built from the current code and pushed to a registry, tagged with the commit that produced it, a deployment platform (Module 5) can then be configured to pick up that new image automatically, completing the path from a pushed commit to a live deployment, no manual steps in between.

Try It

  1. Extend a working CI workflow (Lesson 2) with this build-and-push job, and confirm, using needs, that it only starts after test succeeds.
  2. Change if: github.ref == 'refs/heads/main' to also allow a develop branch, and explain what that change would mean for how often images get published.
  3. Explain, in one or two sentences, why tagging an image with github.sha is more useful for a real deployment than only tagging it latest.
  4. Explain, in your own words, why ${{ secrets.GITHUB_TOKEN }} never appears as a literal value anywhere in the workflow file itself.

Recap

  • needs and if control exactly when a job runs, here, only after tests pass, and only on pushes to main.
  • Docker images are built and published automatically, using repository secrets for registry credentials, never hardcoded.
  • Tagging each image with the commit SHA that produced it makes every deployment traceable back to exact, specific code.

Next lesson: exercises, building a complete CI/CD workflow from scratch.