Stop Waiting on CI: Run GitHub Actions Locally with Act
GitHub Actions is great until every small change requires a push and a 10 minute queue. Act lets you run most workflows locally so you can debug faster and ship with confidence.

You change one line. A lint rule. A tiny env var tweak. A version bump.
Then you do the most wasteful ritual in modern engineering: commit → push → wait.
Not because the change is risky, but because the feedback loop is broken. GitHub Actions is a remote CI system, which means you’re paying a tax every time you want to answer a simple question like: “Will this job even start?” or “Why did it fail in that container?”
The uncomfortable truth: if your workflow debugging depends on remote pipelines, you’re accepting slow iteration as normal. It’s not normal. It’s optional.
In this post, you’ll learn how to run GitHub Actions locally using Act, how it works, the caveats that bite people, and a practical setup that keeps your feedback loop tight.
Core insight: Most CI failures are not “CI problems.” They’re environment problems. Act lets you reproduce the environment locally so you can fix the real issue without burning pushes.
Background
What is Act?
Act is a CLI tool that can execute GitHub Actions workflows locally by interpreting your .github/workflows/*.yml files and running jobs inside Docker containers that mimic GitHub runners.
That means you can:
- validate that the workflow parses,
- run jobs and steps without pushing,
- debug failures with local logs,
- iterate quickly on workflow changes.
Why this matters now:
- Teams increasingly treat CI as “the truth,” but remote CI is inherently slower.
- Developer productivity lives or dies by feedback cycle time.
- “Push to see what happens” is a process smell.
Key terms (briefly):
- Workflow: YAML file defining triggers and jobs (e.g.,
ci.yml). - Job: A set of steps executed on a runner (e.g.,
build,test). - Runner: The machine/container running the job (GitHub-hosted or self-hosted).
- Event: What triggers the workflow (e.g.,
push,pull_request,workflow_dispatch).
The Problem
Remote CI has two big frictions that compound:
-
Latency
- queued runners,
- cold starts,
- network pulls,
- shared infrastructure variability.
-
Forced context switching
- you must commit and push just to test workflow logic,
- you poll for results,
- you forget what you changed,
- you lose momentum.
Concrete symptoms:
- You push multiple “try fix CI” commits just to get a green run.
- You can’t easily inspect the environment when a step fails.
- Small workflow edits (paths, permissions, caching keys) take 5–20 minutes to validate.
- PR feedback becomes “wait for CI” instead of “review the change.”
Why this matters:
- The opportunity cost is enormous. A 10-minute loop repeated 10 times is half a workday gone on pure waiting.
The Approach
Use Act to shift CI debugging left: reproduce the workflow locally, fix it locally, and push once you’re confident.
What Act can do well
- Run many Linux-based workflows locally.
- Execute steps using Docker like
run: npm testorgo test ./.... - Run most marketplace actions (especially JavaScript and Docker-based actions).
- Let you pass secrets and env vars to mimic CI.
Where Act is limited (don’t lie to yourself)
- Workflows that depend on GitHub-hosted services or specific runner quirks.
- macOS/Windows runners (Act is best for Linux via Docker).
- Some actions rely on GitHub internal APIs or runner-specific paths.
- Subtle differences in preinstalled tooling on GitHub runners vs your local image.
That said: for the common case lint/test/build Act is usually enough to eliminate the “push to debug” loop.
A practical workflow for using Act
- Run the workflow locally to reproduce failure.
- Fix the workflow or scripts until it passes locally.
- Push once (ideally a single clean commit) and let remote CI confirm.
Tradeoff: Act is not a perfect emulator. The goal is not perfect fidelity. The goal is a 10x faster iteration loop for most failures.
Practical Walkthrough
1) Install prerequisites: Docker + Act
You need Docker running locally because Act executes jobs in containers.
Install Act (common options):
- macOS (Homebrew):
brew install act
-
Linux:
- Install from your package manager if available, or use the official releases.
-
Windows:
- Use a supported package manager (e.g., Chocolatey/Scoop) and ensure Docker Desktop is working.
2) Run your first workflow locally
From the repository root:
act
This will attempt to run the default event (often push). If your workflow only triggers on pull_request, you’ll need to specify it.
Run a specific event:
act pull_request
3) Pick the right runner image (this is where most people fail)
By default, Act uses minimal images that may not match GitHub’s ubuntu-latest environment. Many pipelines assume tools exist (Node, Go, Java, etc.). If Act can’t find them, your job fails and you’ll falsely blame your workflow.
Use a more compatible image mapping:
act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:full-latest
This is the single highest-leverage fix for “it works on GitHub but fails locally (or vice versa)” caused by missing dependencies.
4) Run only the job you care about
Most repos have multi-job workflows. Don’t run everything.
List jobs (Act usually prints available jobs), then run a specific job:
act -j test -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:full-latest
5) Pass secrets and env vars
Your workflow probably relies on secrets like NPM_TOKEN, GITHUB_TOKEN, cloud creds, etc.
Option A: inline -s (quick and dirty)
act -s NPM_TOKEN=xxxxxxxx
Option B: .secrets file (cleaner)
Create a .secrets file:
NPM_TOKEN=xxxxxxxx
SOME_API_KEY=yyyyyyyy
Then run:
act --secret-file .secrets
For env vars, use --env or --env-file similarly.
6) Simulate workflow_dispatch and inputs (when needed)
If you have manual triggers with inputs, Act can pass event payloads. The exact shape depends on your workflow. A common approach is to provide an event JSON file.
Example (conceptual):
act workflow_dispatch -e event.json
Where event.json contains the inputs you expect. This is one area where you may need to adjust for your specific workflow triggers.
7) Debugging: make Act useful, not noisy
Helpful flags:
-
Verbose logs:
act -v -
Reuse containers (faster iterative runs):
act --reuse
If a step fails, reproduce it outside the workflow too:
- Copy the
run:command and execute it in your local shell. - Or exec into the container (advanced, but powerful) by reusing containers and using Docker CLI.
If you only do one thing…
Standardize an Act command in your repo (Makefile or npm script), so everyone runs the same image and job names:
act -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:full-latest
Then add a few common targets:
act -j lint ...act -j test ...act pull_request -j build ...
This turns “CI debugging” into a 30-second habit.
Pitfalls to avoid (the ones that waste time)
- Assuming Act’s default image matches GitHub runners. It often doesn’t.
- Forgetting secrets. Your workflow may pass on GitHub because secrets exist there.
- Relying on services not started locally. If your CI uses Postgres/Redis services, you need equivalent containers locally.
- Actions that call GitHub APIs in runner-specific ways. Some actions behave differently off-platform.
A quick checklist before you blame Act:
- Are you using a “full” runner image mapping?
- Did you pass the secrets/env vars your workflow expects?
- Are dependent services available (Docker Compose, etc.)?
- Does the workflow use Linux runners only?
Conclusion
Act won’t replace GitHub Actions. It replaces the worst part of GitHub Actions: the slow, push-dependent feedback loop for workflow debugging.
Key takeaways:
- Act accelerates iteration by running workflows locally in Docker.
- Runner image selection matters more than almost anything else.
- Use job-scoped runs to avoid wasting time running full pipelines.
- Secrets/env parity is critical if you want meaningful local results.
- Act is “close enough” for most CI failures and that’s the point.
Your challenge:
- Pick your slowest or flakiest workflow.
- Add a documented
actcommand (or Makefile target) that runs the most important job locally with a compatible runner image. - The next time CI fails, fix it locally first, then push once.
References
- Act: https://github.com/nektos/act
- GitHub Actions Documentation: https://docs.github.com/en/actions
- GitHub Actions Workflow syntax: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
- Runner environments: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
- Catthehacker Ubuntu images for Act: https://github.com/catthehacker/docker_images