Skip to content

Blog

How do I fix "No space left on device" in GitHub Actions?

·

GitHub lists 14 GB of SSD for standard hosted runners, and a GitHub maintainer says only 14 GB free is guaranteed, with preinstalled software sharing the disk. Docker layers and large checkouts fill the rest. Run df -h and docker system df to find the cause, delete what the job does not need, or use a bigger disk.

That is the short version. The rest is the numbers, the commands that find the cause, what each deletion costs in minutes, and the point where paying for a larger disk is cheaper than cleaning. The GitHub and Docker facts were read from their own documentation and repositories on 10 November 2026; where a figure is ours, it says so.

How much disk does a GitHub-hosted runner have, and how much is free when a job starts?

GitHub’s runner reference lists 14 GB of SSD for every standard hosted runner: Linux, Windows and macOS, x64 and arm64, public and private repositories, and ubuntu-slim. It does not say how much of that is free when a job starts.

The nearest thing to an answer is from a GitHub maintainer. On 17 November 2025, replying to a “No space left on device” report in the runner images repository, they said GitHub guarantees at least 14 GB of free disk space, that more may be there but is not promised, and that the newly added Android NDK was taking a lot of space. Their advice was to remove preinstalled software you do not need.

Two things follow. Free space is a floor, not a constant: it moves when the image does, so a workflow that has not changed can start failing, which is what people in that issue reported. And the only figure that counts is the one on your own runner, so measure it before you plan around any number, including these.

RunnerDiskFree when a job starts
GitHub standard runners, every size listed14 GB SSDNot stated in the reference; a maintainer says at least 14 GB is guaranteed
runners.io Ubuntu x64, 1 to 16 vCPU50 GBAt least 38 GB
runners.io Ubuntu x64, 32 vCPU400 GBNot published
runners.io Ubuntu ARM6475 GBAbout 65 GB
runners.io Windows100 GBAbout 30 GB on windows-2025, about 20 GB on windows-2022

The runners.io rows are from our labels page, read on the same day. The disk there is the root disk, shared with the operating system and the preinstalled tools, which the image page lists. The disk belongs to the label, so it is chosen by choosing the size, not as a separate setting.

Why do Docker builds and large monorepos run out of space?

Because everything a job writes lands on the one disk that the operating system and the preinstalled tools already share. A Docker build is the clearest case: the base images you pull, every layer you build and the build cache all sit there together, and docker system df reports what images, containers and volumes hold and how much of it is reclaimable. A multi-stage build pulls the base image of every stage the target depends on.

A large monorepo fills the disk from the other side. The checkout comes first: actions/checkout fetches one commit by default, but a workflow that sets fetch-depth: 0 fetches all history for every branch and tag. Then every package installs its own dependencies, every build writes its outputs, and test fixtures and restored caches arrive on top. None of these is large alone. The sum is what meets the limit, usually in the last step.

The third cause is not in your workflow at all. As the Android NDK case above shows, the image itself can grow under a job that did not change. A failure that arrives with no commit of yours behind it is worth checking against the image’s release notes before you rewrite anything.

How do I find what is filling the disk (df, du, docker system df)?

Read the disk at the start of the job and again at the moment of failure. A step marked if: failure() runs on the same machine right after the step that failed, so the disk still holds whatever filled it.

jobs: build: runs-on: ubuntu-24.04 # any label: the commands are the same steps: - name: Disk at the start run: df -h / && df -i / - uses: actions/checkout@v7 - run: make build - name: Where the space went if: failure() run: | df -h df -i / sudo du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -25 || true du -sh "$GITHUB_WORKSPACE" || true docker system df -v || true

Read it in this order. df -h says which filesystem is full, and if one mount has far more free space than the root, that is somewhere to put large writes. df -i matters because the same error is what Linux says when a filesystem runs out of inodes rather than bytes, which a tree of millions of small files will do while df -h still shows room. du with -x stays on one filesystem and sorts the biggest directories to the top. And docker system df -v splits Docker’s share into images, containers and volumes. On a Windows runner, Get-PSDrive C gives used and free space.

What can I safely delete, and what does that cost in job time?

Delete in this order: what your job made, then what Docker holds, then preinstalled software the job never uses. The first costs nothing. Every later one costs time, on every run of the job, so keep only the deletions that were needed and read the cost off the run page, where each step shows how long it took.

WhatCommandWhat it costs
Build outputs and scratch files your job no longer needsrm -rf dist .cacheAlmost nothing, unless a later step needs them again
Docker build cache, only as far as neededdocker buildx prune --force --min-free-space 20GBThe next build rebuilds any layer that was pruned
Unused Docker images and stopped containersdocker system prune --all --forceAny image you need later is pulled again
A preinstalled SDK you do not usesudo rm -rf "$ANDROID_HOME"The deletion itself takes time, on every run, and the run page shows how much
Preinstalled tool versions you do not usesudo rm -rf "$RUNNER_TOOL_CACHE"/<tool>A setup action that wants the version has to download it

Docker’s own pages say what the prune commands touch. docker system prune removes stopped containers, unused networks, dangling images and unused build cache, and --all extends it to every unused image; volumes are kept unless you add --volumes. docker buildx prune clears the build cache, and its --min-free-space flag stops once a free space target is met, so it removes only as much as the build needs.

steps: - name: Make room run: | df -h / sudo rm -rf "$ANDROID_HOME" docker system prune --all --force df -h / - uses: actions/checkout@v7 with: sparse-checkout: | server shared

The paths above are the ones on GitHub’s Ubuntu 24.04 image. On any other image, du from the last section shows where the tools are. Do not delete the workspace or $RUNNER_TEMP, and put a deletion before the step that would have needed the space, not after it. Printing df -h / on both sides of it is how you know it freed anything.

The cheapest deletion is the file you never write. A shallow checkout and a sparse-checkout of only the packages the job builds, as in the YAML, keep a monorepo’s other packages off the disk. A .dockerignore keeps them out of the build context. And pruning and caching pull against each other: a pruned layer is one the next build rebuilds. The post on Docker build caches covers the backends, including mode=min, which writes a smaller cache. On our Linux runners the Docker layer cache keeps layers between jobs once you add the one-line runners-io/setup-docker-builder@v1 step before your build (it is not offered on Windows), so a layer that has not changed comes from the cache instead of being built again. It is metered by what it stores, at the rate on the pricing page.

When is a runner with a bigger disk the better fix?

When the space is real work rather than waste. An image build that needs tens of gigabytes, large test fixtures, or a stack of deletion steps that now costs more minutes than a larger disk would. Cleaning is paid for on every run; a bigger disk would be charged for on every minute instead, so the question is which costs less.

The test is arithmetic you can do. Take the minutes your cleanup adds to a job, multiply by how many times a month it runs and by the per-minute rate of the label, and set that beside the extra per-minute cost of a larger runner. GitHub sells larger runners with more disk too, as its reference says, and the pricing post has how those are billed.

On our runners the disk comes with the label: 50 GB on Ubuntu x64 up to 16 vCPU, 75 GB on ARM64, 100 GB on Windows and 400 GB at 32 vCPU. So on x64 the only label with more disk is 32 vCPU, which counts 16 run-minutes for every minute it runs, against 1 at 2 vCPU. It pays only for a job that needs the whole machine or the 400 GB, so use it on the one job that needs it and leave the rest where they were.

jobs: test: runs-on: runnersio-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v7 - run: make test image: # the one job that needs the disk runs-on: runnersio-32vcpu-ubuntu-2404 steps: - uses: actions/checkout@v7 - run: docker build -t app .

Per-minute rates for each size are on the pricing page, and the labels page above has every label and its disk. If a job fails on our runners for a reason this post does not cover, the troubleshooting page lists the messages a job page shows and what to do about each.

Run your CI and your infrastructure on one bill

Connect GitHub

Card required. You pay only for the minutes your jobs run.