VPS Tutorials
VPS Shows Free Disk Space but Can't Write Files (2026): How to Diagnose and Clean Up inode Exhaustion
Your VPS reports No space left on device, but df -h clearly shows free space? It's likely not capacity that's full, but inodes that are used up. This article explains the difference between df -h and df -i, how to locate the source of small files, and how to safely clean up Docker, journal, cache and session files.

There's a kind of VPS failure that easily confuses people:
No space left on device
Your first reaction is that the disk is full, so you run:
df -h
And you find that the / partition is only 60% used, so there's clearly still enough space. Yet website uploads fail, the database can't write, Nginx cache throws errors, PHP session creation fails, and even touch test.txt fails.
At this point, what you should suspect is not disk capacity, but inodes.
Simply put, disk space governs "how much file content can be stored," while inodes govern "how many files can be created." If millions of small files pile up on the server, space may not be used up yet, but inodes may already be exhausted. Linux will still report No space left on device, because to the filesystem, it has no place left to register new files.
First look at space:
df -h
Then look at inodes:
df -i
df -h looks at capacity, df -i looks at inodes. The GNU Coreutils documentation for df also lists -i / --inodes separately as inode usage information.
You may see something like this:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 1310720 1310500 220 100% /
This situation is a typical case of inodes nearly used up or already used up. At this point, even if df -h still has tens of GB of space, new files may not be creatable.
You can also do a safe test:
touch /tmp/inode-test
If this also reports No space left on device while df -i shows 100%, it's basically confirmed.
Inodes are usually not eaten up by large files, but by a large number of small files.
Common sources on a VPS include:
- PHP session files
- Nginx / CDN / reverse proxy cache
- Docker overlay2 small files
- npm, pip, composer caches
- Mail queues
- Temporary upload directories
- Small files produced by crawlers, scraping, log rotation
- Applications that write every request, every task, every message as a separate file
A 2 KB small file also occupies one inode. A large video of tens of GB may occupy only a few inodes, but millions of session files can quickly fill up inodes.
The most dangerous operation for an inode failure is deleting directories directly without knowing who is occupying the inodes.
In particular, don't blindly delete these paths:
/var/lib
/etc
/usr
/home
/var/lib/docker
Some directories look large or contain many files, but inside may be databases, Docker layers, package manager state, or data of running services. Delete the wrong thing, and the inode problem isn't solved while the system breaks first.
The correct order is: locate first, then clean; clean caches and temporary files first, then consider business data.
Start counting from the root partition, looking at the file count at each level:
sudo find / -xdev -type f | cut -d/ -f2 | sort | uniq -c | sort -nr | head
If /var has the most, look further down:
sudo find /var -xdev -type f | cut -d/ -f2-3 | sort | uniq -c | sort -nr | head
Continue drilling down:
sudo find /var -xdev -type f | cut -d/ -f2-4 | sort | uniq -c | sort -nr | head
The idea behind this kind of command is not to look at file size, but at "file count." Because when inodes are exhausted by small files, du -sh may not quickly expose the problem.
If you suspect a certain directory, such as PHP session:
sudo find /var/lib/php/sessions -type f | wc -l
If you suspect Docker:
sudo find /var/lib/docker -xdev -type f | wc -l
If you suspect Nginx cache:
sudo find /var/cache/nginx -type f | wc -l
Sometimes you've already deleted files, but space isn't freed. The reason is that processes still have these files open.
You can check:
sudo lsof +L1
This command lists files with a link count less than 1, that is, files already removed from the directory but still held by processes. It's more often used for capacity release issues, but it's also worth a look when troubleshooting disk-related anomalies.
If you find a log file held by a process, usually don't kill the process directly; first determine the service type. For Nginx, PHP-FPM, and application services, prefer reload or restart:
sudo systemctl reload nginx
sudo systemctl restart php8.2-fpm
sudo systemctl restart app
Before restarting, confirm whether this is a production service, to avoid unintentionally interrupting business.
If the systemd journal occupies many files, first look at its disk usage:
journalctl --disk-usage
To clean old logs, you can use the vacuum parameters officially supported by systemd, for example keeping only 7 days:
sudo journalctl --vacuum-time=7d
Or limit the total size:
sudo journalctl --vacuum-size=500M
Don't go directly into /var/log/journal and delete things by hand. journalctl --vacuum-* is a safer, more controllable way.
If you find the journal often grows very fast, keep asking: is some service frantically reporting errors? You can use:
journalctl -p warning --since "24 hours ago" --no-pager
systemctl --failed
Cleaning logs is only stopping the bleeding; what really needs fixing is the service that keeps flooding logs.
Docker is a big inode consumer, especially with frequent builds, frequent image pulls, unlimited container logs, and piled-up overlay2 layers.
First look at Docker usage:
docker system df
List containers:
docker ps -a
Before cleaning, confirm which images, containers, and volumes are still needed. A relatively safe first step is to clean stopped containers, unused networks, dangling images, and build cache:
docker system prune
If you're sure unused images can also be deleted, then use:
docker system prune -a
Note: -a will delete all images not currently used by containers. For production environments, it's best to first confirm whether deployment rollback depends on old images.
Don't directly delete /var/lib/docker/overlay2. This will break Docker's internal state and may prevent containers from starting.
A better prevention method is to add limits to container logs. For example, Docker daemon configuration:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
After changing, Docker needs to be restarted:
sudo systemctl restart docker
Restarting Docker affects containers, so schedule a window for production environments.
If inodes are eaten up by PHP sessions, first confirm the session directory:
php -i | grep session.save_path
Common locations:
/var/lib/php/sessions
Check the count:
sudo find /var/lib/php/sessions -type f | wc -l
When cleaning old sessions, don't delete everything directly. You can first delete those older than 7 days:
sudo find /var/lib/php/sessions -type f -mtime +7 -delete
If it's a cache directory, also try to clean by time:
sudo find /path/to/cache -type f -mtime +7 -delete
If the directory has a huge number of files, find itself may also run for a long time. It's recommended to run during off-peak hours, and first preview with a command without -delete:
sudo find /path/to/cache -type f -mtime +7 | head
Delete only after confirming there's no problem.
APT cache is usually not the biggest inode source, but it can serve as auxiliary cleanup.
View the cache directory:
sudo du -sh /var/cache/apt
Clean downloaded package cache:
sudo apt clean
Cleaning dependencies no longer needed requires more caution:
sudo apt autoremove
autoremove may delete packages you no longer explicitly depend on but are still important in your usage scenarios. Before running on a production machine, look carefully at the list of what will be deleted.
If the website can no longer write sessions, can't upload, and the database can't write, you can stop the bleeding in this order:
df -iconfirm whether inodes are at 100%- Find the directory with the most inodes
- Prioritize cleaning clear temporary files, caches, old sessions
- If it's Docker, first use
docker system df, then decide the prune scope - If it's logs, prioritize
journalctl --vacuum-*and logrotate - After cleaning, run
df -ito verify - Restart or reload affected services
Verify:
df -i
touch /tmp/inode-test
rm /tmp/inode-test
systemctl --failed
If inodes drop from 100% and touch succeeds, the filesystem has regained the ability to create files.
Prevention is more important than cleanup afterward.
It's recommended to do these:
- Monitor
df -i, not justdf -h - Set
max-sizeandmax-filefor Docker logs - Set retention time or size for systemd journal
- Configure logrotate to avoid infinite log slicing
- Set up regular cleanup for session, cache, upload tmp
- For businesses with especially many small files, don't put them all on the system disk
- Limit the number of files written to disk for scraping, crawler, and task queue programs
- When backing up, don't just back up large files; also pay attention to the number of small files
If the business naturally produces a large number of small files, such as image thumbnails, crawler caches, mail queues, object slices, you should consider:
- Mounting a separate data disk
- Using a filesystem better suited to certain scenarios, such as XFS
- Putting static objects in object storage
- Putting temporary data in Redis / database / queue systems
- Regularly archiving or compressing old small files
When you encounter No space left on device but df -h seems to still have space, check in this order:
-
df -hcheck capacity -
df -icheck inodes -
touch /tmp/inode-testverify whether files can be created -
findcount which directory has the most small files - Check PHP session, cache, Docker, journal, mail queue
- Preview before cleaning, don't directly
rm -rf - After cleaning, run
df -iagain - reload/restart affected services
- Add monitoring and retention policies for inodes, logs, caches, Docker logs
The core of the inode problem is not "the disk isn't big enough," but "the number of files is out of control." When you see space remaining but files can't be written, don't just stare at df -h; immediately add a df -i. Many seemingly mysterious VPS write failures have their answer right here.