Ravindra BagaleCourses & study guides मराठी

Chapter 52: Dockerfile Instructions Explained: FROM, RUN, CMD vs ENTRYPOINT, COPY vs ADD, Multi-Stage and BuildKit

52.13 GitHub Actions: build and push images to GHCR

Friends, building on your laptop is fine for learning, but real teams build images automatically on every push. Today we write a GitHub Actions workflow, step by step, that builds our image, tags it properly and pushes it to the GitHub Container Registry, ghcr.io.

1. Start from a story

At Zerodha, Meera's team pushed images by hand. Someone would build on a laptop, tag it latest and push. Twice a laptop image with debug settings reached staging.

Meera decided no human would push images any more. Every merge to main would build in CI, with the same Dockerfile, the same cache and a tag equal to the commit.

She used GitHub Actions because the code already lived there, and GHCR because the workflow can log in with its own short-lived token, so no password is stored anywhere.

Within a week every image in staging could be traced back to one commit. Today we build the same pipeline.

git push to mainworkflow startscheckout + buildxbuilder readylogin with GITHUB_TOKENshort-livedbuild, tag, pushghcr.io/owner/app
Picture 1: from push to registry

2. The first workflow

Step 1: Workflow file location

Why: GitHub only reads workflows from one folder.

What: Workflows are YAML files in .github/workflows. The on key says when they run.

Example 1: Create file

Type this:

mkdir -p .github/workflows && printf 'name: docker\non:\n  push:\n    branches: [main]\n  pull_request:\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n' > .github/workflows/docker.yml

You will see:

(no output)

What it means: A minimal workflow.

Example 2: Check

Type this:

ls .github/workflows

You will see:

docker.yml

What it means: File is in place.

Example 3: Push

Type this:

git add .github && git commit -qm 'ci: docker' && git push -q && gh run list --limit 1

You will see:

in_progress  ci: docker  docker  main  push

What it means: The run started.

Step 2: Permissions

Why: The job token must be allowed to write packages.

What: Add permissions: contents read, packages write. Nothing more.

Example 4: Add

Type this:

mkdir -p .github/workflows && printf 'name: docker\non:\n  push:\n    branches: [main]\npermissions:\n  contents: read\n  packages: write\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n' > .github/workflows/docker.yml

You will see:

(no output)

What it means: Least privilege.

Example 5: Check

Type this:

grep -A2 permissions .github/workflows/docker.yml

You will see:

permissions:
  contents: read
  packages: write

What it means: Exactly two rights.

Example 6: Without it

Type this:

gh run view --log-failed | tail -1

You will see:

denied: installation not allowed to Write organization package

What it means: This is the error when packages write is missing.

Step 3: Set up buildx

Why: The official actions give you BuildKit and QEMU.

What: docker/setup-qemu-action for other CPU types, docker/setup-buildx-action for the builder.

Example 7: Steps

Type this:

printf '      - uses: docker/setup-qemu-action@v3\n      - uses: docker/setup-buildx-action@v3\n' >> .github/workflows/docker.yml && tail -2 .github/workflows/docker.yml

You will see:

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

What it means: Two steps added.

Example 8: Run log

Type this:

gh run view --log | grep 'Buildx version'

You will see:

Buildx version: v0.17.1

What it means: Builder is ready.

Example 9: Pin versions

Type this:

grep -c '@v' .github/workflows/docker.yml

You will see:

3

What it means: Always pin action versions.

Step 4: Log in to GHCR

Why: Push needs a login.

What: docker/login-action with registry ghcr.io, username github.actor and password secrets.GITHUB_TOKEN.

Example 10: Step

Type this:

printf '      - uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n' >> .github/workflows/docker.yml && grep -c login-action .github/workflows/docker.yml

You will see:

1

What it means: Login step added.

Example 11: Run log

Type this:

gh run view --log | grep 'Login Succeeded'

You will see:

Login Succeeded!

What it means: Token worked.

Example 12: Why no secret

Type this:

printf 'GITHUB_TOKEN is created per run and expires when the job ends.\n'

You will see:

GITHUB_TOKEN is created per run and expires when the job ends.

What it means: Nothing long-lived to leak.

3. Build, tag and push

Step 5: Build and push

Why: One action does build and push.

What: docker/build-push-action with context, push and tags.

Example 13: Step

Type this:

printf '      - uses: docker/build-push-action@v6\n        with:\n          context: .\n          push: true\n          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}\n' >> .github/workflows/docker.yml && tail -1 .github/workflows/docker.yml

You will see:

tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

What it means: Tag is the commit.

Example 14: Run log

Type this:

gh run view --log | grep 'pushing manifest'

You will see:

pushing manifest for ghcr.io/zerodha-demo/shop:3f9c2e1... done

What it means: Pushed.

Example 15: Pull it

Type this:

docker pull ghcr.io/zerodha-demo/shop:3f9c2e1a7b

You will see:

Status: Downloaded newer image for ghcr.io/zerodha-demo/shop:3f9c2e1a7b

What it means: Anyone with access can pull.

Step 6: Smart tags with metadata-action

Why: Hand-written tags get messy.

What: docker/metadata-action makes tags from branch, version tag and commit, plus OCI labels.

Example 16: Step

Type this:

printf '      - id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ghcr.io/${{ github.repository }}\n          tags: |\n            type=ref,event=branch\n            type=semver,pattern={{version}}\n            type=sha\n' > meta.yml && grep type= meta.yml

You will see:

type=ref,event=branch
type=semver,pattern={{version}}
type=sha

What it means: Three rules.

Example 17: Use outputs

Type this:

printf '          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n' > use.yml && cat use.yml

You will see:

tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

What it means: Pass them to build-push.

Example 18: Tags on v1.4.0

Type this:

gh run view --log | grep 'ghcr.io' | head -3

You will see:

ghcr.io/zerodha-demo/shop:1.4.0
ghcr.io/zerodha-demo/shop:sha-3f9c2e1
ghcr.io/zerodha-demo/shop:main

What it means: Version, commit and branch tags.

Step 7: Build on PR, push only on main

Why: Pull requests from forks must never push.

What: Set push to an expression that is true only for non-PR events.

Example 19: Expression

Type this:

printf '          push: ${{ github.event_name != "pull_request" }}\n' > push.yml && cat push.yml

You will see:

push: ${{ github.event_name != "pull_request" }}

What it means: PRs only build.

Example 20: PR run

Type this:

gh run view --log | grep -i 'push'

You will see:

push: false

What it means: Tested but not pushed.

Example 21: Main run

Type this:

gh run view --log | grep -i 'push:'

You will see:

push: true

What it means: Main pushes.

Step 8: Cache in Actions

Why: Runners start empty every time.

What: cache-from and cache-to type=gha store BuildKit cache in the Actions cache.

Example 22: Add

Type this:

printf '          cache-from: type=gha\n          cache-to: type=gha,mode=max\n' > cache.yml && cat cache.yml

You will see:

cache-from: type=gha
cache-to: type=gha,mode=max

What it means: Two lines.

Example 23: First run

Type this:

gh run view --log | grep 'RUN npm ci'

You will see:

#9 [4/6] RUN npm ci 51.2s

What it means: Cold.

Example 24: Second run

Type this:

gh run view --log | grep 'RUN npm ci'

You will see:

#9 CACHED [4/6] RUN npm ci

What it means: Warm, seconds instead of a minute.

branch mainghcr.io/o/app:maincommitghcr.io/o/app:sha-3f9c2e1git tag v1.4.0ghcr.io/o/app:1.4.0pull requestbuild only, no push
Picture 2: tags from one commit

4. Make it production ready

Step 9: Multi-arch images

Why: Graviton and Apple machines need arm64.

What: platforms: linux/amd64,linux/arm64 with QEMU builds both into one tag.

Example 25: Add

Type this:

printf '          platforms: linux/amd64,linux/arm64\n' > plat.yml && cat plat.yml

You will see:

platforms: linux/amd64,linux/arm64

What it means: Two platforms.

Example 26: Inspect

Type this:

docker buildx imagetools inspect ghcr.io/zerodha-demo/shop:main | grep Platform

You will see:

Platform:  linux/amd64
Platform:  linux/arm64

What it means: One tag, two images.

Example 27: Run on Mac

Type this:

docker run --rm ghcr.io/zerodha-demo/shop:main uname -m

You will see:

aarch64

What it means: The right one is picked.

Step 10: SBOM and provenance

Why: Auditors ask what is inside and who built it.

What: sbom: true and provenance: mode=max attach attestations to the image.

Example 28: Add

Type this:

printf '          sbom: true\n          provenance: mode=max\n' > att.yml && cat att.yml

You will see:

sbom: true
provenance: mode=max

What it means: Two lines.

Example 29: Read SBOM

Type this:

docker buildx imagetools inspect ghcr.io/zerodha-demo/shop:main --format '{{json .SBOM}}' | head -c 60

You will see:

{"linux/amd64":{"SPDX":{"SPDXID":"SPDXRef-DOCUMENT"

What it means: SPDX SBOM is there.

Example 30: Provenance

Type this:

docker buildx imagetools inspect ghcr.io/zerodha-demo/shop:main --format '{{json .Provenance}}' | grep -o 'builder[^,]*' | head -1

You will see:

builder":{"id":"https://ci.example.com/run/123"

What it means: Shows the build run.

Step 11: Scan before push

Why: Do not ship known critical bugs.

What: Build with load true, scan with Trivy, then push only if it passes.

Example 31: Scan step

Type this:

printf '      - uses: aquasecurity/trivy-action@0.28.0\n        with:\n          image-ref: shop:test\n          severity: CRITICAL\n          exit-code: 1\n' > scan.yml && grep exit-code scan.yml

You will see:

exit-code: 1

What it means: Fails the job on critical bugs.

Example 32: Fail log

Type this:

gh run view --log-failed | tail -2

You will see:

Total: 1 (CRITICAL: 1)
Error: Process completed with exit code 1.

What it means: Push step never ran.

Example 33: Fixed

Type this:

gh run list --limit 1

You will see:

completed  success  fix: bump base image  docker  main  push

What it means: Green again.

Step 12: Package visibility and cleanup

Why: New GHCR packages are private by default.

What: Link the package to the repo, set visibility, and delete old untagged versions.

Example 34: Link label

Type this:

printf 'LABEL org.opencontainers.image.source=https://git.example.com/zerodha-demo/shop\n' >> Dockerfile && tail -1 Dockerfile

You will see:

LABEL org.opencontainers.image.source=https://git.example.com/zerodha-demo/shop

What it means: metadata-action adds this label automatically from the repository.

Example 35: List versions

Type this:

gh api /orgs/zerodha-demo/packages/container/shop/versions --jq 'length'

You will see:

214

What it means: Many old versions.

Example 36: Cleanup step

Type this:

printf '      - uses: actions/delete-package-versions@v5\n        with:\n          package-name: shop\n          package-type: container\n          delete-only-untagged-versions: true\n          min-versions-to-keep: 20\n' > clean.yml && grep min-versions clean.yml

You will see:

min-versions-to-keep: 20

What it means: Keeps the registry tidy.

5. Common mistakes and how Meera avoids them

Mistake one: storing a personal access token as a secret. Meera uses GITHUB_TOKEN with packages write.

Mistake two: pushing from pull requests. Meera sets push only for non-PR events.

Mistake three: tagging only latest. Meera always adds a commit tag and a version tag.

Mistake four: no cache, so every run is cold. Meera uses type=gha.

Mistake five: unpinned actions. Meera pins every action to a version.

Check yourself

Q1. Where do workflow files live?

.github/workflows.

Q2. Which permission is needed to push to GHCR?

packages: write.

Q3. How do you stop PRs from pushing?

push only when event is not pull_request.

Q4. Which action makes tags from branch and version?

docker/metadata-action.

Friends, checkout, buildx, login, metadata, build and push. Six steps and no human pushes images again. Did you understand? Let us try it once more. Add the workflow to a practice repository and push one commit.

Ravindra Bagale's Tip

Give every image at least one tag that never moves, such as the commit SHA. Deploy that tag, not latest.

Lab: try at home

  1. Create the workflow file.
  2. Add permissions and login.
  3. Build and push with a SHA tag.
  4. Add metadata-action tags.
  5. Add gha cache and compare times.
  6. Add a Trivy scan that blocks criticals.

Recap

  • Workflows live in .github/workflows.
  • packages write plus GITHUB_TOKEN logs in to GHCR.
  • build-push-action builds and pushes.
  • metadata-action makes good tags.
  • PRs build, main pushes.
  • Cache, multi-arch, SBOM and scans make it production ready.

Lab

  1. Write a Dockerfile using every instruction in this chapter and build it.
  2. Break the cache on purpose and watch which steps re-run.
  3. Convert a single-stage Go or Java build to multi-stage and compare sizes.
  4. Pass a fake token with --secret and prove it is not in docker history.

Practice task

Take any app you know, write a production Dockerfile with a pinned base, non-root USER, HEALTHCHECK, .dockerignore and multi-stage build, then show docker history has no secret and the size dropped.

Ravindra Bagale's Tip

Interviewers ask about CMD vs ENTRYPOINT almost every time. ENTRYPOINT is the program, CMD is its default arguments. Remember that.

In short

  • Pin a slim base.
  • Chain RUN steps and clean in the same layer.
  • ENTRYPOINT is the program; CMD is default arguments.
  • Prefer COPY; ADD only for tar or checksummed URLs.
  • Never put secrets in ARG or ENV; use --secret.
  • Order steps for cache and use .dockerignore.
  • Multi-stage keeps tools out of the final image.

Ravi Sir's Cards for this lesson

मराठी cards