Week 12

Spring profiles

CI/CD Concepts

GitHub Actions

Docker in CI

Deploying from a Pipeline

Quality Gates

Practice

Assignment

Backend Track

The pipeline ships the box

Your pipeline now proves the code works. Next it should package it — because last week's promise is due: in the Container Registries chapter you pushed to Docker Hub by hand and read that GitHub Actions would log in to GHCR with a built-in token, no setup at all. Today is that day.

GITHUB_TOKEN: the secret you never create

Pushing to a registry needs authentication — on your laptop that was docker login with your account. On the runner, GitHub mints a fresh GITHUB_TOKEN for every single run: it belongs to your repository, it can do only what you allow, and it expires when the run ends. You never generate it, never store it, never rotate it.

You only allow it things. By default it may read your code; pushing a package needs one extra permission, declared on the job:

permissions:
  contents: read
  packages: write

The build-and-push job

Add a second job to ci.yaml. The commands inside are deliberately the same three you typed by hand last week — log in, build with tags, push:

build-and-push:
  runs-on: ubuntu-latest
  needs: test
  if: github.ref == 'refs/heads/main'
  permissions:
    contents: read
    packages: write
  steps:
    - uses: actions/checkout@v7
    - name: Log in to GHCR
      uses: docker/login-action@v4
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    - name: Build image
      run: >
        docker build
        -t ghcr.io/<your-username>/<your-repo>:latest
        -t ghcr.io/<your-username>/<your-repo>:${{ github.sha }}
        .
    - name: Push image
      run: docker push --all-tags ghcr.io/<your-username>/<your-repo>

The new pieces, one by one:

<aside> ⚠️

GHCR image names must be lowercase. If your GitHub username or repository has capital letters, write the image name in lowercase yourself (ghcr.io/hackyourfuture/shop-api, not ghcr.io/HackYourFuture/Shop-API) — otherwise the push fails with a confusing invalid reference format error.

</aside>

<aside> ❗

The rule from last week did not change: the image must contain no secrets. Configuration arrives as environment variables at runtime — the pipeline builds the same secret-free image you built by hand.

</aside>

Find your package

Merge the change to main and let the pipeline run. On your repository's front page, look for Packages in the right sidebar — your image is there, with both tags. This is GHCR: the image lives next to the code that produced it.

<aside> ⌨️

Hands on: open the package page and make the package public (Package settings → Change visibility). Your first push creates it as private; the next chapter's deployment needs to pull it without credentials. Same reasoning as last week: fine for a learning project that contains no secrets.

</aside>

💬 Question: the workflow logs in to a registry and pushes an image — yet nowhere in the repository is any password stored. Where does the credential come from, and why is this safer than the access token you used on your laptop?

In the wild

Many real pipelines use Docker's published actions instead of raw commands — docker/login-action, docker/build-push-action — which add conveniences like build caching and multi-platform images. They do exactly what your three commands do; you will recognise every part when you meet them.

Extra resources


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.