act runs your GitHub Actions workflows on your own machine, in Docker containers set up to look like GitHub's runners. You find the broken step in seconds on your laptop, rather than through another push and another wait.
Why debugging CI on GitHub hurts
A workflow is a YAML file in .github/workflows/ that tells GitHub what to run when something happens, usually a push or a pull request. If GitHub Actions is new to you, start there: jobs, steps and runners are the vocabulary for everything below.
The trouble starts when a workflow fails and the only place to test the fix is GitHub itself. Every attempt means a commit, a push, a wait for a runner to start, and then reading the log in a browser. A one-character typo in a YAML file can take five or six round trips to find, and the history fills up with commits called 'fix ci' and 'please work'.
It costs money too. Standard GitHub-hosted runners are free in public repositories, but in a private repository every job draws on your plan's included usage, and a failed run is charged for the time it ran. GitHub's billing docs give the example of a 10-minute workflow that fails after 5 minutes: fix it, re-run it, and you have paid for 15.
What act does
act is an open source command-line tool from the nektos project. When you run it in a repository, it:
- reads the workflow files in
.github/workflows/; - works out which jobs the event triggers (a push, unless you say otherwise) and the order they depend on each other;
- pulls or builds the Docker images it needs;
- starts a container for each job and runs its steps inside it, with the environment variables and file layout GitHub provides.
Actions you use with uses:, such as actions/checkout, are fetched from GitHub and run inside the container, just as they would on a hosted runner. Your own run: steps run in a shell in the same container. The log streams straight to your terminal, each line prefixed with the job's name, so you can follow a test, build and deploy job side by side.
Because everything runs in containers, act needs Docker, or another engine that speaks the Docker Engine API. If containers in general are hazy, Docker explained covers what an image and a container are. Podman is not officially supported: it may work, but act makes no promise, and Docker vs Podman explains how the two differ.
The runner image
On GitHub, runs-on: ubuntu-24.04 gets you a full virtual machine with a long list of languages and tools preinstalled. act can't download that machine, so it maps each runner label to a Docker image instead. The first time you run act, it asks which default image to use:
- Large: a snapshot of GitHub's hosted runners, about 17 GB to download and over 50 GB on disk.
- Medium: around 500 MB, with just the tools needed to start actions. It aims to work with most of them.
- Micro: under 200 MB, with little more than Node.js. Some actions won't run on it.
Medium is the usual choice. Your answer is saved in act's own config file in your user folder, and you can change it later.
Setting up act
On macOS, Homebrew installs it with brew install act. On Linux, the project publishes an install script, and act is also in the Arch, Nix, COPR and other package repositories. Windows users can get it from Chocolatey, Scoop or WinGet.
The default images cover the common labels such as ubuntu-latest and ubuntu-22.04. If your workflow asks for a label the default mapping doesn't know, act skips those jobs with 'Skipping unsupported platform' and suggests the -P option, which maps a runner label to an image. To avoid typing it every time, put it in a .actrc file at the root of the repository. act reads it whenever you run act from that folder. Each line holds one command-line argument. For a workflow on ubuntu-24.04, the line maps that label to catthehacker/ubuntu:act-24.04, the medium image for that Ubuntu release.
Secrets need a home too. On GitHub they live in the repository settings; locally, act reads a .secrets file from the repository root by default, one NAME=value per line in the same format as a .env file. Add .secrets to .gitignore straight away, so the real values never land in a commit.
Here is that whole setup: choosing the medium image, seeing the jobs skipped, then writing .actrc and .secrets:
One secret needs extra care. GitHub creates GITHUB_TOKEN for every run, but act can't, so a workflow that calls the GitHub API fails locally with a token error. Pass a personal access token as that secret. If you use the GitHub CLI, gh auth token prints one you can hand to act.
A worked example: fixing a failing test
Say a small shop's repository has a workflow with three jobs: test runs the test suite, build builds the site, and ship deploys it once both pass. A change to the discount code breaks a test, and the push has already gone red on GitHub.
Rather than guess at a fix and push again, run act in the repository. The test and build jobs start in their own containers, and within seconds the test output shows the failing check and the job ends with 'Job failed'. Nothing was pushed and no hosted runner time was used. Fix the code, run act again, and this time both tests pass and ship runs to the end:
Now you push once, and the run GitHub sees is the one that passes.
A few options make that loop quicker:
act -llists every job act can see, with its workflow and the events that trigger it.act -j testruns only the job with the IDtest, so you don't wait for the rest.act --watchwatches the repository and runs the workflow again every time you save a file.act pull_requestsimulates a different event. With no event named, act behaves as if you pushed.act -nis a dry run: it checks that the workflows are valid without starting any containers.
Where act differs from GitHub
A container is not the virtual machine GitHub gives you, and act's own docs say its default images are 'intentionally incomplete'. A tool that comes preinstalled on GitHub's runner may be missing from the medium image, and some software won't run in a container at all: Docker doesn't run systemd, for example.
Some workflow features are also ignored or only partly supported, as the project lists. concurrency, timeout-minutes, continue-on-error and job permissions are ignored, the github context is incomplete, and secrets can't be scoped to deployment environments. Annotations and step summaries are dropped as well.
Then there is the processor. act uses your machine's architecture by default, so on an Apple Silicon Mac the containers are arm64, while a standard GitHub runner is x86-64. Most of the time that doesn't matter. When a tool only ships x86-64 builds, the --container-architecture linux/amd64 option runs the containers under emulation instead, which is slower but closer to GitHub.
So treat a green run in act as strong evidence, not proof. The final run still happens on GitHub.
Common mistakes
- Committing
.secrets. Ignore it in git before you put anything real in it. - Running deploy steps locally. act runs everything the event triggers, including the job that publishes a package or posts to Slack. act sets an
ACTenvironment variable, so a step can check it and skip itself; whole jobs can be skipped with a custom event property. - Expecting the full runner's tools. If a step fails with 'command not found', the medium image probably lacks that tool. Install it in a step, or map that runner to a fuller image with
-P. - Forgetting the token. A workflow that works on GitHub but fails locally with an authentication error usually needs
GITHUB_TOKENpassed as a secret.
When to use it
act pays off while you are writing or changing a workflow, and when you need to reproduce a broken build. It is also handy as a local task runner, since the steps in your workflow already describe how to test and build the project.
It helps much less when the problem lives on GitHub's side: a macOS or Windows runner, a feature act ignores, or permissions and environments that only exist on GitHub. For those, push to a branch and let GitHub run it.
Key takeaways
- act reads
.github/workflows/and runs each job in a Docker container that mimics a GitHub runner. - Testing locally avoids a push for every attempt and saves the hosted runner time that failed runs use in private repositories.
- Pick the medium image on first run, map other runner labels in
.actrc, and keep secrets in a git-ignored.secretsfile. -l,-jand--watchmake the local loop quick.- A container is not GitHub's real machine, so the final run still happens on GitHub.