Act: Running GitHub Actions Locally Before You Push

Stop using your CI as a slow, public debugger

We have all done the thing. You tweak a workflow file, you can’t run it locally, so you push a commit called “fix ci”, watch it fail, push “fix ci 2”, fail, “actually fix ci”, fail, and by the time it’s green your git history reads like a cry for help. The feedback loop on GitHub Actions is dreadful precisely because the only way to test it is to use the production system itself, slowly, in public. act fixes that by running your workflows locally in Docker.

Advertisement

act reads your .github/workflows/*.yml files, builds the dependency graph of jobs, and executes them inside containers chosen to approximate GitHub’s runners. It pulls the same actions from the marketplace, sets up the same GITHUB_* environment variables, and runs your steps. It is not a perfect emulator — more on that honesty below — but it catches the overwhelming majority of “I made a typo in my YAML” and “this step’s command is wrong” mistakes in seconds instead of minutes.

Installation is a single binary. On most systems:

# macOS / Linux via the official install script
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash

# or via your package manager
brew install act          # macOS

The only hard requirement is a working Docker (or Podman) daemon, because every job runs in a container.

The first time you run act, it asks which size of runner image you want. This matters more than it looks. The default act images are stripped down to keep them small; the full GitHub-equivalent images are enormous (20+ GB) because they ship every tool GitHub preinstalls. I use the medium image and accept that I occasionally have to install a tool myself.

# List what act thinks it will run, without executing
act -l

# Run the default push event
act push

# Run a specific job by name
act -j build

# Pin a runner image so behaviour is reproducible
act -P ubuntu-latest=catthehacker/ubuntu:act-22.04 push

That -P flag maps a GitHub runner label to a Docker image. I keep these mappings in a .actrc file at the repo root so everyone on the team gets the same images without remembering the flag:

-P ubuntu-latest=catthehacker/ubuntu:act-22.04
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
--container-architecture linux/amd64

That last line is essential on Apple Silicon — without it, half the marketplace actions fall over because they ship amd64-only binaries and the arm64 emulation confuses them.

Real workflows need secrets and react to specific event payloads. act handles both. Secrets come from a file or flags, and you can feed a synthetic event JSON to test, say, a pull-request workflow without opening a pull request:

# Provide secrets from a gitignored file
act -j deploy --secret-file .secrets

# Simulate a pull_request event with a crafted payload
act pull_request -e event.json

Where event.json is a trimmed-down version of the webhook payload GitHub would send. You only need the fields your workflow actually reads. This is brilliant for debugging the if: conditions that gate jobs by branch or label, which are otherwise a nightmare to test because triggering them for real means manufacturing the exact event.

Honesty time, because act will eventually bite you if you trust it blindly. It is an approximation, and the gaps are real. The runner images are not byte-identical to GitHub’s, so a step relying on a preinstalled tool may pass on GitHub and fail locally, or vice versa. services: containers work but networking can differ. Anything that calls the GitHub API for real — creating releases, posting comments, OIDC token exchange for cloud auth — either needs a token or simply can’t be exercised locally. And matrix builds run sequentially rather than in parallel, so timing-dependent behaviour won’t surface.

The way I treat it: act is for validating workflow logic — syntax, step ordering, conditionals, that your scripts run — not for certifying that a deploy will succeed against live infrastructure. It moves the cheap, common failures left to your laptop and leaves the genuinely environment-specific ones to the real runner. That is exactly the right division of labour.

If you write or maintain GitHub Actions workflows of any complexity, absolutely. The amount of “fix ci” commit-spam it eliminates pays for the setup ten times over in the first week, and your colleagues stop having to scroll past your failed-pipeline history. The cost is a Docker dependency and the discipline to remember it’s an approximation rather than gospel.

It is not worth it if your workflows are three trivial steps that never break, and it won’t help you debug a deploy that only fails against production. But for the iterative grind of getting a non-trivial pipeline working, running it locally first is the obvious move, and I’m mildly annoyed it took me as long as it did to adopt. Keep a .actrc checked in, treat green-locally as “probably fine” rather than “guaranteed”, and you’ll push far fewer embarrassing commits.

Advertisement

Related Content

Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.