Skip to content
Server Setup #VPS #Linux #Docker

"No Space Left on Device" on a VPS: Find What Filled the Disk and Stop It Happening Again

Your VPS disk is full and MySQL, Docker or apt is failing. Here is how to find what is using the space in a few minutes (Docker images and logs, the systemd journal, old backups, deleted files still held open, inodes) and the settings that keep it from filling up again.

"No space left on device" rarely arrives on its own. You usually meet it as something else: MySQL refusing to start, WordPress failing to upload an image, apt stopping halfway through an upgrade, or a Docker build dying with an error that mentions /var/lib/docker. On a small VPS with a 20–80 GB disk, a few culprits account for almost every case. This guide finds which one it is, frees space safely, and then stops it happening again.

Step 1: confirm what is actually full

df -h
df -i

The first command shows space, the second shows inodes (the number of files a filesystem can hold). Look at the Use% column for / and any other mount.

Terminal: tar fails with "gzip: stdout: No space left on device"; df -h shows the root filesystem 100% used with 0 available, and df -i shows only 2% of inodes used.
Step 1 on a full disk (reproduced on a 10 GB test server): / is at 100%, but inodes are fine, so it's a space problem.
  • Space at or near 100%: carry on with step 2.
  • Space fine but IUse% at 100%: you have run out of inodes, not bytes. Something has created millions of tiny files. Jump to the inodes section.
  • /tmp full but / fine: on Ubuntu 26.04, /tmp is a tmpfs that lives in RAM and has its own small size limit. Something is writing large temporary files; clear them or point that program at /var/tmp.

Step 2: find the big directories

Start from the top and work down, staying on one filesystem so you don't wander into /proc:

sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail -15

Then repeat on whichever directory is largest, for example sudo du -xh --max-depth=1 /var | sort -h | tail. If you prefer something interactive, sudo apt install ncdu and run sudo ncdu -x /. But on a completely full disk, apt may not be able to install anything until you free a little space first.

Terminal: du sorted by size shows /var at 3.4G and /root at 2.7G out of 7.7G counted; drilling down shows /var/www at 3.4G and /root/backups at 2.7G.
du points to /var/www and /root/backups. It counts 7.7G while df reports 9.8G used; the gap is explained further down.

On a typical VPS, the answer is almost always under /var. Here are the usual suspects, in the order I check them.

Culprit 1: Docker images, build cache and container logs

docker system df

This shows space used by images, containers, volumes and build cache, and how much of each is reclaimable. Old images pile up quickly if you deploy by pulling new tags, and build cache grows with every docker build.

The safe clean-up removes stopped containers, unused networks, dangling images and build cache:

docker system prune

To also remove every image not used by a running container, add -a. That is usually fine on a server that pulls its images from a registry, but slower to redeploy afterwards. Do not add --volumes unless you are certain: unused volumes can include the database volume of a stack you have stopped temporarily.

The other Docker culprit hides inside containers: their logs. By default Docker keeps every line a container writes to stdout, forever, in a JSON file:

sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log' | sort -h | tail

A chatty app can produce gigabytes. Emptying a log file in place is safe while the container runs:

sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log

Then fix it permanently in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Restart Docker with sudo systemctl restart docker. The limit applies to containers created after the change, so recreate yours with docker compose up -d --force-recreate. If you run several apps behind Caddy, our multi-app Docker guide shows where these settings fit.

Culprit 2: the systemd journal

journalctl --disk-usage

The journal caps itself at a percentage of the filesystem, but on a small disk that can still be several gigabytes. Trim it now:

sudo journalctl --vacuum-size=200M

Then set a permanent limit with a drop-in file:

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

Culprit 3: application logs and old log files

sudo du -sh /var/log/* | sort -h | tail

Check the web server, PHP-FPM and MySQL logs, plus your app's own logs. Laravel's storage/logs/laravel.log is a classic: with the default single channel it grows forever. Switch to LOG_CHANNEL=daily and set LOG_DAILY_DAYS=14 so Laravel deletes old files itself. WordPress sites with WP_DEBUG_LOG left on in production grow wp-content/debug.log the same way.

For anything else that writes its own log files, add a logrotate rule in /etc/logrotate.d/.

Culprit 4: backups stored on the same disk

A nightly database dump that is never deleted fills a disk at a steady, predictable rate, and it gives you no protection if the server itself dies. Look in your backup script's output folder, and in /root and home directories for *.sql, *.sql.gz and *.tar.gz files:

sudo find / -xdev -type f -size +500M -exec ls -lh {} + 2>/dev/null
Terminal: find lists the files over 500 MB: five 540 MB database dumps in /root/backups and a 3.4 GB laravel.log.
Five nightly dumps kept on the server, and a Laravel log that never rotates.

Keep a few local copies at most, and send the rest off the server. Our guide to automatic MySQL backups to Cloudflare R2 includes a retention rule, and this one makes sure you notice when a backup stops working, which a full disk is very good at causing.

Culprit 5: apt cache and old kernels

sudo apt clean
sudo apt autoremove --purge

This is rarely more than a few hundred megabytes, but on a disk at 100% it is often enough to get MySQL or apt running again while you deal with the real cause.

When df says full but du can't find it

If the numbers don't add up, a process is probably still holding a file that has been deleted. The space is not freed until that process closes it. This happens when someone runs rm on a large log file that a service still has open.

sudo lsof +L1
Terminal: df -h shows 9.8G used but du counts only 7.7G; lsof +L1 shows a php process still writing to /var/www/app/storage/logs/worker.log (deleted), about 2.2 GB.
The missing 2 GB: a deleted log file still held open by a PHP worker.

This lists open files with no remaining name on disk. Restart the service that owns them (for example sudo systemctl restart nginx) and the space comes back. Next time, empty an active log with truncate -s 0 instead of deleting it.

Terminal: truncating laravel.log, deleting the three oldest backups, stopping the worker process, then df -h showing 31% used with 6.5G free.
After the clean-up the disk drops from 100% to 31% used.

When you are out of inodes, not space

Find the directory with the most files:

sudo find / -xdev -type f | cut -d/ -f2-4 | sort | uniq -c | sort -n | tail

The usual causes are PHP session files in /var/lib/php/sessions that aren't being cleaned up, a cache directory written by an app (Laravel's storage/framework/cache, WordPress page-cache plugins), or a mail queue filled with failed messages. Delete the files the app no longer needs, then fix whatever stopped them being cleaned up.

Recovering services after you free space

Once there is room again, check what fell over:

systemctl --failed
sudo systemctl restart mysql   # or mariadb, php8.5-fpm, docker…

If apt was interrupted mid-upgrade, finish the job with sudo dpkg --configure -a and then sudo apt -f install.

Stop it happening again

  • Docker log limits in daemon.json, and a weekly docker image prune -f from cron.
  • A journald size limit.
  • Daily log files with a retention period for your apps.
  • Backups shipped off the server, with only a few kept locally.
  • A warning before you hit 100%. Even a one-line cron job that emails you when df passes 85% beats finding out from a broken site.

Not sure how a disk size like "40 GB" compares with what df -h reports in GiB? Our Data Size Converter shows both. And if you are running out of space regularly, compare what a bigger disk costs in our 2027 VPS price comparison.

The commands here are standard on Ubuntu 24.04 and 26.04 with Docker Engine and systemd. I checked the Docker logging options and journald settings against their official documentation as of October 2026.

Comments 0

Be the first to comment.

Leave a comment