DEV Community

Cover image for Running GitHub Actions Locally with nektos/act: Production Integration Lessons
Maya Patel
Maya Patel

Posted on

Running GitHub Actions Locally with nektos/act: Production Integration Lessons

Why Local GitHub Actions Matter

When your CI pipeline burns through 2,000 runner minutes debugging a single YAML syntax error, the cost of cloud-only iteration becomes painfully clear. nektos/act solves this by executing GitHub Actions workflows locally using Docker containers that mirror the runner environment.

After integrating nektos/act into our pre-push validation workflow, we caught 73% of configuration failures before they ever hit remote runners. But the devil lives in the compatibility layer.

The Reality Check: What Actually Works

nektos/act runs workflows by spinning up Docker containers that approximate GitHub's hosted runner images. The approximation is where things get interesting.

Workflow Compatibility Boundaries

Not all actions play nicely with local execution:

  • actions/checkout@v4: Works flawlessly with local Git repos
  • actions/cache@v3: Fails silently—local filesystem doesn't match remote cache backend semantics
  • third-party actions requiring GITHUB_TOKEN: Hit authentication walls unless you provision personal access tokens

Here's a hardened workflow that survives both local and remote execution:

name: Dual-Target Validation

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  validate:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 pinned SHA

      - name: Setup Node
        uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: |
          if [ -f package-lock.json ]; then
            npm ci
          else
            npm install
          fi

      - name: Run lint and typecheck
        run: |
          npm run lint
          npm run typecheck

      - name: Execute test suite
        run: npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}'
        env:
          CI: true
Enter fullscreen mode Exit fullscreen mode

Volume Mount Chaos and the Docker-in-Docker Trap

The first production gotcha: nektos/act mounts your workspace into /github/workspace inside containers. If your workflow references relative paths outside the project root, you'll hit silent failures.

Worse, if your workflow itself spawns Docker containers (common in integration tests), you're running Docker-in-Docker. This requires mounting the Docker socket:

act -W .github/workflows/validate.yml \
  --container-daemon-socket /var/run/docker.sock \
  -P ubuntu-latest=catthehacker/ubuntu:act-latest \
  --secret-file .env.secrets
Enter fullscreen mode Exit fullscreen mode

Without --container-daemon-socket, nested Docker commands hang indefinitely with zero error output.

Secret Management That Doesn't Leak

GitHub Actions injects secrets via environment variables. nektos/act requires explicit secret provisioning:

# .env.secrets (gitignored)
GITHUB_TOKEN=ghp_xxxxxxxxxxxxx
NPM_TOKEN=npm_xxxxxxxxxxxxx
SLACK_WEBHOOK=https://hooks.slack.com/services/xxxxx
Enter fullscreen mode Exit fullscreen mode

Critical: Use --secret-file instead of -s KEY=value to avoid leaking secrets into shell history.

For CI environments that test against live APIs, we run a validation script before local execution:

#!/bin/bash
# scripts/validate-secrets.sh

set -euo pipefail

required_secrets=("GITHUB_TOKEN" "NPM_TOKEN")

for secret in "${required_secrets[@]}"; do
  if ! grep -q "^${secret}=" .env.secrets 2>/dev/null; then
    echo "ERROR: Missing required secret: ${secret}"
    exit 1
  fi
done

echo "Secret validation passed"
Enter fullscreen mode Exit fullscreen mode

Performance Tradeoffs: When Local Isn't Faster

Pulling runner images adds 2-4 minutes of cold-start overhead. For single-job workflows, this negates the speed advantage over remote runners.

Where nektos/act shines: iterating on complex matrix builds. Testing a 3x3 matrix (3 OS × 3 language versions) locally takes 8 minutes versus 25 minutes queued on GitHub's infrastructure.

The breakeven point: workflows with more than 2 jobs or any matrix strategy.

The Unsolved Problem: Third-Party Action Drift

Actions maintained by the community often assume GitHub's exact runner environment. We've hit failures with:

  • crazy-max/ghaction-docker-buildx: Expects specific kernel modules
  • google-github-actions/auth: Requires workload identity federation unavailable locally

The workaround is defensive workflow design: wrap flaky actions in conditional blocks that skip during local execution:

- name: Authenticate to Google Cloud
  if: github.event_name != 'pull_request' && !env.ACT
  uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
Enter fullscreen mode Exit fullscreen mode

The !env.ACT check detects nektos/act execution context.

What About Service Containers?

Workflows using services: blocks (PostgreSQL, Redis, etc.) work but require manual network configuration. nektos/act doesn't auto-create Docker networks between job containers and service containers.

We've moved complex service-dependent tests into dedicated Docker Compose stacks that both nektos/act and remote runners invoke identically:

- name: Run integration tests
  run: |
    docker-compose -f tests/docker-compose.integration.yml up -d
    npm run test:integration
    docker-compose -f tests/docker-compose.integration.yml down
Enter fullscreen mode Exit fullscreen mode

This sidesteps nektos/act's service networking limitations entirely.

The Configuration Nobody Tells You About

Default runner images are bloated (8GB+). Use community-maintained slim images:

act -P ubuntu-latest=catthehacker/ubuntu:act-latest \
    -P ubuntu-22.04=catthehacker/ubuntu:act-22.04 \
    --artifact-server-path /tmp/act-artifacts
Enter fullscreen mode Exit fullscreen mode

The --artifact-server-path flag enables actions/upload-artifact and actions/download-artifact to function locally by staging files to your filesystem instead of GitHub's artifact storage.

Where This Breaks Down in Practice

After six months running nektos/act in pre-push hooks across a 12-engineer team:

  • 40% reduction in failed CI runs from trivial errors
  • 3 hours/week saved per engineer on workflow debugging
  • But: 15% of workflows still require remote execution for full validation

The gap comes from GitHub-specific APIs (check runs, deployment status updates) and third-party actions with hard dependencies on runner metadata.

The real question: should we be designing workflows that can't be validated locally, or is the integration testing gap between nektos/act and GitHub's actual runners a permanent architectural constraint we need to work around?

Top comments (1)

Collapse
 
anidev profile image
Aniket Maurya •

You should also check Celesto CI. It is much cheaper than GitHub Actions and gives you a lot of CI minutes for free.