All tutorials
17 min read

How to Fix No Space Left on Device on a Linux Server

Fix the no space left on device error on Ubuntu or Debian: find what filled the disk, clear old logs and unused Docker images, and stop it filling up again.

TL;DR:

  • No space left on device is the ENOSPC error, number 28 on Linux: a filesystem has run out of data blocks or inodes. df -h shows space and df -i shows inodes.
  • sudo du -xh --max-depth=1 / | sort -h lists the largest folders. Check /var/log, /var/cache/apt, /var/lib/docker and /var/lib/containerd first.
  • sudo journalctl --vacuum-size=200M shrinks the systemd journal, and SystemMaxUse=200M in a journald drop-in keeps it there. By default, the journal can use up to 10% of the filesystem, capped at 4 GB.
  • On Docker Engine 29.9.3, docker builder prune -a and docker image prune -a remove unused build cache and images. Container logs have no size limit until you set max-size.
  • A deleted file that a process still holds open keeps its space. sudo lsof +aL1 / lists these files, and restarting the process releases them.
  • On Ubuntu 26.04 and Debian 13, /tmp is a tmpfs in RAM limited to half of the memory, so it can fill while / has space.

Applies to: Ubuntu 24.04 LTS and 26.04 LTS · Debian 12 and 13 · Docker Engine 29.9.3 · checked November 2026

No space left on device is the error Linux returns, as error code ENOSPC (number 28), when a program writes to a filesystem that has run out of data blocks or inodes. To fix it on an Ubuntu or Debian server, find the full filesystem with df and the largest folders with du, then clear the systemd journal, old logs, apt caches and unused Docker data, and release the space that deleted files still hold. The last step adds an hourly check that warns before the disk fills again.

What causes no space left on device?

The error appears wherever a write fails: write error: No space left on device in the shell, cp: error writing, [Errno 28] No space left on device from Python programs, an apt install that stops, or a Docker build step that fails.

CauseWhat you seeFix
The filesystem has no data blocks leftdf -h shows Use% at 100% or Avail at 0Steps 2 to 5
The filesystem has no inodes leftdf -i shows IUse% at 100% while df -h shows spaceStep 2
A deleted file is still opendf counts the space, du cannot find itStep 6
Another filesystem is full/tmp or /boot is full while / has spaceStep 4 (/boot) and the troubleshooting table (/tmp)

Prerequisites

  • A server running Ubuntu 24.04 LTS or 26.04 LTS, or Debian 12 or 13, with SSH access. Arct Cloud Linux VPS images include Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 and Debian 13.
  • A non-root user with sudo privileges and SSH key login. How to Generate an SSH Key on Windows, macOS, and Linux creates the key and the sudo user.
  • A recent backup of any data you cannot recreate. VPS backups are optional and not offered in every location.

Commands run as the sudo user. Replace values written in capitals, such as YOUR_FOLDER, with your own.

Step 1: Check disk space and inodes

df reports used and available space for every mounted filesystem. -h prints sizes in units such as M and G:

df -h

The output looks similar to this:

Filesystem      Size  Used Avail Use% Mounted on
...
/dev/loop0      116M  108M     0 100% /srv/data

In this example, a small test filesystem mounted on /srv/data is full: Use% is 100% and Avail is 0. On a server, read the line mounted on / first, then /boot and /tmp if they have their own lines. ext4 reserves 5% of its blocks for root by default, so Avail reaches 0 before Used equals Size.

Check inodes next. Every file and directory uses one inode:

df -i
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
...
/dev/loop0        2048   2048       0  100% /srv/data

In this second test, the same filesystem had 107M available in df -h but no inodes left, so touch failed with No space left on device. A folder with a large number of small files, such as cache or session files, causes this.

Step 2: Find the folders that take the most space

du adds up the space each folder uses. -x stays on one filesystem, so it skips /proc and other mounts, including /tmp when it is a tmpfs:

sudo du -xh --max-depth=1 / | sort -h | tail -n 10

The largest folders are at the bottom:

...
575M	/usr
1018M	/var
1.6G	/

Run it again on the largest folder:

sudo du -xh --max-depth=1 /var | sort -h | tail -n 5
12K	/var/tmp
53M	/var/lib
195M	/var/cache
770M	/var/log
1018M	/var

In this run, /var/log held 521 MB of journal files and 250 MB of application logs, and /var/cache held 93 MB of downloaded packages. With Docker installed, /var/lib/docker and /var/lib/containerd appear under /var/lib. Once you have cleared some space, install ncdu with sudo apt install ncdu and run sudo ncdu -x / for an interactive view.

If df -i showed 100%, count files instead of bytes on that filesystem:

sudo du --inodes -x / | sort -n | tail -n 10

Replace / with the mount point that df -i showed as full. In the test, that was /srv/data:

1	/srv/data/lost+found
2034	/srv/data/sessions
2038	/srv/data

The last lines are the folders with the most files. Look at what a folder holds before you delete anything, and count the files last modified 8 or more days ago:

sudo ls YOUR_FOLDER | head
sudo find YOUR_FOLDER -type f -mtime +7 | wc -l

Replace YOUR_FOLDER with the folder from the du output, such as /srv/data/sessions. If the count is 0, the files are newer; find the program that writes them first. Otherwise, delete those files and check inodes again:

sudo find YOUR_FOLDER -type f -mtime +7 -delete
df -i

Step 3: Clear the systemd journal and old logs

The systemd journal stores logs in /var/log/journal. Check its size:

sudo journalctl --disk-usage
Archived and active journals take up 520M in the file system.

Delete the oldest archived journal files until the archived files use less than 200 MB:

sudo journalctl --vacuum-size=200M

The output looks similar to this:

...
Deleted archived journal /var/log/journal/9d2c64e6576f47a0aa03a8ccaab0a948/system@50434745b62d48a39fe7784d3baf3213-0000000000000001-00065f9f540d69e9.journal (128M).
...
Vacuuming done, freed 384M of archived journals from /var/log/journal/9d2c64e6576f47a0aa03a8ccaab0a948.

Vacuuming removes archived files only, so the active files stay. In this run the journal went from 520M to 136M. To keep logs by age instead, use --vacuum-time=2weeks.

By default, journald lets the journal use up to 10% of the filesystem and keeps 15% of it unused, with each value capped at 4 GB. To set a lower limit, create a drop-in file:

sudo mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSystemMaxUse=200M\n' | sudo tee /etc/systemd/journald.conf.d/50-max-use.conf
sudo systemctl restart systemd-journald

systemd's documentation states that restarting systemd-journald is safe. Check the new limit in journald's start message:

sudo journalctl -u systemd-journald -n 3 --no-pager

The output includes a line similar to this:

...
Nov 06 17:58:31 nospace-test systemd-journald[3383]: System Journal (/var/log/journal/9d2c64e6576f47a0aa03a8ccaab0a948) is 136M, max 200M, 63.9M free.
...

Ubuntu 24.04 and Debian 12 print the limit as max 200.0M. Find other large files in /var/log:

sudo find /var/log -type f -size +50M -exec ls -lh {} +
-rw-r--r-- 1 root root 190M Nov  6 17:57 /var/log/app-worker/app.log

In this example an application writes to /var/log/app-worker/, and ls -lh /var/log/app-worker/ lists app.log with three rotated copies of 21M, app.log.1.gz to app.log.3.gz. Files ending in .1 or .gz are rotated copies. Delete the ones you no longer need, and empty the active log with truncate instead of rm, because a program that still has a deleted log open keeps its space (Step 6):

sudo rm /var/log/app-worker/app.log.*.gz
sudo truncate -s 0 /var/log/app-worker/app.log

Files under /var/log/journal belong to the journal; shrink them with journalctl, not rm. Packages such as apt and dpkg ship their own logrotate rules in /etc/logrotate.d. For an application log that has none, create /etc/logrotate.d/app-worker. If that folder is missing, install logrotate first with sudo apt install logrotate:

sudo tee /etc/logrotate.d/app-worker > /dev/null <<'EOF'
/var/log/app-worker/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}
EOF

This keeps 7 compressed daily copies. copytruncate copies the log and then empties it, for programs that cannot reopen their log file; the logrotate manual notes that lines written between the copy and the truncate can be lost. Test the rule without changing anything:

sudo logrotate --debug /etc/logrotate.d/app-worker

The output shows the rule logrotate would apply, and its first line says that debug mode changes nothing.

Step 4: Clear package caches and unused packages

apt-get keeps the packages it downloads in /var/cache/apt/archives, while the apt command deletes its downloads after a successful install. Check the cache size, then delete the cached files:

sudo du -sh /var/cache/apt/archives
sudo apt clean
sudo du -sh /var/cache/apt/archives

In this run the cache went from 93M to 12K. apt downloads packages again when it needs them. Next, remove packages that were installed as dependencies and are no longer needed:

sudo apt autoremove --purge

On Ubuntu 26.04 the output looks similar to this:

REMOVING:
  libexpat1-dev*    libpython3-dev*  libpython3.14-dev*  python3.14-dev*
  libjs-sphinxdoc*  libpython3.14*   python3-dev*        zlib1g-dev*

Summary:
  Upgrading: 0, Installing: 0, Removing: 8, Not Upgrading: 4
  Freed space: 39.3 MB

Read the list before you answer Y. To keep a package from the list, mark it with sudo apt-mark manual YOUR_PACKAGE. Autoremove also removes old kernel packages: apt always keeps the running kernel and the latest one. When /boot is a separate filesystem, check it with df -h /boot after the cleanup.

If the server has snapd, snap list --all lists older revisions of each snap, marked disabled. Remove one with sudo snap remove YOUR_SNAP --revision=YOUR_REVISION, using the name and revision from the list. Without --revision, snap remove removes the whole snap and saves a snapshot of its data, unless you add --purge.

Step 5: Clean up Docker images, build cache and logs

Skip this step if Docker is not installed, and run docker as a member of the docker group or with sudo. Docker keeps container logs and volumes in /var/lib/docker. Fresh installs of Docker Engine 29 or later keep images in /var/lib/containerd, a store that Docker's documentation says needs more space because it keeps each image compressed and uncompressed. Clean both with docker commands, not rm:

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          4         3         360.3MB   125.9MB (34%)
Containers      3         2         90.11kB   4.096kB (4%)
Local Volumes   0         0         0B        0B
Build Cache     5         0         267.2MB   12.29kB

Remove build cache. docker builder prune removes only dangling cache, such as the layers a failed build leaves; -a removes all build cache that no running build uses. The next build is slower while the cache fills again:

docker builder prune -a

Type y to confirm. The output ends with the total:

WARNING! This will remove all build cache. Are you sure you want to continue? [y/N]
...
Total:	351.1MB

docker container prune removes every stopped container, shown as Exited or Created, together with any files it wrote outside a volume. List them first:

docker ps -a --filter status=exited --filter status=created

Then remove the stopped containers and every image that no container uses:

docker container prune
docker image prune -a

The image prune output looks similar to this:

WARNING! This will remove all images without at least one container associated to them.
...
untagged: report-app:1
...
Total reclaimed space: 126MB

docker compose up and docker run pull a removed image again from its registry. An image built on the server, such as report-app:1 here, has to be built again with docker build or docker compose build; until then, docker run stops with pull access denied. docker system prune -a runs all of these prunes at once, including all build cache, and keeps volumes unless you add --volumes.

Prune totals can differ from the space released: in this run, docker container prune reported 4.096kB while it removed a 380 MB container log. Check the result with df -h. docker volume prune removes anonymous volumes, and with -a also named volumes, which can hold database files; leave volumes alone unless you know their data is not needed.

Container logs grow without a size limit by default. Find the large ones:

sudo find /var/lib/docker/containers -name "*-json.log" -exec du -h {} + | sort -h
...
380M	/var/lib/docker/containers/35e925c610db292848e4b954e5bd439d94908cf7fa990cc1097caad5f61d7a5b/35e925c610db292848e4b954e5bd439d94908cf7fa990cc1097caad5f61d7a5b-json.log

The folder name is the container ID, and docker ps -a shows its first 12 characters. Docker's documentation states that these files are meant for the Docker daemon only, so cap them in /etc/docker/daemon.json instead of editing them. Check whether that file exists:

sudo cat /etc/docker/daemon.json

If it prints settings, keep them, because replacing the file removes settings such as the 127.0.0.1 port binding from How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04. If they include max-size and max-file under log-opts, the cap is set: skip to docker compose up below. Otherwise, add the log-driver and log-opts keys from the next block inside the file's outer braces with sudo nano /etc/docker/daemon.json. If cat prints No such file or directory, create the file with a limit of 3 files of 10 MB per container:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF

Restarting Docker stops running containers. Containers with the always or unless-stopped restart policy start again; start the others with docker start YOUR_CONTAINER. Validate the file and restart Docker:

sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker
configuration OK

The limit applies only to containers created after the restart, so recreate each Compose project from its folder, which also removes the old container and its log:

docker compose up -d --force-recreate

A container started with docker run keeps its old log settings until you remove it and run it again. In this run, a recreated container's logs stayed at 3 files of 9.6M or less.

Step 6: Find deleted files that still hold space

A deleted file keeps its space until every process that has it open closes it. When df shows a full disk and du cannot find the files, compare the two:

sudo du -sh /srv/data
df -h /srv/data

Replace /srv/data with the mount point that df showed as full, such as /. The output looks similar to this:

28K	/srv/data
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0      109M   91M  9.9M  91% /srv/data

du finds 28K of files while df counts 91M. List the open files on that filesystem that no longer have a name:

sudo lsof +aL1 /srv/data

Without a mount point, lsof +L1 also lists memory mappings and files on every other filesystem. The output looks similar to this:

COMMAND  PID   USER FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 7264 deploy 3w   REG    7,0 94371840     0   16 /srv/data/report-worker/worker.log.2 (deleted)

SIZE/OFF is the size in bytes, here 90 MB. In this example, logrotate renamed the service's log and later removed the old copy while the service still wrote to it. The copytruncate option from Step 3 avoids this for programs that keep their log open.

If lsof is not installed and the disk is too full to install it, list the same files from /proc:

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

Each line shows /proc/PID/fd/FD and the deleted file it points to, on every filesystem. Find the service that runs the process. Replace YOUR_PID with the number in the PID column:

systemctl status YOUR_PID --no-pager

The first line names the service:

● report-worker.service - Test worker that keeps its log file open
...

Restart the service to close the file:

sudo systemctl restart report-worker
df -h /srv/data

Replace report-worker with your service and /srv/data with your filesystem. In this run Used dropped from 91M to 160K. A reboot also releases these files, because it stops every process.

If you cannot restart the process, check that the descriptor still points to the deleted file. Replace YOUR_PID with the PID and YOUR_FD with the number in the FD column without its letter (3w becomes 3):

sudo ls -l /proc/YOUR_PID/fd/YOUR_FD

The line ends in (deleted). Then empty the file while the process keeps running:

sudo truncate -s 0 /proc/YOUR_PID/fd/YOUR_FD

Step 7: Get a warning before the disk fills

A small script run by a systemd timer writes a warning to the journal when the root filesystem passes 85% of its space or inodes. Create the script:

sudo tee /usr/local/bin/disk-space-check > /dev/null <<'EOF'
#!/bin/sh
LIMIT="${LIMIT:-85}"
for MOUNT in ${MOUNTS:-/}; do
  SPACE=$(df --output=pcent "$MOUNT" | tail -n 1 | tr -dc "0-9")
  INODES=$(df --output=ipcent "$MOUNT" | tail -n 1 | tr -dc "0-9")
  if [ "${SPACE:-0}" -ge "$LIMIT" ] || [ "${INODES:-0}" -ge "$LIMIT" ]; then
    MSG="$(hostname) $MOUNT: ${SPACE}% of disk space and ${INODES}% of inodes used"
    logger -p user.warning -t disk-space "$MSG"
    if [ -n "$WEBHOOK_URL" ]; then
      curl -fsS -m 10 -d "$MSG" "$WEBHOOK_URL" > /dev/null
    fi
  fi
done
EOF
sudo chmod 755 /usr/local/bin/disk-space-check

Create the service that runs it:

sudo tee /etc/systemd/system/disk-space-check.service > /dev/null <<'EOF'
[Unit]
Description=Warn when disk space or inodes run low

[Service]
Type=oneshot
Environment=MOUNTS=/
EnvironmentFile=-/etc/disk-space-check.env
ExecStart=/usr/local/bin/disk-space-check
EOF

To check more filesystems, list them in MOUNTS, for example Environment="MOUNTS=/ /boot". Run sudo systemctl daemon-reload after each change to the file.

To get the warning on your phone, store an address that takes the message as a POST body, such as an ntfy topic URL, in a file only root can read. ntfy's documentation says the topic name works like a password, and systemd's says that Environment= values are visible to unprivileged users:

sudo install -m 600 /dev/null /etc/disk-space-check.env
echo 'WEBHOOK_URL=YOUR_WEBHOOK_URL' | sudo tee /etc/disk-space-check.env > /dev/null

Replace YOUR_WEBHOOK_URL with your address. The script posts with curl, so run sudo apt install curl if command -v curl prints nothing. Create the timer that runs the check every hour:

sudo tee /etc/systemd/system/disk-space-check.timer > /dev/null <<'EOF'
[Unit]
Description=Check disk space every hour

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target
EOF

Enable the timer and check its next run:

sudo systemctl daemon-reload
sudo systemctl enable --now disk-space-check.timer
systemctl list-timers disk-space-check.timer --no-pager
NEXT                         LEFT LAST PASSED UNIT                   ACTIVATES
Fri 2026-11-06 19:00:00 UTC 55min -         - disk-space-check.timer disk-space-check.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

Test the warning with a limit of 1%, using the webhook file if you created one, and read it from the journal:

sudo systemd-run --wait -q -p EnvironmentFile=-/etc/disk-space-check.env -E LIMIT=1 /usr/local/bin/disk-space-check
sudo journalctl -t disk-space -n 1 --no-pager
Nov 06 18:53:03 nospace-test disk-space[114]: nospace-test /: 50% of disk space and 13% of inodes used

With a webhook file, the same message goes to that address. Then run the service once the way the timer does:

sudo systemctl start disk-space-check.service
systemctl show -p Result --value disk-space-check.service

The second command prints success. Run df -h and df -i from Step 1 again to confirm that space and inodes are back below the limit.

When to grow the disk instead

Grow the disk when the space goes to data you keep, such as databases, uploads and the images your containers run, or when Steps 2 to 6 recover little. On Arct Cloud, resizing a server to a larger plan keeps the IP address and data and requires a restart, and disk capacity can increase but cannot be reduced. See Upgrade or resize a VPS for these details. The disk size of each plan is listed on the pricing page. After the restart, check the root filesystem with df -h /.

Troubleshoot common disk space errors

SymptomCauseFix
No space left on device while df -h shows spaceNo inodes left: df -i shows IUse% at 100%Find the folder with sudo du --inodes -x / (Step 2) and delete the files it no longer needs
No space left on device from a program that watches files, while df -h and df -i show spaceThe per-user inotify watch limit, which the kernel reports as ENOSPCcat /proc/sys/fs/inotify/max_user_watches shows the limit, described in the inotify(7) manual page
Use% stays the same after you delete filesA process still holds a deleted file opensudo lsof +aL1 /, then restart that service (Step 6)
No space left on device when a program writes to /tmp/tmp is a tmpfs limited to half of the RAM on Ubuntu 26.04 and Debian 13Check df -h /tmp and delete files there, or change the size with sudo systemctl edit tmp.mount as the Debian 13 release notes show
You don't have enough free space in /var/cache/apt/archives/. after E: or Error:The filesystem that holds the apt cache is fullsudo apt clean, then Steps 2 and 3
docker build prints No space left on device in a RUN step, then ERROR: failed to build: failed to solveThe filesystem that holds /var/lib/containerd or /var/lib/docker is fulldocker builder prune clears what the failed build left, then docker system df and Step 5
journalctl --vacuum-size reports freed 0B on every line while journalctl --disk-usage stays above the limitVacuuming removes archived journal files onlysudo journalctl --rotate --vacuum-size=200M archives the active files first

FAQ

What does no space left on device mean on Linux?

A write failed because the filesystem has run out of data blocks or inodes. The kernel returns the ENOSPC error, number 28, which programs print as No space left on device or [Errno 28]. Run df -h to check space and df -i to check inodes.

How do I fix no space left on device?

Find the full filesystem with df -h and df -i, then find the largest folders with sudo du -xh --max-depth=1 /. Shrink the journal with journalctl --vacuum-size, run sudo apt clean, prune unused Docker images and build cache, and restart any process that holds a deleted file open.

Why does Docker say no space left on device?

The filesystem that holds Docker's data is full. Docker keeps images, build cache, container logs and volumes in /var/lib/docker and, on fresh installs of Docker Engine 29 or later, /var/lib/containerd. Run docker system df, then docker builder prune -a and docker image prune -a.

Why does df show space but I still get no space left on device?

The filesystem has run out of inodes, which df -i shows as 100% in IUse%, or the write goes to another filesystem such as /tmp. A program that watches files gets the same error when it reaches the inotify watch limit in /proc/sys/fs/inotify/max_user_watches.

Is it safe to delete log files in /var/log?

Yes for rotated copies, such as files ending in .1 or .gz, once you no longer need them. Empty an active log with sudo truncate -s 0 instead of deleting it, and shrink /var/log/journal with journalctl, not rm.

How do I check disk space on a Linux server?

Run df -h to see the size, used space and available space of each mounted filesystem, and df -i to see inodes. To find the folders that use the space, run sudo du -xh --max-depth=1 / | sort -h and repeat it on the largest folder.

How do I clear journal logs?

Run sudo journalctl --vacuum-size=200M to delete the oldest archived journal files until they use less than 200 MB. To keep the journal small, set SystemMaxUse=200M in /etc/systemd/journald.conf.d/50-max-use.conf and restart systemd-journald. By default, the journal can use 10% of the filesystem, up to 4 GB.

Next steps

Arct Cloud Linux images deploy in under 30 seconds, with NVMe storage on every plan. Compare plans.

This work is licensed under CC BY-NC-SA 4.0.