Github Actions: Understanding the Basics and Building a Docker Pipeline

Github Actions: Understanding the Basics and Building a Docker Pipeline

Note: I wrote this guide because I wish I had it when I started with GitHub Actions. The official documentation is comprehensive but overwhelming for beginners. This is a distilled, practical introduction that skips the fluff and gets you building real pipelines from day one.

GitHub Actions Without Dying in the Attempt

A pragmatic, no-fluff guide for developers who want real pipelines running in production — not more YAML tutorials that end with printing “hello world”.


Introduction: Why Actions Scares People (And Shouldn’t)

We’ve all been there. You open a .github/workflows/ file for the first time, stare at a wall of YAML indentation, and immediately feel like you’re diffusing a bomb. One wrong space and everything explodes.

Dont worry: You will make errors anyway :D

90% of the errors you’ll hit as in Github Actions are syntax issues or misunderstood context variables — not complex logic. The underlying concepts are straightforward once you have the right mental model.

The reason GitHub Actions feels intimidating is the same reason any powerful tool feels intimidating: there are a lot of knobs. But you don’t need most of them to build something genuinely useful on day one.

My first pipeline in Production was a simple Docker build-and-push workflow. It had zero secrets, no complex logic, and it worked on the 8th try because I wrong understood how secrets and variables work in GitHub.

In this guide we skip the toy examples and go straight to something you’d actually use: a workflow that builds a Docker image from your repository’s Dockerfile and pushes it to GitHub Container Registry (GHCR), versioned by the commit SHA. That’s a real, production-grade pipeline you can take and use today.


The Right Mental Model

Before writing a single line of YAML, internalize these five concepts. They’re the entire vocabulary of GitHub Actions. Everything else is just combinations of these.

ConceptWhat it is
WorkflowA YAML file living in .github/workflows/. Defines the full automation: when it runs and what it does.
Trigger (on:)The event that kicks off the workflow. A push, a pull request, a schedule, or even a manual button click.
JobA group of steps that run on the same machine. Jobs run in parallel by default, or sequentially if you set dependencies.
RunnerThe virtual machine that executes the job. GitHub provides Ubuntu at default but you can configure your own runners.
StepA single task inside a job. It’s either a shell command (run:) or a reusable action (uses:).
ActionA packaged, reusable unit of automation. Think of it like an npm package, but for CI/CD steps.

If you can draw a mental diagram where trigger → workflow → jobs → steps → actions, you already understand 80% of what you need. The rest is just the details.

ℹ️ Context variables like ${{ github.sha }} or ${{ github.actor }} are GitHub’s way of injecting runtime information into your workflow. Think of them as environment variables that GitHub populates automatically for every run.

  • There are diferences between secrets and variables, and they are not swappable. Secrets are encrypted and masked in logs, while variables are plain text and visible in logs. Use secrets for sensitive data (like tokens or stuff like that) and variables for non-sensitive configuration (like environment labels, tags or minimal data).

My Common mistakes

These are the errors that burn everyone at least once. Knowing them in advance saves you from a very frustrating afternoon staring at red pipeline icons.

Wrong indentation YAML is indentation-sensitive. Use spaces, never tabs. Two spaces per level is the standard. A single rogue space will break the entire file without a meaningful error message.

Secrets hardcoded in YAML Never put tokens, passwords, or API keys directly in the workflow file. Use ${{ secrets.YOUR_SECRET }} — the value lives in the GitHub environment, not in your code.

Confusing run: vs uses: run: executes a shell command. uses: references a pre-built action from the Marketplace. They are not the same.

Missing permissions: block Trying to push to a package registry or write to issues without declaring the correct permissions will fail silently or with a cryptic 403 error.

Burning free minutes GitHub gives you 2,000 free minutes/month on public repos and fewer on private ones. Avoid running heavy workflows on every commit to every branch — use branch filters.

Forgetting fetch-depth By default, actions/checkout does a shallow clone (depth 1). Some tools need the full history. Add fetch-depth: 0 when needed.


Secrets & Environment Variables

This deserves its own section because it’s where most developers make mistakes that end up in production — or in a public repo.

How secrets work

Secrets are encrypted key-value pairs stored in GitHub, never in your code. You reference them with the ${{ secrets.NAME }} syntax. GitHub automatically masks their values in log output — if a secret ever appears in a log, it shows as ***.

Types of secrets

ScopeWhere it’s setWhen to use it
RepositorySettings → Secrets → ActionsFor a single project’s credentials
EnvironmentSettings → EnvironmentsDifferent values per environment (staging vs prod)
OrganizationOrg Settings → SecretsShared across multiple repos
GITHUB_TOKENAuto-injected by GitHubDefault token for repo operations — no setup needed

⚠️ The GITHUB_TOKEN is automatically available in every workflow run. It’s scoped to the repository and expires when the run ends. For pushing to GHCR, this is all you need — no manual configuration required.

Variables vs Secrets

Not everything needs to be a secret. Non-sensitive configuration (like a region name, an environment label, or a Slack channel ID) should live in Variables (Settings → Variables → Actions), not Secrets. Variables are visible in logs; secrets are not. Use the right tool for the job.


Reusability: The Next Level

Once you have a few workflows running, you’ll notice you’re repeating yourself. Setting up Node, checking out the repo, configuring credentials — these steps appear in every workflow. That’s when reusability becomes essential.

Reusable Workflows

A reusable workflow is a workflow file that other workflows can call, passing inputs and secrets. Think of it like a function you can invoke from other pipelines. You define it once with on: workflow_call: and reference it elsewhere with uses: ./.github/workflows/my-reusable.yml.

Composite Actions

Composite actions let you bundle multiple steps into a single, versioned unit stored in a repository. They’re ideal for steps that need to be shared across repositories or published to the GitHub Marketplace.

💡 Don’t reach for reusability on day one. Build your first three or four workflows as standalone files. The duplication will make the patterns clear, and you’ll know exactly what to extract when the time comes.


Debugging Like a Pro

When a workflow fails, most developers scroll through the logs looking for red text and hoping for the best. There’s a better way.

Enable Debug Logging Set the repository secret ACTIONS_STEP_DEBUG to true. GitHub will emit verbose logs for every step, including hidden internal details.

Strategic echo statements Add run: echo "${{ toJson(github) }}" to dump the full context object. Invaluable for understanding what variables are actually available at runtime.

if: always() Use if: always() on diagnostic steps so they run even when previous steps fail. Essential for uploading artifacts or logs from broken builds.

Run locally with act nektos/act is an open source tool that runs GitHub Actions locally using Docker. It won’t replicate everything, but it catches 80% of issues without burning cloud minutes.

Act is convenient for syntax and logic errors, but remember that some issues only appear in the GitHub environment ( like permissions or secrets). Always test critical workflows in the real environment before relying on them.


🛠 Hands-On: Build & Push Docker Image to GHCR

Time to build something real. This workflow builds a Docker image from your repo’s Dockerfile and pushes it to GitHub Container Registry — versioned by the exact commit SHA that triggered the build. Zero manual secrets needed.

You can use any personal repository that has a Dockerfile. If you don’t have one, create a simple one at the root:

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl
CMD ["echo", "Container is alive!"]

Step 1 — Create the workflow file

In your repository, create the directory .github/workflows/ if it doesn’t exist. Inside it, create a file called docker-publish.yml:

Note: The directory must be .github/workflows/ (with a dot at the beginning). Also, the file name can be anything and not necessarily the same, but it must end with .yml or .yaml.

name: Build & Push Docker Image

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-push:
    name: Build & Push to GHCR
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write  # Required to push to GHCR

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}  # Auto-injected, no setup needed

      - name: Build & Push image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest

Step 2 — Configure credentials (the easy part)

Here’s the beautiful thing about this workflow: there are zero secrets to configure manually.

The GITHUB_TOKEN is automatically injected by GitHub into every workflow run. It’s scoped to your repository, has write access to packages (since we declared packages: write in the permissions block), and expires when the run ends.

⚠️ The permissions: block is not optional here. Without packages: write, the push to GHCR will fail with a 403 even though GITHUB_TOKEN exists. Declaring permissions explicitly is also a security best practice — it follows the principle of least privilege.

Step 3 — Push to main and watch it run

Commit and push the workflow file to your main branch:

git add .github/workflows/docker-publish.yml
git commit -m "ci: add docker build and push workflow"
git push origin main

Navigate to your repository on GitHub and click the Actions tab. You should see the workflow running within seconds. Click on it to watch the live log output.

Step 4 — Find your image in GHCR

Once the workflow completes successfully, your Docker image is live. On your GitHub profile page, click the Packages tab — your image will be listed there with both tags.

You can also pull it directly from any machine with Docker installed:

# Pull the latest build
docker pull ghcr.io/YOUR_USERNAME/YOUR_REPO:latest

# Or a specific commit (immutable, always the same image)
docker pull ghcr.io/YOUR_USERNAME/YOUR_REPO:a3f5c91d...

ℹ️ The SHA-tagged image is immutable. Six months from now, pulling :a3f5c91d will give you the exact same bits built from that commit. The :latest tag always points to the most recent build. Use SHA tags in production deployments, latest for quick local testing.

Step 5 — What to do if it fails

403 Forbidden on push: Check that the permissions: block includes packages: write. Also verify the package visibility in your GitHub profile under Packages → your package → Package settings → Change visibility.

Dockerfile not found: The context: . in the build step assumes your Dockerfile is at the root of the repository. If it’s in a subdirectory, update the context path accordingly.

Workflow not triggering: Confirm the file is in .github/workflows/ (note the dot before “github”) and that you pushed to the main branch exactly as specified in the on.push.branches filter.


Resources to Keep Going

The best way to learn Actions is by reading workflows written by experienced teams.

The real leap in understanding comes from reading the .github/workflows/ directories of popular open source projects. Look at how teams at Vercel, Grafana, or any major open source project structure their pipelines — you’ll learn more in an hour of reading real workflows than in a day of tutorials.


You now have a working Docker pipeline, a solid mental model, and a map of the most common pitfalls. The rest is iteration. Break things, read the logs, fix them. That’s the whole game.

Glhf! Happy Coding!