Running GitHub Actions Locally with Act
Developing GitHub Actions workflows can be a frustrating experience of trial and error. A typical scenario involves writing a workflow, pushing it to a feature branch, discovering syntax errors or mistakes, pushing multiple fixes, and finally squashing dozens of "fix workflow" commits before merging.
The problem isn't just the wait times between runs, but the inability to debug workflows locally. While you can test individual scripts or commands on your machine, you can't verify how they'll behave in GitHub's environment until you push them.
Act significantly improves this experience by enabling you to run and debug GitHub Actions workflows locally by simulating GitHub's environment using Docker containers. Instead of pushing changes and waiting for results, you can test workflows on your machine, catch and fix issues immediately, and only push once you're confident everything works.
This tutorial will walk you through setting up and using Act for turning workflow development from a frustrating remote debugging process into a seamless local development exercise.
Let's get started!
Prerequisites
To follow this tutorial, you need a recent version of Docker installed on your system. Additionally, ensure that the Docker daemon is running before proceeding. You can check its status with:
sudo systemctl status docker
Place your right thumb on the fingerprint reader
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: disabled)
Drop-In: /usr/lib/systemd/system/service.d
└─10-timeout-abort.conf, 50-keep-warm.conf
Active: active (running) since Wed 2025-02-12 20:41:10 WAT; 18h ago
Invocation: 2a7d5b896ebc4528ada8349330f91b09
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 82184 (dockerd)
Tasks: 22
Memory: 243.6M (peak: 651.5M swap: 12M swap peak: 13.8M)
CPU: 3min 15.813s
CGroup: /system.slice/docker.service
└─82184 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
If the Active status is not active (running)
, start Docker with:
sudo systemctl start docker
With Docker properly set up, you're ready to install and use Act.
Installing the Act CLI
To get started with Act, run the command below to install the latest version available for your operating system:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
Alternatively, you can explore other installation methods or download a pre-built binary from the GitHub releases page.
You can confirm that act
is installed correctly by running:
act --version
act version 0.2.74
How does Act work?
Act allows you to run GitHub Actions workflows locally by simulating the CI/CD
environment within Docker containers. When executed, Act first reads your
workflow files from .github/workflows/
and analyzes which actions need to run
based on the specified triggers and conditions.
The tool then interacts with Docker to prepare the necessary container images.
It either pulls pre-built images (like catthehacker/ubuntu:act-latest
) or
builds custom ones as defined in your workflows.
Throughout execution, Act maintains a local context that simulates GitHub's environment, allowing workflows to run as they would on GitHub's infrastructure. This includes managing workspace directories, setting up action runners, handling event payloads, and providing GitHub-compatible environment variables and contexts.
The key difference from GitHub's actual environment is that Act runs everything locally, using Docker to isolate and execute actions while providing similar capabilities to GitHub's hosted runners. This allows developers to test and debug their workflows locally before pushing changes to their repository.
Running Act for the first time
Before running act
, you need a project that contains GitHub Actions workflows.
To follow along,
fork this sample project,
which includes a simple Go application with pre-configured workflows, and clone
it to your machine:
git clone https://github.com/<your_username>/act-tutorial
Then change into the project directory:
cd act-tutorial
You can inspect the workflows in the repository by running:
act --list
Stage Job ID Job name Workflow name Workflow file Events
0 lint lint CI ci.yml pull_request
0 test test Run tests test.yml push
This output shows the available workflows and their corresponding jobs. The
lint
job runs as part of the CI
workflow when a pull request is made, while
the test
job runs within the Run tests
workflow upon a push to the
repository.
To understand these jobs better, let's examine the workflow files:
name: CI
on:
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.64.2
name: Run tests
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Run tests
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
These are simple workflows that lints pull requests to maintain code quality, and runs the project's tests on push events to verify that changes do not break the application.
By default, running act
without any arguments executes all workflows
associated with push
events. Since the "CI" workflow only runs on pull
requests, only the "Run tests" workflow will execute.
To see this in action, run:
act
When running act
for the first time, you'll be prompted to select a default
Docker image:
? Please choose the default image you want to use with act:
- Large size image: ca. 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images
- Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most actions
- Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions
Default image and other options can be changed manually in /home/ayo/.config/act/actrc (please refer to https://github.com/nektos/act#configuration for additional information about file stru
cture) [Use arrows to move, type to filter, ? for more help]
Large
> Medium
Micro
The Medium image is recommended as it supports most actions while keeping
the disk usage manageable. Once selected, act
downloads the image and executes
the workflows.
Once the image is set up, act
will begin running the workflow. You'll see logs
similar to the following:
INFO[0000] Using docker host 'unix:///var/run/docker.sock', and daemon socket 'unix:///var/run/docker.sock'
. . .
[Run tests/test] ⭐ Run Main Run tests
[Run tests/test] 🐳 docker exec cmd=[bash -e /var/run/act/workflow/2] user= workdir=
| === RUN TestCountWords
| === RUN TestCountWords/basic_counting
| === RUN TestCountWords/empty_string
| === RUN TestCountWords/mixed_case
| --- PASS: TestCountWords (0.00s)
| --- PASS: TestCountWords/basic_counting (0.00s)
| --- PASS: TestCountWords/empty_string (0.00s)
| --- PASS: TestCountWords/mixed_case (0.00s)
| PASS
| coverage: 50.0% of statements
| ok github.com/betterstack-community/act-tutorial 1.027s coverage: 50.0% of statements
[Run tests/test] ✅ Success - Main Run tests
[Run tests/test] ⭐ Run Post Setup Go
[Run tests/test] 🐳 docker exec cmd=[/opt/acttoolcache/node/18.20.5/x64/bin/node /var/run/act/actions/actions-setup-go@v5/dist/cache-save/index.js] user= workdir=
| [command]/opt/hostedtoolcache/go/1.24.0/x64/bin/go env GOMODCACHE
| [command]/opt/hostedtoolcache/go/1.24.0/x64/bin/go env GOCACHE
| /root/go/pkg/mod
| /root/.cache/go-build
| [command]/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/ayo/dev/betterstack/demo/act-tutorial --files-from manifest.txt --use-compress-program zstdmt
| Cache Size: ~26 MB (27232261 B)
| Cache saved successfully
| Cache saved with the key: setup-go-Linux-x64-ubuntu20-go-1.24.0-4b11ccdf1e7dd226ff50ddececac0c8ee38714da25a0be96f5d1e7fbde9ecde2
[Run tests/test] ✅ Success - Post Setup Go
[Run tests/test] ⭐ Run Complete job
[Run tests/test] Cleaning up container for job test
[Run tests/test] ✅ Success - Complete job
[Run tests/test] 🏁 Job succeeded
Each step in the workflow is prefixed with [Run tests/test]
, indicating which
job is being executed. The test
job successfully runs go test
to verify code
correctness, and reports code coverage.
If you're actively modifying workflows and want act to re-run automatically when files change, enable watch mode with:
act --watch
This will detect modifications and re-execute the relevant workflows, making it easier to iterate on your CI/CD configurations.
In the next section, we'll explore how to customize the runner images used by Act to match the GitHub-hosted environment or your own requirements.
Customizing Act's runner images
Act allows you to define and modify its behavior using actrc
configuration
files, which persist your preferred settings across runs, ensuring that you
don't have to specify them manually each time.
If you selected the Medium image in the previous section, you'll see the
configuration file in $XDG_CONFIG_HOME/act/actrc
(~/.config/act/actrc
on
Linux) with the following contents:
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
-P ubuntu-20.04=catthehacker/ubuntu:act-20.04
-P ubuntu-18.04=catthehacker/ubuntu:act-18.04
Each entry follows the format:
-P <runner-name>=<docker-image>
These mappings determine which Docker images Act will use when simulating GitHub Actions runners. For example:
-P ubuntu-latest=catthehacker/ubuntu:act-latest
ensures that workflows targeting ubuntu-latest run inside thecatthehacker/ubuntu:act-latest
container.- Similarly, the
ubuntu-22.04
,ubuntu-20.04
, andubuntu-18.04
runners are mapped to their respective container images.
The catthehacker
images are designed to resemble GitHub-hosted runners and
provide a compatible environment for most workflows. However, they do not
include all pre-installed tools available in GitHub Actions' official runners so
full compatibility with your workflows isn't guaranteed.
Overriding default runner images
If your workflow requires a custom environment or additional dependencies, you
can override the default images in your actrc
file.
For example, to replace the ubuntu-latest
runner with a custom image, you can
use:
-P ubuntu-latest=my-custom-image:latest
Or you can specify a different image dynamically when running act
:
act -P ubuntu-latest=my-custom-image:latest
This flexibility allows you to use your own Docker images that better match your production environment or include pre-installed tools needed by your actions.
Windows and macOS runners
GitHub Actions supports Windows and macOS runners (such as windows-latest
and
macos-latest
), but Act only supports Ubuntu-based containers. Attempting to
use act with non-Ubuntu runners will result in an "unsupported platform"
message, and those jobs will be skipped.
If you're running Act on a Windows or macOS host, you can bypass this limitation
by running workflows directly on your host machine instead of inside Docker. To
do this, use -self-hosted
as the runner value:
act -P windows-latest=-self-hosted # Run workflows on Windows host
act -P macos-latest=-self-hosted # Run workflows on macOS host
This approach also works for Ubuntu-based workflows if you want to avoid Docker overhead:
act -P ubuntu-latest=-self-hosted # Run workflows on Ubuntu host
Beyond customizing runner images, Act offers additional configuration options to fine-tune its behavior. In the next section, we'll explore how to control workflow execution and specify event payloads.
Customizing Act's behavior
Act provides various options to tailor its execution, allowing you to control events, workflows, jobs, and individual steps. These customizations help simulate different GitHub Actions triggers and improve efficiency when testing workflows locally.
By default, Act runs workflows as if they were triggered by a push
event (as
we saw earlier). However, you can specify different
event types
when running workflows:
act push # equivalent to act
act pull_request # simulates a pull_request event
act workflow_dispatch # similates a workflow_dispatch event
To see all workflows that would be triggered for a given event, use the
-l/--list
flag:
act --list
Stage Job ID Job name Workflow name Workflow file Events
0 lint lint CI ci.yml pull_request
0 test test Run tests test.yml push
You can run the lint
job by simulating the pull_request
event with:
act pull_request
The CI/lint
job will run and it should complete successfully:
. . .
[CI/lint] ⭐ Run Post golangci-lint
[CI/lint] 🐳 docker exec cmd=[/opt/acttoolcache/node/18.20.5/x64/bin/node /var/run/act/actions/golangci-golangci-lint-action@v6/dist/post_run/index.js] user= workdir=
| Cache hit occurred on the primary key golangci-lint.cache-Linux-2876-524aac7ed694dd8236ed1e3a9988a38036156b49, not saving cache.
[CI/lint] ✅ Success - Post golangci-lint
[CI/lint] ⭐ Run Post actions/setup-go@v5
[CI/lint] 🐳 docker exec cmd=[/opt/acttoolcache/node/18.20.5/x64/bin/node /var/run/act/actions/actions-setup-go@v5/dist/cache-save/index.js] user= workdir=
| [command]/opt/hostedtoolcache/go/1.24.0/x64/bin/go env GOMODCACHE
| [command]/opt/hostedtoolcache/go/1.24.0/x64/bin/go env GOCACHE
| /root/go/pkg/mod
| /root/.cache/go-build
| [command]/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/ayo/dev/betterstack/demo/act-tutorial --files-from manifest.txt --use-compress-program zstdmt
| Cache Size: ~26 MB (27268645 B)
| Cache saved successfully
| Cache saved with the key: setup-go-Linux-x64-ubuntu20-go-1.24.0-4b11ccdf1e7dd226ff50ddececac0c8ee38714da25a0be96f5d1e7fbde9ecde2
[CI/lint] ✅ Success - Post actions/setup-go@v5
[CI/lint] ⭐ Run Complete job
[CI/lint] Cleaning up container for job lint
[CI/lint] ✅ Success - Complete job
[CI/lint] 🏁 Job succeeded
Using custom event payloads
Some workflows depend on event-specific properties that Act cannot infer automatically. To fully simulate these events, you need to provide a custom event payload file.
For example, to simulate a push
event with a tag, you can create a JSON file
with the following payload:
{
"ref": "refs/tags/v1.0.0",
}
Then, pass this file to Act using the -e/--eventpath
flag:
act --eventpath payload.json
Filtering workflows and jobs
Another common need is narrowing down what workflows or jobs should be ran which can be achieved in various ways.
For instance, to execute only specific workflows, use the -W/--workflows
flag:
act --workflows '.github/workflows/ci.yml' # run all jobs in the ci.yml file
act --workflows '.github/workflows/checks' # run all workflows in the checks subdirectory
You can also run specific jobs with the -j/--job
flag:
act --job lint
Skipping jobs and steps
In some cases, certain jobs should only run on GitHub Actions and be skipped when using Act locally.
You can prevent Act from running specific jobs or steps by leveraging custom event properties or environment variables.
Let's start with skipping jobs:
on: push
jobs:
deploy:
if: ${{ !github.event.act }} # skip during local actions testing
runs-on: ubuntu-latest
steps:
. . .
You can conditionally skip a job in Act by adding a custom event property
(github.event.act
) and checking for it in the job's if
condition.
Then, in your payload.json
file, set the act
property:
{
"act": true
}
When running act
, ensure to provide the payload file:
act --eventpath payload.json
This ensures that the deploy job is skipped when running Act locally but runs on GitHub Actions.
If you'd like to skip a step when running workflows locally, use an environment
variable (such as env.ACT
), which can be set dynamically:
- name: Some step
if: ${{ !env.ACT }} # skip this step when running locally
run: |
...
Then run act
while setting the ACT
environment variable:
act --env ACT=true
Note that this environmental variable approach cannot work for skipping jobs
because the env
context isn't available in job
-level if
conditions.
Specifying variables and secrets
Act provides multiple ways to handle environment variables, repository variables, and secrets when testing GitHub Actions workflows locally. Let's look at each one in turn.
Environmental variables
Environment variables are configuration values accessible using
${{ env.VARIABLE_NAME }}
in workflows or $VARIABLE_NAME
in scripts. Act
allows you to define these variables using the --env
flag or an .env
file.
act --env NODE_ENV=production --env DEBUG=true
act --env-file .env
NODE_ENV=production
DEBUG=true
PORT=3000
Repository variables
In GitHub Actions, repository variables provide reusable configuration values
across workflows, accessible via ${{ vars.VARIABLE }}
. Act supports these
using --var
or --var-file
.
act --var DEPLOY_ENV=staging --var API_VERSION=v2
You can specify repository variables using a file (formatted like .env
):
act --var-file .vars
Secrets
Secrets are encrypted variables used for sensitive data like API keys,
credentials, or passwords. They are specified in the GitHub repository settings
and accessed via ${{ secrets.SECRET_NAME }}
in workflow files.
Since Act runs locally, you must explicitly provide secrets when testing workflows, and there are three ways to achieve this:
1. Secure prompt
Running Act with --secret SECRET_NAME
(without assigning a value) prompts you
to enter the secret securely:
act --secret API_TOKEN
2. Passing secrets directly (not recommended)
You can also specify a secret inline, but this exposes it in shell history:
act --secret API_TOKEN=secret1234
- Using a secrets file
For multiple secrets, use a secrets file in .env
format:
act --secret-file .secrets
API_TOKEN=secret1234
SOME_PASSWORD=s3cureP@ss
Using GITHUB_TOKEN in Act
On GitHub, a GITHUB_TOKEN
is automatically generated for each workflow
execution. However, when running Act locally, this token is not available by
default, which may cause authentication errors.
To prevent this, you need to supply a Personal Access Token (PAT) using any of the methods mentioned above.
act --secret GITHUB_TOKEN # prompt for value securey
If you have the GitHub CLI installed, you can automatically retrieve and pass your authentication token to Act with:
act --secret GITHUB_TOKEN="$(gh auth token)"
Note that passing an actual GITHUB_TOKEN
would make act
able to access and
modify your GitHub resources so use with caution.
Working with GitHub Action artifacts
GitHub Action artifacts allow jobs to share data within a workflow and retain files after execution. They are commonly used for:
- Sharing build outputs between jobs
- Storing test results and coverage reports
- Preserving logs for debugging and analysis
However, when testing workflows locally with Act, special configuration is required because GitHub's artifact storage system isn't available.
To use artifacts with Act, specify a local storage path using the
--artifact-server-path
flag:
act --artifact-server-path <path_to_artifact_dir>
Without this flag, jobs using actions/upload-artifact
or
actions/download-artifact
will fail with a ACTIONS_RUNTIME_TOKEN
error.
To see artifact handling in action, add the following steps to the test
job in
your workflow:
. . .
jobs:
. . .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
. . .
- uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: ./coverage.txt
Then run act
and provide a directory where the artifacts should be uploaded
to:
act --artifact-server-path /tmp/artifacts
Once the workflow completes, the artifact (in this case, the coverage report) is stored in the specified directory. You can inspect the contents using:
tree /tmp/artifacts
You'll see that the artifact was stored in a zip file which you can subsequently unzip to inspect its contents.
/tmp/artifacts/
└── 1
└── code-coverage-report
└── code-coverage-report.zip
This local artifact storage allows you to easily test multi-job workflows
depending on upload-artifact
and download-artifact
without modification
before running on GitHub.
Speeding up Act
By default, Act always pulls the Docker images for the specified runner even if its already present. If you want to speed up the workflow runs, you can enable offline mode which has the following behaviors:
- It prevents downloading container images if they already exist locally.
- If an action has been used before and is cached, offline runs can used these cached resources.
- Since Act won't make repeated requests to GitHub for action downloads, it helps avoid hitting GitHub's API rate limits.
- It eliminates timeouts caused by slow or unstable internet connections.
To activate offline mode, provide the --action-offline-mode
:
act --action-offline-mode
Setting defaults in actrc
Act allows you to define default settings in an actrc
configuration file,
enabling persistent configurations without needing to specify options manually
each time.
The syntax of the configuration file is one argument per line, with no comments allowed:
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
-P ubuntu-20.04=catthehacker/ubuntu:act-20.04
-P ubuntu-18.04=catthehacker/ubuntu:act-18.04
Aside from the user-specific file located at $XDG_CONFIG_HOME/act/actrc
, you
can provide a project-specific .actrc
at your project root to customize Act's
behavior on a per-project basis:
For example:
-P ubuntu-latest=catthehacker/ubuntu:act-latest
--action-offline-mode
--artifact-server-path=/tmp/artifacts
--secret-file=.secrets
--env-file=.env
--watch
This configuration sets a default runner image, enables offline mode, defines local artifact storage, loads secrets and environment variables, and enables watch mode for automatic workflow re-runs.
You can then execute act
or specify the options for filtering workflows and
jobs as needed. Note that most Defaults in actrc
can be overridden per run
using command-line arguments:
act --artifact-server-path=./artifacts # override the default artifacts path
Example
To demonstrate some of the features we just covered, let's create a GitHub
Actions workflow that automatically generates a release when a new version tag
(v*
) is pushed to the repository:
name: Stable release
on:
push:
tags:
- v*
jobs:
release-counter:
name: Build and release binary
runs-on: ubuntu-latest
if: github.ref_type == 'tag'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setting up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Running Go Build
run: go build -o counter
- name: Zipping binary
run: zip counter.zip counter
- name: Create stable release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: true
prerelease: false
files: |
counter.zip
This workflow defines a single job named release-counter
, which runs on the
ubuntu-latest
runner. It begins by checking out the repository, setting up the
required Go version, and building the project into a binary named counter
.
It then packages the binary into a zip file and uses
softprops/action-gh-release
to create a draft GitHub release, attaching the
zip file from the previous step for distribution.
With Act, the workflow can be run and debugged before committing changes. However, since it cannot automatically generate a tag push event, you need to provide a simulated event payload:
{
"ref": "refs/tags/v1.0.0",
"ref_type": "tag",
"ref_name": "v1.0.0"
}
This mimics a push
event where a tag (v1.0.0
) is pushed. The ref_type
ensures the event behaves as a tag push, allowing the condition
if: github.ref_type == 'tag'
in the workflow to evaluate as true
.
You can then run act
with the following arguments:
act \
-e payload.json \
-W .github/workflows/release.yml \
--env GO_VERSION=1.24 \
--secret GITHUB_TOKEN=$(gh auth token)
Here's a breakdown of the command arguments:
-e payload.json
: Specifies the simulated event file.-W .github/workflows/release.yml
: Runs only the release workflow.--env GO_VERSION=1.24
: Sets the Go version required by the workflow.--secret GITHUB_TOKEN=$(gh auth token)
: Passes an authentication token to allowsoftprops/action-gh-release
to create the draft release.
Provided that your GITHUB_TOKEN
has the right permissions, the job will
complete successfully:
After the job runs, visit your repository's Releases page to verify the draft release:
Once confirmed, you can deploy this workflow for automated version releases on GitHub.
Integrating Act with Visual Studio Code
If you're using VS Code, you can integrate your Act workflows into your editor through the GitHub Local Actions extension. Once its installed, you can click its icon on the sidebar to see all your workflows and run them as desired:
Final thoughts
While Act significantly improves the GitHub Actions development experience, it has limitations. Workflows that rely heavily on GitHub's API operations may not work locally, and platform-specific actions or certain GitHub contexts may also behave differently in Act's environment.
For scenarios where local testing isn't sufficient, action-tmate provides a powerful complement to Act as it opens an interactive SSH session within your running workflow, and enables real-time debugging in the actual GitHub environment.
Despite its limitations, Act remains an invaluable tool for catching common
issues early and reducing the iterative push-fix-push cycle typically needed
when developing GitHub Actions' workflows. When used alongside action-tmate
,
it provides a comprehensive solution for efficient CI/CD workflow development.
Thanks for reading!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github