Introducing Tenki's code reviewer: deep, context-aware reviews that actually find bugs.Try it for Free
GitHub Actions|CI/CD|+3
Jan 2026

Test GitHub Actions Locally with act

Hayssem Vazquez-Elsayed
Hayssem Vazquez-Elsayedproduct

Share Article:

Every team that maintains GitHub Actions workflows knows the cycle: change a YAML file, commit, push, wait for the runner to pick it up, watch it fail on line 47, fix the typo, push again. Repeat until it works or you lose the will to live. The feedback loop is brutal compared to almost any other kind of development.

act exists to fix that. It's an open-source CLI (69k+ stars on GitHub, v0.2.84 as of late 2025) that reads your workflow files from .github/workflows/ and executes them inside Docker containers on your local machine. The environment variables, filesystem layout, and execution model approximate what GitHub's hosted runners provide.

But "approximate" is doing real work in that sentence. act's Docker-based approach has sharp edges, and understanding them upfront will save you from a different kind of push-and-pray debugging.

Installing act

You need Docker running first. act doesn't bundle its own container runtime; it talks to whatever Docker daemon is available on your system.

Installation options vary by platform:

# macOS (Homebrew)
brew install act

# Windows (Chocolatey)
choco install act-cli

# Windows (Scoop)
scoop install act

# Linux (GitHub CLI extension)
gh extension install https://github.com/nektos/gh-act

# Any platform (Go install)
go install github.com/nektos/act@latest

# Any platform (shell script)
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash

The first time you run act in a repository, it asks you to pick a default runner image size. This choice matters more than you'd expect.

Choosing your runner image

act maps runs-on labels in your workflows to Docker images. There are three tiers:

Micro images (node:16-buster-slim) are tiny. Around 200MB. They have Node.js and basically nothing else. Most workflows that use setup-* actions will work because those actions install their own toolchains, but anything expecting pre-installed tools like Python, Docker CLI, or common build utilities will fail.

Medium images (catthehacker/ubuntu:act-latest) are a reasonable middle ground. They include common tools and weigh a few gigabytes. This is what most people should start with.

Large images (catthehacker/ubuntu:full-latest) are filesystem dumps of GitHub's actual runner images. They're 18GB+. They give you the closest parity with GitHub's hosted runners, but pulling and storing them is painful. Only use these if the medium images don't cut it for your specific workflow.

Your selection gets written to ~/.actrc. You can override it per-project with a local .actrc in the repo root, or on the command line with the -P flag:

# Use medium image for ubuntu-latest
act -P ubuntu-latest=catthehacker/ubuntu:act-latest

# Or put it in .actrc (one flag per line)
echo '-P ubuntu-latest=catthehacker/ubuntu:act-latest' > .actrc

Basic usage

Run act from your repo root and it triggers all workflows that respond to the push event by default. You can target specific events, workflows, or jobs:

# Run all push-triggered workflows
act

# Run pull_request-triggered workflows
act pull_request

# Run a specific workflow file
act -W .github/workflows/ci.yml

# Run only a specific job by name
act -j test

# List workflows without running them
act -l

# Dry run (validate without execution)
act -n

act reads your workflows, resolves the dependency graph between jobs, pulls the necessary Docker images, and runs each step in sequence inside a container. It mounts your repo as the workspace, sets up the standard GITHUB_* environment variables, and streams output back to your terminal.

Secrets and variables

Your workflows probably reference secrets. act gives you three ways to provide them:

# Inline (careful with shell history)
act -s MY_SECRET=some-value

# Secure prompt (recommended)
act -s MY_SECRET

# From a file (.env format)
act --secret-file .secrets

The .secrets file uses standard dotenv format. Add it to .gitignore immediately. If you've got the GitHub CLI installed, you can feed it a GITHUB_TOKEN automatically:

act -s GITHUB_TOKEN="$(gh auth token)"

Repository variables (accessed via ${{ vars.* }}) work the same way, with --var and --var-file flags.

What works reliably

act handles the core workflow features well:

  • Run steps execute shell commands exactly as you'd expect. This is the bread and butter.
  • JavaScript actions (like checkout, setup-node, setup-python) generally work. act downloads and caches them locally.
  • Matrix strategies work and you can filter them with --matrix node:18 to test a single combination instead of the full grid.
  • Conditionals and expressions using if: are evaluated. The expression parser handles most of the GitHub Actions expression syntax.
  • Job dependencies via needs: are respected. Jobs run in the correct order.
  • Container actions (actions that define their own Dockerfile) are built and run.
  • Event payloads can be provided via -e event.json to simulate pull_request, workflow_dispatch, or other triggers.

What breaks (and what's missing)

This is where act earns its reputation as a tool you need to understand before relying on. The official docs maintain a list of unsupported features, and some of them are silent failures.

OIDC tokens don't exist locally. The OpenID Connect URL is undefined in act. Any step that calls aws-actions/configure-aws-credentials with role-to-assume will fail. There's no workaround within act itself; you either skip the step or provide static credentials.

Service containers are unsupported. The services: block in your workflow (for spinning up Postgres, Redis, etc.) is ignored. Your tests that depend on services.postgres will just see a connection refused error with no helpful context.

The github context is incomplete. act populates what it can from your local git state, but values like github.event.pull_request.number or github.actor may be empty or incorrect. If your workflow logic branches on these values, results will differ.

Concurrency, permissions, and timeouts are ignored. The concurrency:, permissions:, timeout-minutes:, and continue-on-error: fields are silently skipped. A workflow that relies on continue-on-error: true to handle expected failures will behave differently locally.

Step summaries and annotations vanish. Anything written to $GITHUB_STEP_SUMMARY is discarded. Problem matchers and annotations are also ignored. Your workflow won't error, but those outputs just go nowhere.

Artifacts require explicit opt-in. The artifact server isn't started by default. You need to pass --artifact-server-path $PWD/.artifacts to enable upload/download within a run. Cross-run or cross-repository artifact downloads don't work.

Deployment environments are ignored. The environment: field on jobs is skipped, and environment-scoped secrets aren't resolved to the correct scope.

The Docker-in-Docker problem

This one deserves its own section because it trips up nearly everyone who tries to run Docker-heavy workflows locally.

GitHub's hosted runners are full VMs with Docker pre-installed. When your workflow runs docker build or docker push on GitHub, it's talking to a real Docker daemon on the VM. act runs your steps inside a Docker container, so if your step tries to use Docker, you're doing Docker-in-Docker.

By default, act doesn't give the container access to the host Docker socket. Workflows that build images, push to registries, or use Docker Compose will fail. You can bind-mount the socket:

act --container-daemon-socket /var/run/docker.sock

This gives the container access to your host's Docker daemon, which works but has implications: images built inside the workflow land on your host, volume mounts reference host paths (not container paths), and you're essentially giving the workflow root-equivalent access to your machine.

On Apple Silicon Macs, there's another layer of friction. The runner images are linux/amd64, so everything runs under QEMU emulation. It's slower and occasionally triggers architecture-specific bugs. Adding --container-architecture linux/amd64 to your .actrc is practically required on M-series Macs.

Structuring workflows for local testability

Once you've hit act's limitations a few times, you'll start writing workflows differently. The patterns that make workflows locally testable also tend to make them better in general.

Extract logic into scripts. Instead of jamming 30 lines of bash into a run: block, put it in scripts/ci/run-tests.sh. The script is testable without act or GitHub at all. Your workflow step becomes a one-liner:

- name: Run tests
  run: ./scripts/ci/run-tests.sh
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

Now you can test the script directly on your machine, independent of any CI tooling.

Use the ACT environment variable to skip steps. act automatically sets an ACT environment variable. Use it to skip steps that can't work locally:

- name: Deploy to production
  if: ${{ !env.ACT }}
  run: ./scripts/deploy.sh

- name: Notify Slack
  if: ${{ !env.ACT }}
  uses: slackapi/slack-github-action@v1

Skip entire jobs with a custom event property. The ACT env var doesn't work in job-level if: conditions. For job-level skipping, use a custom event payload:

jobs:
  deploy:
    if: ${{ !github.event.act }}
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Then run act with an event file that sets the flag:

echo '{"act": true}' > .act-event.json
act -e .act-event.json

Pin your .actrc per project. Commit a .actrc in your repo root. This way every developer on the team gets the same configuration without having to figure out the right flags:

-P ubuntu-latest=catthehacker/ubuntu:act-latest
--container-architecture linux/amd64
--action-offline-mode
-e .act-event.json

Speeding things up with offline mode

By default, act pulls Docker images and clones actions from GitHub on every run. For a tight iteration loop this is slow and wasteful. The --action-offline-mode flag changes the behavior: it stops re-pulling images and re-downloading actions that are already cached. Run once online to populate the cache, then iterate offline.

Similarly, --pull=false skips image pulls if the image already exists locally. Useful when you're working with a local runner image you've customized.

act vs. running steps manually

act isn't the only way to test workflows locally. Some teams just skip it and run individual steps by hand. Here's how they compare:

act gives you the full workflow execution: event triggers, job ordering, matrix expansion, action resolution, and environment setup. It's the closest you can get to the real thing without pushing. The tradeoff is the limitations listed above and the Docker overhead.

Running steps manually means copying the shell commands from your workflow's run: blocks and executing them in your terminal. It's fast and has zero overhead. But you don't get expression evaluation, matrix expansion, or any of the workflow orchestration. It's fine for testing a single build script. It's not useful for testing the workflow itself.

The sweet spot for most teams: use act to validate workflow structure and job ordering, but invest in making your actual CI logic runnable as standalone scripts. That way you can test the scripts directly and use act for integration-level checks.

Common failure modes and fixes

A few patterns come up repeatedly when teams start using act:

"Cannot find module" errors from Node.js actions. This usually means the runner image doesn't have the right Node.js version for the action. The micro images ship with Node 16, but newer actions may require Node 20. Either switch to a medium image or ensure the action version matches available Node.

Permissions errors on volume mounts. act mounts your repo into the container. If the container runs as root and creates files, those files end up owned by root on your host. --container-options "--user $(id -u):$(id -g)" can help, but some actions expect to run as root.

The workflow works on GitHub but not in act. Start by checking whether the issue is a missing tool in the runner image, a missing secret/variable, or an unsupported feature. Running act -v (verbose mode) shows the exact Docker commands and environment being configured, which usually reveals the problem.

Slow first runs. The first run pulls the runner image and all referenced actions from GitHub. Subsequent runs are much faster. Use --action-offline-mode after the initial run to avoid re-downloading everything.

Cache actions don't work. The actions/cache action talks to GitHub's cache API, which isn't available locally. The step won't crash, but it won't actually cache anything either. Your builds will be slower locally because of it. Accept this or restructure caching to be filesystem-based where possible.

A practical .actrc for most projects

Here's a starting-point configuration that works for most Node.js or Python projects:

# .actrc
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
--container-architecture linux/amd64
--action-offline-mode
--secret-file .secrets
--var-file .vars
-e .act-event.json

Pair it with a .secrets.example file that lists the required keys without values, so new team members know what to fill in:

# .secrets.example (commit this)
GITHUB_TOKEN=
NPM_TOKEN=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

When act isn't enough

act is best for catching syntax errors, validating job ordering, testing expression logic, and verifying that your build/test commands actually run. It's not a replacement for running workflows on GitHub. The features it can't emulate (OIDC, service containers, deployment environments, the full GitHub context) mean the final validation still has to happen on a real runner.

The value isn't perfect local parity. It's turning a 10-minute push-wait-fail cycle into a 30-second local iteration for the 80% of issues that are typos, incorrect paths, wrong environment variables, or broken shell commands. That's the use case where act earns its place in your toolchain.

Recommended for you

What's next in your stack.

GET TENKI

Smarter reviews. Faster builds. Start for Free in less than 2 min.