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
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:
docker versionInstallation
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:
brew install actWith Homebrew:
brew install actWith asdf-vm:
asdf plugin add actasdf install act latestasdf set --home act latestFrom the GitHub releases, checking the fingerprint published by the project:
VERSION=0.2.84curl -sSL -O "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz"curl -sSL -O "https://github.com/nektos/act/releases/download/v${VERSION}/checksums.txt"
# Must print "act_Linux_x86_64.tar.gz: OK" before any extractionsha256sum --check --ignore-missing checksums.txt
tar -xzf act_Linux_x86_64.tar.gz actsudo install -m 0755 act /usr/local/bin/actThe checksums.txt file is published by the release pipeline of act next to the
archives. If sha256sum --check does not answer OK, stop there: the
archive has been altered or the download is incomplete.
With Chocolatey:
choco install act-cliWith Scoop:
scoop install actChecking the installation:
act --versionact version 0.2.84The first run
On the first run, act asks which Docker image to use to simulate the GitHub runners. Three options are offered:
| Image | Size | Compatibility |
|---|---|---|
| Micro (~200 MB) | Very light | Limited (many tools are missing) |
| Medium (~500 MB) | Balanced | Good for most cases |
| Large (~18 GB) | Complete | Close to the GitHub environment |
To start with, pick Medium. You can change later.
# First run, pick Mediumactact 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:
actRunning 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.
# By file pathact -W .github/workflows/ci.yml
# By directory: every workflow of a folderact -W .github/workflows/Running one specific job
If your workflow holds several jobs, you can run just one:
# Run only the "test" jobact -j test[CI/test] 🚀 Start image=catthehacker/ubuntu:act-latest[CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-latest[CI/test] 🐳 docker create image=catthehacker/ubuntu:act-latest[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 succeededThe icons show the status: a star for the step in progress, a tick for success, a cross for failure.
# Run only the "build" job of the ci.yml workflowact -W .github/workflows/ci.yml -j buildSimulating the different events
GitHub Actions triggers on different events. act can simulate them:
# Simulate a push (the default)act push
# Simulate a pull requestact pull_request
# Simulate a manual workflowact workflow_dispatch
# Simulate a release eventact releaseListing 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.
# See every workflow and its jobsact -lStage Job ID Job name Workflow name Workflow file Events0 test test CI ci.yml push,pull_request1 build build CI ci.yml push,pull_requestEvery line shows the stage (the execution order), the job ID, its name, the parent workflow and the triggering events.
# See the jobs for one specific eventact -l pushact -l pull_requestVisualising the dependency graph
To see the execution order of the jobs graphically:
act -g ╭──────╮ │ 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:
# .secrets (key=value format)GITHUB_TOKEN=ghp_xxxxxxxxxxxxNPM_TOKEN=npm_xxxxxxxxxxAWS_ACCESS_KEY_ID=AKIAXXXXXXXXAWS_SECRET_ACCESS_KEY=xxxxxxxxxxThen use:
act --secret-file .secretsSecurity
Add .secrets to your .gitignore so you never commit it:
echo ".secrets" >> .gitignoreSecrets 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.
act -s GITHUB_TOKEN=ghp_xxxx -s MY_SECRET=valueEnvironment variables
For the ${{ vars.X }} values (non-sensitive), use a .vars file:
ENVIRONMENT=developmentAPI_URL=https://api-dev.example.comact --var-file .varsAdvanced configuration
The .actrc file
Create a .actrc file at the root of the project to save your options:
--secret-file .secrets--var-file .vars-P ubuntu-24.04=catthehacker/ubuntu:act-latest-P ubuntu-22.04=catthehacker/ubuntu:act-22.04--container-architecture linux/amd64Every line corresponds to an option of the act command.
Custom Docker images
act uses Docker images imitating the GitHub runners. By default, those images are lighter but less complete. For more compatibility:
# Use a more complete imageact -P ubuntu-24.04=catthehacker/ubuntu:act-latest
# Or your own imageact -P ubuntu-24.04=my-registry/my-image:tagVerbose 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.
# Detailed logsact -vlevel=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:
# Structured logs, consumable by an analysis toolact --json
# Enable the GitHub Actions debug logs (::debug::)act -s ACTIONS_STEP_DEBUG=true -s ACTIONS_RUNNER_DEBUG=trueDry-run
To see what would run without actually running it:
act -n*DRYRUN* [CI/test] ⭐ Run Set up job*DRYRUN* [CI/test] 🚀 Start image=catthehacker/ubuntu:act-latest*DRYRUN* [CI/test] 🐳 docker pull image=catthehacker/ubuntu:act-latest*DRYRUN* [CI/test] ✅ Success - Set up job*DRYRUN* [CI/test] ⭐ Run Main Checkout*DRYRUN* [CI/test] ✅ Success - Main Checkout*DRYRUN* [CI/test] 🏁 Job succeededThe *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:
| Limitation | Explanation |
|---|---|
| Limited events | schedule, deployment and page_build are not supported |
| Limited GitHub API | No access to the GitHub API as in production |
| Docker services | Service containers can behave differently |
| Cache | actions/cache works partially (local storage) |
| Artifacts | actions/upload-artifact creates local files, with no API |
| Marketplace | Some third-party actions are incompatible |
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.
# Run a precise job with detailed logsact -j build -v
# Keep the container of a successful run to inspect itact --reuseA workflow with a matrix
act automatically runs every combination of the matrix in parallel:
jobs: build: runs-on: ubuntu-24.04 strategy: matrix: node: [18, 20, 22] steps: - run: echo "Testing Node.js ${{ matrix.node }}"act -W .github/workflows/matrix.yml[Matrix Build/build-1] 🚀 Start image=catthehacker/ubuntu:act-latest[Matrix Build/build-2] 🚀 Start image=catthehacker/ubuntu:act-latest[Matrix Build/build-3] 🚀 Start image=catthehacker/ubuntu:act-latest[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 22To run only one combination of the matrix:
# Filter by matrix valueact --matrix node:20Testing a workflow_dispatch with inputs
For a workflow with inputs:
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:
{ "inputs": { "environment": "staging" }}Then run:
act workflow_dispatch -e event.json[Deploy/deploy] ⭐ Run Main| Deploying to staging[Deploy/deploy] ✅ Success - Main[Deploy/deploy] 🏁 Job succeededFitting it into the development flow
A pre-push validation script
Create a Git hook to validate before every push:
#!/bin/bashecho "Validating the CI workflow..."act -n -W .github/workflows/ci.yml
if [ $? -ne 0 ]; then echo "The workflow has errors. Push cancelled." exit 1fi
echo "The workflow is valid"Do not forget to make the script executable:
chmod +x .git/hooks/pre-pushCombining it with actionlint
For a complete validation, combine the static validation and the execution:
# 1. Validate the syntax with actionlint (fast, no Docker)actionlint .github/workflows/*.yml
# 2. If it passes, validate the structure with act in dry-runact -n
# 3. If everything passes, run it for realactSee 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.
name: CIon: push: branches: [main] pull_request:
# No rights by default: every job asks for the minimumpermissions: {}
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..."# Visualise the structureact -g ╭──────╮ │ lint │ ╰──────╯ ⬇ ╭──────╮ │ test │ ╰──────╯ ⬇ ╭───────╮ │ build │ ╰───────╯# Run only the tests (with their dependencies)act -j testTroubleshooting
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.
| Problem | Cause | Fix |
|---|---|---|
| Cannot connect to Docker daemon | Docker not started, or permissions | docker info to check. On Linux: sudo usermod -aG docker $USER then log out and back in |
| Image not found / an 18 GB pull | The Large image selected by default | Use Medium: -P ubuntu-24.04=catthehacker/ubuntu:act-latest |
| Action not found | An incompatible Marketplace action | Check the compatibility on the act repository, use the Full image |
| exec format error (Mac M1/M2) | An x86 image on ARM | Add --container-architecture linux/amd64 |
| Secret not found | A missing or badly formatted .secrets file | Check the KEY=value format (no spaces) and --secret-file .secrets |
| Unsupported event | schedule and deployment are not implemented | Test on GitHub, act does not support every event |
| Partial actions/cache | Local storage only | Normal, 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.
| Command | Description |
|---|---|
act | Run every workflow (push event) |
act -l | List every job |
act -g | Show the dependency graph |
act -n | Dry-run (validation with no execution) |
act -j <job> | Run one specific job |
act pull_request | Simulate a pull request |
act workflow_dispatch -e event.json | Trigger it with inputs |
act -v | Verbose mode (a boolean flag, -vv adds nothing) |
act --secret-file .secrets | Load the secrets |
act --matrix node:20 | Filter one matrix combination |
act -P ubuntu-24.04=catthehacker/ubuntu:act-latest | Set the image |
act --container-architecture linux/amd64 | Force the architecture (Mac M1/M2) |
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
.secretsand.actrcto 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
- Official site: github.com/nektos/act
- Docker images: catthehacker/docker_images
- Changelog: Releases
Frequently asked questions
Definition
act is a command line tool that runs your GitHub Actions workflows locally on your machine, with no need to push the code to GitHub.
How does it work?
- act reads your YAML files in
.github/workflows/ - It creates Docker containers imitating the GitHub runners
- It runs the jobs as if they were running on GitHub
The main benefits
| Benefit | Description |
|---|---|
| Fast iteration | Tests in seconds instead of minutes |
| Savings | No GitHub Actions minutes consumed |
| Local debugging | Detailed logs, investigation inside the containers |
| Offline | Works with no internet connection (after the initial pull) |
A quick example
# Test the ci.yml workflow
act -W .github/workflows/ci.yml
# See what would run (dry-run)
act -n
Comparison
| Criterion | act (local) | GitHub Actions (cloud) |
|---|---|---|
| Environment | Local Docker containers | GitHub-hosted VMs |
| Images | ~500 MB (Medium) | ~18 GB (complete) |
| Latency | Seconds | Minutes (queue plus boot) |
| GitHub API | Limited | Complete |
| Secrets | A .secrets file |
GitHub Secrets |
| Cache | Local only | Shared between runs |
| Cost | Free (local Docker) | Billed minutes |
What act supports well
- YAML syntax and workflow structure
- Steps with
run:anduses: - Environment variables and secrets
- Build matrices
- Marketplace actions (most of them)
What act does not support (or only partially)
- The
schedule,deploymentandpage_buildevents actions/cache(local storage only)actions/upload-artifact(local files, no API)- Full access to the GitHub API
- Complex Docker services
Recommendation
Use act for: development, quick tests, debugging.
Always validate on GitHub: before merging to production.
Prerequisite
Docker has to be installed and running:
docker version
macOS
brew install act
Linux
Option 1: Homebrew
brew install act
Option 2: the GitHub release, with its checksum
VERSION=0.2.84
curl -sSL -O "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz"
curl -sSL -O "https://github.com/nektos/act/releases/download/v${VERSION}/checksums.txt"
sha256sum --check --ignore-missing checksums.txt
tar -xzf act_Linux_x86_64.tar.gz act
sudo install -m 0755 act /usr/local/bin/act
Option 3: asdf-vm
asdf plugin add act
asdf install act latest
asdf set --home act latest
Windows
Chocolatey
choco install act-cli
Scoop
scoop install act
Check
act --version
# act version 0.2.84
First run
On the first run, act asks which image to use:
| Image | Size | Use |
|---|---|---|
| Micro | ~200 MB | Basic tests |
| Medium | ~500 MB | Recommended for most cases |
| Large | ~18 GB | Maximum compatibility |
Method 1: a .secrets file (recommended)
Create a .secrets file at the root:
# .secrets
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
NPM_TOKEN=npm_xxxxxxxxxx
AWS_ACCESS_KEY_ID=AKIAXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxx
Usage:
act --secret-file .secrets
Method 2: the command line
act -s GITHUB_TOKEN=ghp_xxxx -s MY_SECRET=value
Method 3: environment variables
export GITHUB_TOKEN=ghp_xxxx
act
Non-sensitive values (.vars)
For ${{ vars.X }}:
# .vars
ENVIRONMENT=development
API_URL=https://api-dev.example.com
act --var-file .vars
Permanent configuration (.actrc)
# .actrc
--secret-file .secrets
--var-file .vars
Security
Add .secrets to your .gitignore:
echo ".secrets" >> .gitignore
Never commit secrets.
The available images
| Image | Size | Compatibility | Use |
|---|---|---|---|
| Micro | ~200 MB | Limited | Syntax tests only |
| Medium | ~500 MB | Good | Recommended for most cases |
| Large | ~18 GB | Excellent | Complex workflows |
Configuration in .actrc
# .actrc
-P ubuntu-24.04=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
The catthehacker images (recommended)
# Medium (default)
catthehacker/ubuntu:act-latest
catthehacker/ubuntu:act-22.04
# Full (more complete)
catthehacker/ubuntu:full-latest
catthehacker/ubuntu:full-22.04
Mac M1 and M2 (Apple Silicon)
Some images do not work natively on ARM:
# Force x86_64 emulation
act --container-architecture linux/amd64
Add it to .actrc to avoid repeating it.
On the command line
# Use a specific image for this run
act -P ubuntu-24.04=catthehacker/ubuntu:full-latest
The supported events
# Push (default)
act
act push
# Pull request
act pull_request
# Manual workflow
act workflow_dispatch
# Release
act release
# Issue
act issues
# Pull request review
act pull_request_review
Listing the workflows
# Every workflow and job
act -l
# For one specific event
act -l push
act -l pull_request
workflow_dispatch with inputs
For a workflow with inputs:
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, prod]
Create event.json:
{
"inputs": {
"environment": "staging"
}
}
Run it:
act workflow_dispatch -e event.json
The events that are NOT supported
schedule(cron)deploymentpage_buildrepository_dispatch(partially)
Verbosity
# Normal
act
# Verbose (detailed logs)
act -v
--verbose is a boolean switch, not a level: repeating the flag changes nothing to the output.
Dry-run (no execution)
# See what would run
act -n
Useful to validate the syntax before a real run.
Keeping the container for investigation
# The container stays after the run
act --reuse
# You can then get into it
docker exec -it <container_id> bash
Targeting one specific job
# Run only the "test" job
act -j test
# One specific job of a workflow
act -W .github/workflows/ci.yml -j build
The recommended debugging flow
# 1. Validate the syntax
actionlint .github/workflows/*.yml
# 2. Dry-run
act -n
# 3. Run with logs
act -v
# 4. On failure, keep the container
act --reuse -v
The GitHub debug variables
# Enable the GitHub debug logs
act -s ACTIONS_STEP_DEBUG=true
act -s ACTIONS_RUNNER_DEBUG=true
Diagnosis
# Check that Docker runs
docker info
# On an error, start Docker
sudo systemctl start docker # Linux
# Or open Docker Desktop # macOS/Windows
Cause 1: Docker is not started
Linux:
sudo systemctl start docker
sudo systemctl enable docker # Automatic start
macOS/Windows: open Docker Desktop.
Cause 2: permissions (Linux)
# Add the user to the docker group
sudo usermod -aG docker $USER
# IMPORTANT: log out and back in
# Or use newgrp
newgrp docker
# Check
docker info
Cause 3: the Docker socket is unreachable
# Check the socket
ls -la /var/run/docker.sock
# It must show something like:
# srw-rw---- 1 root docker ... /var/run/docker.sock
Cause 4: Docker Desktop not started (macOS/Windows)
Open the Docker Desktop application and wait for the icon to say "Running".
Final test
docker run hello-world
act --version
The default behaviour
act runs every combination of the matrix:
jobs:
test:
strategy:
matrix:
node: [18, 20, 22]
# act will run 3 jobs (node 18, 20, 22)
Seeing the combinations
act -l
# Shows every combination as a separate job
Running a single combination
# Filter by matrix value
act --matrix node:20
Matrices with include and exclude
strategy:
matrix:
node: [18, 20]
include:
- node: 22
experimental: true
exclude:
- node: 18
os: windows-2022
act honours include and exclude.
Limiting the combinations (a trick)
To test quickly, create a test workflow file:
# .github/workflows/ci-local.yml
jobs:
test:
strategy:
matrix:
node: [20] # A single version
act -W .github/workflows/ci-local.yml
Performance
Large matrices (3 operating systems by 4 versions means 12 jobs) can take a long time. Prefer testing one combination locally, then validating the complete matrix on GitHub.
The complete validation flow
# 1. Static validation (syntax, typos, references)
actionlint .github/workflows/*.yml
# 2. Dry-run (structure, resolution)
act -n
# 3. Real execution
act -v
What each tool catches
| Tool | Catches |
|---|---|
| actionlint | YAML syntax, typos in uses:, invalid variables, permissions |
| act -n | Action resolution, workflow structure |
| act | Runtime errors, logic, scripts |
An automatic pre-push hook
#!/bin/bash
# .git/hooks/pre-push
echo "Validating the workflows..."
# Step 1: actionlint
if ! actionlint .github/workflows/*.yml; then
echo "Syntax errors. Push cancelled."
exit 1
fi
# Step 2: act dry-run
if ! act -n -W .github/workflows/ci.yml; then
echo "Structure errors. Push cancelled."
exit 1
fi
echo "Workflows are valid"
Make it executable:
chmod +x .git/hooks/pre-push
Installing actionlint
# macOS/Linux
brew install actionlint
# Go
go install github.com/rhysd/actionlint/cmd/actionlint@latest
See the complete guide: actionlint