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.
| Concept | What it is |
|---|---|
| Workflow | A 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. |
| Job | A group of steps that run on the same machine. Jobs run in parallel by default, or sequentially if you set dependencies. |
| Runner | The virtual machine that executes the job. GitHub provides Ubuntu at default but you can configure your own runners. |
| Step | A single task inside a job. It’s either a shell command (run:) or a reusable action (uses:). |
| Action | A 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
| Scope | Where it’s set | When to use it |
|---|---|---|
| Repository | Settings → Secrets → Actions | For a single project’s credentials |
| Environment | Settings → Environments | Different values per environment (staging vs prod) |
| Organization | Org Settings → Secrets | Shared across multiple repos |
GITHUB_TOKEN | Auto-injected by GitHub | Default token for repo operations — no setup needed |
⚠️ The
GITHUB_TOKENis 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.ymlor.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. Withoutpackages: write, the push to GHCR will fail with a 403 even thoughGITHUB_TOKENexists. 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
:a3f5c91dwill give you the exact same bits built from that commit. The:latesttag always points to the most recent build. Use SHA tags in production deployments,latestfor 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.
- GitHub Actions Official Docs
- GitHub Actions Marketplace
- nektos/act — Run workflows locally
- Docker + GitHub Actions
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!