Git & CI/CD Advanced

GitHub Actions: Resolving ‘Runner Out of Disk Space’ Errors on Alpine Linux Builds

Troubleshoot and fix 'runner out of disk space' errors in GitHub Actions workflows, specifically when building applications on Alpine Linux. Optimize your CI/CD builds for efficiency.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Troubleshoot and fix 'runner out of disk space' errors in GitHub Actions workflows, specifically when building applications on Alpine Linux. Optimize your CI/CD builds for efficiency.

When running complex build processes within GitHub Actions, especially when targeting or utilizing Alpine Linux images, encountering "runner out of disk space" errors can be a frustrating hurdle. While Alpine is renowned for its minimal footprint, the accumulated artifacts, dependencies, and temporary files generated during a build can quickly exhaust the finite disk resources of a GitHub Actions runner, leading to abrupt workflow failures. This guide provides a highly technical, step-by-step approach to diagnose, understand, and resolve these disk space issues.

Symptom & Error Signature

The most common symptom is a GitHub Actions workflow step failing with an error indicating insufficient disk space. This can manifest in various ways, often with the process attempting to write data and failing due to a full filesystem.

Typical error messages you might observe in your workflow logs:

## Example 1: npm build failure
...
npm ERR! code ENOSPC
npm ERR! syscall write
npm ERR! path /__w/your-repo/node_modules/.bin/webpack
npm ERR! errno -28
npm ERR! No space left on device
npm ERR! A complete log of this run can be found in:
npm ERR!     /home/runner/.npm/_logs/...
Error: Process completed with exit code 256.
...

## Example 2: Docker build failure
...
Error: failed to create task for build: failed to build LLB: disk usage limit exceeded
Error: The process '/usr/bin/docker' failed with exit code 1
...

## Example 3: General system error
...
/bin/sh: write error: No space left on device
df: /var/lib/docker: No space left on device
...

## Example 4: Output from a diagnostic step within the workflow
- name: Check Disk Space
  run: df -h
...
Filesystem      Size  Used Avail Use% Mounted on
overlay          64G   64G     0 100% /
tmpfs           7.8G     0  7.8G   0% /dev
tmpfs           7.8G     0  7.8G   0% /sys/fs/cgroup
/dev/sda1        64G   64G     0 100% /__w/_temp/docker_build_home
tmpfs           7.8G  4.0K  7.8G   1% /tmp
...

The key indicators are "No space left on device," "ENOSPC," or "disk usage limit exceeded," often accompanied by a 100% Use% reported by df -h.

Root Cause Analysis

GitHub Actions runners, even the ubuntu-latest variant, operate with finite resources. Specifically, the ephemeral disk allocated to a job is typically around 14-64 GB (depending on the runner type and configuration, ubuntu-latest often presents ~64GB total with a significant portion allocated to the Docker daemon and pre-installed tools), which can be quickly consumed. When building on Alpine Linux, several factors contribute to this issue:

  1. Ephemeral Runner Limitations: The underlying GitHub Actions runner provides a limited disk. While 64GB might seem ample, the Docker daemon's storage, pre-installed software, and the base operating system consume a substantial portion, leaving less for your build.
  2. Verbose Dependency Management:
    • Node.js: node_modules directories can easily grow to several gigabytes, especially with large frontend projects or numerous development dependencies.
    • Python: Pip caches (~/.cache/pip) and large virtual environments can accumulate rapidly.
    • Java/JVM: Maven (.m2 directory) and Gradle caches can become massive over time.
    • Go: The Go module cache can also consume significant space.
    • PHP: Composer's vendor directory and cache.
  3. Inefficient Docker Builds:
    • Non-multi-stage Dockerfiles: Building all dependencies and runtime components in a single Dockerfile stage leads to large intermediate layers that are not always effectively pruned.
    • Lack of .dockerignore: Copying unnecessary files (e.g., node_modules, .git, temporary build artifacts from the host context) into the Docker build context.
    • Unoptimized RUN commands: Creating new Docker layers for each command, rather than chaining related commands to minimize layer count and intermediate file retention.
    • Docker Build Cache Bloat: The Docker daemon on the runner maintains its own layer cache, which can grow large if not managed.
  4. Temporary File Accumulation: Compilers, test runners, or specific build tools can generate large temporary files in /tmp or within the project directory that are not automatically cleaned up.
  5. GitHub Actions Cache Management: While the actions/cache action is designed to save time, improperly configured or excessively large caches can indirectly contribute to disk pressure if they store vast amounts of data that are not efficiently reused or pruned.

Step-by-Step Resolution

Addressing disk space issues requires a multi-pronged approach focusing on minimizing consumption, aggressive cleanup, and optimizing build processes.

1. Diagnose Disk Usage During Workflow Execution

The first step is to pinpoint what is consuming the disk space. Add diagnostic steps to your workflow:

- name: Check Initial Disk Space
  run: df -h

- name: List Top 20 Largest Directories in Repository
  run: sudo du -sh $(find . -maxdepth 2 -type d) | sort -rh | head -n 20
  working-directory: ${{ github.workspace }} # Adjust if your build directory is different

- name: Check Docker Image Disk Usage (if applicable)
  run: docker system df -v # Or docker images --format "{{.Size}}t{{.Repository}}:{{.Tag}}" | sort -rh

# Run your actual build steps here

- name: Check Disk Space After Build
  run: df -h

- name: List Top 20 Largest Directories After Build
  run: sudo du -sh $(find . -maxdepth 2 -type d) | sort -rh | head -n 20
  working-directory: ${{ github.workspace }}

Analyze the output of du -sh to identify unexpectedly large directories, such as node_modules, vendor, .m2, or target directories.

2. Implement Aggressive Cleanup Strategies

After build steps that generate large intermediate files or dependencies, immediately clean them up if they are no longer needed for subsequent steps or the final artifact.

- name: Build Node.js Application
  run: |
    npm install
    npm run build --if-present

- name: Clean up Node.js modules (if not needed for deployment artifact)
  if: success() # Only clean if build was successful
  run: |
    echo "Cleaning up node_modules..."
    rm -rf node_modules
    npm cache clean --force # Clear npm cache
    echo "Disk space after cleanup:"
    df -h

- name: Cleanup Python caches (if applicable)
  if: success()
  run: |
    echo "Cleaning up pip cache..."
    pip cache purge
    echo "Disk space after cleanup:"
    df -h

- name: Cleanup Maven/Gradle caches (if applicable)
  if: success()
  run: |
    echo "Cleaning up Maven/Gradle caches..."
    rm -rf ~/.m2/repository/**/*.jar.lastUpdated # Remove partial downloads
    # For Gradle, consider `gradle cleanBuildCache` or similar if configured
    echo "Disk space after cleanup:"
    df -h

- name: Remove Alpine Linux package cache
  if: success()
  run: |
    echo "Cleaning up Alpine apk cache..."
    sudo apk cache clean
    echo "Disk space after cleanup:"
    df -h

- name: Prune Docker system if using Docker builds extensively
  if: success() && always() # Always attempt to prune
  run: |
    echo "Pruning Docker system..."
    docker system prune -af --volumes # Prune all unused Docker data aggressively
    echo "Disk space after Docker prune:"
    df -h

Be cautious when using sudo rm -rf /tmp/* or docker system prune -af. While effective, ensure you understand the implications and that you are not deleting files or containers required by other concurrent processes or subsequent workflow steps. In most GitHub Actions runner setups, /tmp is job-isolated, but Docker prune affects the daemon's global state.

3. Optimize Docker Builds (Crucial for Alpine-based images)

If your workflow involves building Docker images, optimizing your Dockerfiles is paramount.

a. Implement Multi-Stage Builds

Separate your build environment from your runtime environment. Alpine is excellent for the final runtime stage.

# Stage 1: Build Environment
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install # Install dev and runtime dependencies for build
COPY . .
RUN npm run build # Compile your application

# Stage 2: Runtime Environment (minimal Alpine)
FROM alpine:3.18
LABEL authors="Your Name <[email protected]>"
WORKDIR /app

# Install only runtime dependencies needed for the application to run
RUN apk add --no-cache nodejs
COPY --from=builder /app/dist ./dist
# If you have specific assets or other files to copy:
# COPY --from=builder /app/public ./public

EXPOSE 8080
CMD ["node", "dist/main.js"]
b. Utilize .dockerignore

Ensure your build context is as small as possible by excluding unnecessary files like node_modules (if installed inside the container), .git, dist, target, etc., from being copied to the Docker daemon.

# .dockerignore example
node_modules/
.git/
.gitignore
.DS_Store
dist/
build/
*.log
tmp/
c. Chain Commands and Use --no-cache

Minimize layers and intermediate file accumulation by chaining RUN commands and clearing caches immediately.

# Bad: creates multiple layers, intermediate cache
# RUN apk update
# RUN apk add some-package
# RUN rm -rf /var/cache/apk/*

# Good: single layer, clears cache
RUN apk update && 
    apk add --no-cache some-package another-package && 
    rm -rf /var/cache/apk/*
d. Leverage BuildKit for Efficient Caching

Use docker buildx build with GitHub Actions caching to cache Docker layers between workflow runs.

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Cache Docker layers
  uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    push: false # Set to true if pushing to a registry
    tags: your-repo/image:latest
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max,compression=zstd # or gzip, default is uncompressed

4. Leverage GitHub Actions Cache Effectively

Strategically cache language-specific dependencies to speed up builds and reduce repeated downloads. Be mindful of cache size.

- name: Cache Node.js modules
  uses: actions/cache@v4
  with:
    path: ~/.npm # Cache npm global cache or node_modules directly
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Install Node.js Dependencies
  run: npm ci # Use npm ci for clean, reproducible installs

- name: Cache Python dependencies
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip # Cache pip's download cache
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

- name: Install Python Dependencies
  run: pip install -r requirements.txt

While caching reduces build time, it also consumes disk space. Ensure your cache keys are granular and invalidate correctly. Large, frequently changing caches can become counterproductive. GitHub Actions cache has a maximum size per repository (typically 10GB total), which is shared.

5. Minimize Repository Checkout Size

If your repository contains many large, irrelevant files for the build (e.g., historical data, large test datasets, unrelated projects in a monorepo), reduce the checkout depth.

- name: Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 1 # Only fetch the latest commit, not full history

fetch-depth: 1 can cause issues with tools that require full Git history (e.g., git describe, semantic-release). Test thoroughly to ensure compatibility. For monorepos, consider sparse-checkout.

6. Consider Self-Hosted Runners (Last Resort)

If all optimization efforts fail to meet your disk space requirements, or if you have highly specialized needs (e.g., very large artifact caching, massive temporary files), a self-hosted GitHub Actions runner offers full control over hardware resources, including disk space. This, however, shifts the operational burden to your team.

runs-on: self-hosted

This requires setting up and maintaining your own server or VM to act as a runner, which comes with its own costs and management overhead.

By systematically applying these strategies, you can effectively manage disk space on GitHub Actions runners, ensuring your Alpine Linux builds complete successfully and efficiently.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.