Skip to content
Français
Français
CI/CD & Automatisation medium

act: running GitHub Actions workflows locally

45 min read

Read this page in French

Test your GitHub Actions workflows in seconds, without pushing to GitHub. act runs your workflows locally through Docker, which allows fast iterative development. You save your Actions minutes and avoid the frustrating push, wait, fail, fix, push loop.

The architecture of act

The act architecture: local workflow, Docker, runner container, results

act reads your YAML files in .github/workflows/, creates Docker containers imitating the GitHub runners, and runs the jobs as if they were running on GitHub. The results (logs, artifacts, status) are printed locally.

What you will learn

  • Install act on Linux, macOS and Windows
  • Run workflows with different events (push, PR, workflow_dispatch)
  • Handle secrets and environment variables
  • Configure the optimal Docker images
  • Debug efficiently with the advanced options

Prerequisites

Before installing act, you need:

  • Docker installed and running (act creates containers)
  • A terminal (bash, zsh, PowerShell)
  • A project with workflows in .github/workflows/

To check that Docker works:

Terminal window
docker version

Installation

act is a static Go binary with no system dependency: installing it comes down to fetching an executable and putting it in the PATH. The package managers below do that work for you and handle the updates; the manual method is mostly useful when you have to pin a precise version, so that a whole team tests with the same behaviour for instance.

With Homebrew:

Terminal window
brew install act

Checking the installation:

Terminal window
act --version
Expected output
act version 0.2.89

The first run

On the first run, act asks which Docker image to use to simulate the GitHub runners. Three options are offered:

ImageSizeCompatibility
Micro (~200 MB)Very lightLimited (many tools are missing)
Medium (~500 MB)BalancedGood for most cases
Large (~18 GB)CompleteClose to the GitHub environment

To start with, pick Medium. You can change later.

Terminal window
# First run, pick Medium
act

act saves your choice into ~/.actrc for the next runs.

Basic usage

Running the default workflow

The act command with no argument simulates a push event and runs the workflows answering that event:

Terminal window
act

Running one specific workflow

The -W option expects a path, not the name: declared in the file. Point it at a precise file to run only that one, or at a directory to narrow the search down to a subset of workflows. Without -W, act sweeps the whole .github/workflows/, which quickly becomes annoying on a repository holding a dozen of them.

Terminal window
# By file path
act -W .github/workflows/ci.yml
# By directory: every workflow of a folder
act -W .github/workflows/

Running one specific job

If your workflow holds several jobs, you can run just one:

Terminal window
# Run only the "test" job
act -j test
Output
[CI/test] 🚀 Start image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[CI/test] 🐳 docker create image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[CI/test] ⭐ Run Main Checkout
[CI/test] ✅ Success - Main Checkout
[CI/test] ⭐ Run Main Run tests
| Tests passed!
[CI/test] ✅ Success - Main Run tests
[CI/test] 🏁 Job succeeded

The icons show the status: a star for the step in progress, a tick for success, a cross for failure.

Terminal window
# Run only the "build" job of the ci.yml workflow
act -W .github/workflows/ci.yml -j build

Simulating the different events

GitHub Actions triggers on different events. act can simulate them:

Terminal window
# Simulate a push (the default)
act push
# Simulate a pull request
act pull_request
# Simulate a manual workflow
act workflow_dispatch
# Simulate a release event
act release

Listing the available workflows

Before starting anything, act -l gives the inventory of what act actually understood from your files. It is the first diagnosis reflex: a job missing from that list will never run, usually because its triggering event does not match the one being simulated, or because the YAML file is in the wrong place.

Terminal window
# See every workflow and its jobs
act -l
Output
Stage Job ID Job name Workflow name Workflow file Events
0 test test CI ci.yml push,pull_request
1 build build CI ci.yml push,pull_request

Every line shows the stage (the execution order), the job ID, its name, the parent workflow and the triggering events.

Terminal window
# See the jobs for one specific event
act -l push
act -l pull_request

Visualising the dependency graph

To see the execution order of the jobs graphically:

Terminal window
act -g
Output
╭──────╮
│ test │
╰──────╯
⬇
╭───────╮
│ build │
╰───────╯

Useful to understand the needs: dependencies between jobs.

Handling secrets

Workflows often use secrets (${{ secrets.TOKEN }}). act offers several methods to supply them.

The .secrets file

Create a .secrets file at the root of the project:

Terminal window
# .secrets (key=value format)
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
NPM_TOKEN=npm_xxxxxxxxxx
AWS_ACCESS_KEY_ID=AKIAXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxx

Then use:

Terminal window
act --secret-file .secrets

Security

Add .secrets to your .gitignore so you never commit it:

Terminal window
echo ".secrets" >> .gitignore

Secrets on the command line

This form suits a one-off test, not regular use: the value goes into the shell history and stays visible in ps during the run. If you omit the value (-s GITHUB_TOKEN), act takes the environment variable of the same name, and failing that asks you interactively. That form avoids writing the secret in plain text on the command line.

Terminal window
act -s GITHUB_TOKEN=ghp_xxxx -s MY_SECRET=value

Environment variables

For the ${{ vars.X }} values (non-sensitive), use a .vars file:

.vars
ENVIRONMENT=development
API_URL=https://api-dev.example.com
Terminal window
act --var-file .vars

Advanced configuration

The .actrc file

Create a .actrc file at the root of the project to save your options:

.actrc
--secret-file .secrets
--var-file .vars
-P ubuntu-24.04=catthehacker/ubuntu:act-24.04@sha256:c58e2b364da03b0c804c7d660f2ecbedf2f221a382b9baa0b344b0144780ff43
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04@sha256:3488f78aa97770c8d5d835f2913c1a782d3121ad3f2aafb17054fb1d554ea4e1
--container-architecture linux/amd64

Every line corresponds to an option of the act command.

Pin the runner image just like an action. The act-latest tag that most tutorials recommend is mutable: the image behind it changes without notice, and your local run stops reproducing your teammates'. The act-24.04 tag matches the ubuntu-24.04 label your workflows ask for, and the digest following it freezes the image exactly. This course applies that rule to actions; it applies just as much to the container running them.

Custom Docker images

act uses Docker images imitating the GitHub runners. By default, those images are lighter but less complete. For more compatibility:

Terminal window
# Use a more complete image
act -P ubuntu-24.04=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
# Or your own image
act -P ubuntu-24.04=my-registry/my-image:tag

Verbose mode for debugging

--verbose is a boolean switch, not a level: act has a single verbosity step, and repeating the flag changes nothing to the output. That mode mostly serves to understand what act loaded and which Git context it inferred from the local repository, two frequent causes of divergence between a local run and the real runner.

Terminal window
# Detailed logs
act -v
Excerpt of the verbose output
level=debug msg="Loading workflows from '.github/workflows'"
level=debug msg="Found workflow 'ci.yml'"
level=debug msg="Planning job: test"
level=debug msg="Job.Steps: Checkout"
level=debug msg="Job.Steps: Run tests"
level=debug msg="using github ref: refs/heads/main"
level=debug msg="Detected CPUs: 16"

The -v mode reveals the loading of the workflows, the planning of the jobs and the detected Git environment variables. To go further, two complementary options exist:

Terminal window
# Structured logs, consumable by an analysis tool
act --json
# Enable the GitHub Actions debug logs (::debug::)
act -s ACTIONS_STEP_DEBUG=true -s ACTIONS_RUNNER_DEBUG=true

Dry-run

To see what would run without actually running it:

Terminal window
act -n
Dry-run output
*DRYRUN* [CI/test] ⭐ Run Set up job
*DRYRUN* [CI/test] 🚀 Start image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
*DRYRUN* [CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
*DRYRUN* [CI/test] ✅ Success - Set up job
*DRYRUN* [CI/test] ⭐ Run Main Checkout
*DRYRUN* [CI/test] ✅ Success - Main Checkout
*DRYRUN* [CI/test] 🏁 Job succeeded

The *DRYRUN* prefix says that no container is created. Useful to validate the syntax and the structure before a real run.

The limitations of act

act cannot reproduce 100 % of the GitHub Actions environment. Here are the main limitations to know about:

LimitationExplanation
No scheduleract schedule does run a workflow declaring on: schedule, but nothing fires it at the stated time: the cron: expression is never evaluated
Limited GitHub APINo access to the GitHub API as in production
Docker servicesService containers can behave differently
Cache--cache-server-path returns a genuine cache-hit on the second run, but the cache stays local to your machine
ArtifactsThe built-in server (--artifact-server-path) does implement the API, but rejects upload-artifact v7 and download-artifact v8: stay on v5 or v6 locally
MarketplaceSome third-party actions are incompatible
Trigger filters ignoredbranches:, tags: and paths: are not evaluated: the job runs whatever the local branch or the changed files

When to use act:

  • Syntax and structure tests
  • Iterative development of workflows
  • Validation of the commands and scripts
  • Debugging logic problems

When NOT to rely on act:

  • The final validation (always test on GitHub)
  • Integration tests with the GitHub API
  • Workflows with complex services

The common use cases

Interactive debugging

The --reuse (-r) flag prevents act from deleting the container at the end of a workflow that succeeded, which lets you both keep the state between two runs and get into the container with docker exec to inspect the file system. Mind the trade-off: the container you keep holds the files of the previous run, so a build passing thanks to a leftover artefact gives you false confidence. Clean it up before the final validation.

Terminal window
# Run a precise job with detailed logs
act -j build -v
# Keep the container of a successful run to inspect it
act --reuse

A workflow with a matrix

act automatically runs every combination of the matrix in parallel:

.github/workflows/matrix.yml
jobs:
build:
runs-on: ubuntu-24.04
strategy:
matrix:
node: [18, 20, 22]
steps:
- run: echo "Testing Node.js ${{ matrix.node }}"
Terminal window
act -W .github/workflows/matrix.yml
Output (3 parallel jobs)
[Matrix Build/build-1] 🚀 Start image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[Matrix Build/build-2] 🚀 Start image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[Matrix Build/build-3] 🚀 Start image=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
[Matrix Build/build-1] ⭐ Run Main
| Testing Node.js 18
[Matrix Build/build-2] ⭐ Run Main
| Testing Node.js 20
[Matrix Build/build-3] ⭐ Run Main
| Testing Node.js 22

To run only one combination of the matrix:

Terminal window
# Filter by matrix value
act --matrix node:20

Testing a workflow_dispatch with inputs

For a workflow with inputs:

.github/workflows/deploy.yml
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options: [dev, staging, prod]
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- run: echo "Deploying to ${{ github.event.inputs.environment }}"

Create a JSON event file:

event.json
{
"inputs": {
"environment": "staging"
}
}

Then run:

Terminal window
act workflow_dispatch -e event.json
Output
[Deploy/deploy] ⭐ Run Main
| Deploying to staging
[Deploy/deploy] ✅ Success - Main
[Deploy/deploy] 🏁 Job succeeded

Fitting it into the development flow

A pre-push validation script

Create a Git hook to validate before every push:

.git/hooks/pre-push
#!/bin/bash
echo "Validating the CI workflow..."
act -n -W .github/workflows/ci.yml
if [ $? -ne 0 ]; then
echo "The workflow has errors. Push cancelled."
exit 1
fi
echo "The workflow is valid"

Do not forget to make the script executable:

Terminal window
chmod +x .git/hooks/pre-push

Combining it with actionlint

For a complete validation, combine the static validation and the execution:

Terminal window
# 1. Validate the syntax with actionlint (fast, no Docker)
actionlint .github/workflows/*.yml
# 2. If it passes, validate the structure with act in dry-run
act -n
# 3. If everything passes, run it for real
act

See the actionlint guide for the static validation of workflows.

A complete example

The workflow below chains three jobs linked by needs: and applies the hardening rules of this course: actions pinned by SHA with the tag as a comment, permissions: {} at workflow level then the strict minimum per job, persist-credentials: false on the checkout and a pinned runner version. It is that combination act lets you check locally before the first push.

.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
# No rights by default: every job asks for the minimum
permissions: {}
jobs:
lint:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Lint
run: echo "Linting..."
test:
runs-on: ubuntu-24.04
needs: lint
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Run tests
run: echo "Tests passed!"
build:
runs-on: ubuntu-24.04
needs: test
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Build
run: echo "Building..."
Terminal window
# Visualise the structure
act -g
Dependency graph
╭──────╮
│ lint │
╰──────╯
⬇
╭──────╮
│ test │
╰──────╯
⬇
╭───────╮
│ build │
╰───────╯
Terminal window
# Run only the tests (with their dependencies)
act -j test

Troubleshooting

Almost every act failure falls into two categories. Either Docker does not answer as expected (stopped daemon, socket permissions, processor architecture), or the runner image does not hold the tool the workflow calls. The error message rarely points at the real cause, so start by checking docker info and the image actually used, printed at the top of every job.

ProblemCauseFix
Cannot connect to Docker daemonDocker not started, or permissionsdocker info to check. On Linux: sudo usermod -aG docker $USER then log out and back in
Image not found / an 18 GB pullThe Large image selected by defaultUse Medium: -P ubuntu-24.04=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…
Action not foundAn incompatible Marketplace actionCheck the compatibility on the act repository, use the Full image
exec format error (Mac M1/M2)An x86 image on ARMAdd --container-architecture linux/amd64
Secret not foundA missing or badly formatted .secrets fileCheck the KEY=value format (no spaces) and --secret-file .secrets
The job runs although the filter should exclude itbranches: and paths: are not evaluatedExpected behaviour: check the filter on GitHub itself, with gh run list
Partial actions/cacheLocal storage onlyNormal, the cache works but is not shared between runs

Cheatsheet

The first four lines cover 90 % of the daily usage. The following ones serve when something does not go as planned or when you test a particular case. Remember the order above all: act -l to check what is detected, act -n to validate the structure with no container, then act -j to run only the job you care about.

CommandDescription
actRun every workflow (push event)
act -lList every job
act -gShow the dependency graph
act -nDry-run (validation with no execution)
act -j <job>Run one specific job
act pull_requestSimulate a pull request
act workflow_dispatch -e event.jsonTrigger it with inputs
act -vVerbose mode (a boolean flag, -vv adds nothing)
act --secret-file .secretsLoad the secrets
act --matrix node:20Filter one matrix combination
act -P ubuntu-24.04=catthehacker/ubuntu:act-24.04@sha256:c58e2b36…Set the image
act --container-architecture linux/amd64Force the architecture (Mac M1/M2)

Check your understanding

Make sure the essentials of this guide are yours. The questions only cover what was explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Key points

  • Fast iteration: test your workflows in seconds without pushing to GitHub
  • Saved resources: no Actions minutes consumed during development
  • Efficient debugging: verbose mode (-v, a single level) and the option to keep the containers (--reuse)
  • Configuration: use .secrets and .actrc to centralise your options
  • Images: start with Medium (~500 MB), move to Large if needed
  • Complete validation: combine act with actionlint to catch every error
  • Security: keep act up to date (regular fixes on x/crypto and SELinux)
  • Limitation: act is not a complete replacement, always validate on GitHub before merging

Resources

Frequently asked questions

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain this site for free, with no ads, no ad profiling and no account to create. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn