> ## Documentation Index
> Fetch the complete documentation index at: https://blog.onetsolutions.net/llms.txt
> Use this file to discover all available pages before exploring further.

# No space left on device on a Linux VPS: find and free disk space

> No space left on device on your Linux VPS? Find what fills the disk and free space safely: journal, logs, Docker, MySQL binlogs. An OnetSolutions guide.

*Published on November 14, 2026*

"No space left on device": your Linux VPS has filled its disk. From that moment, everything that writes fails. The database rejects queries, PHP can no longer create sessions, an update stops halfway, the journal drops lines.

The good news: the space can almost always be recovered without breaking anything, once you know where it went. This guide follows the order of a diagnosis, on Debian, Ubuntu, AlmaLinux and Rocky Linux. Each section gives the problem, the command that fixes it and how to check.

## Where a VPS's disk space goes

The most common sources, as of November 14, 2026:

| Source | Measure | Free safely |
| - | - | - |
| Deleted files still held open | `lsof -nP +L1` | Restart the service holding them |
| systemd journal | `journalctl --disk-usage` | `journalctl --vacuum-size=` |
| Application logs | `du -sh /var/log/*` | `truncate -s 0`, then logrotate |
| Package cache | `du -sh /var/cache/apt` or `/var/cache/dnf` | `apt clean`, `dnf clean all` |
| Old kernels | `dpkg -l 'linux-image*'`, `rpm -q kernel` | `apt autoremove --purge`, `dnf remove --oldinstallonly` |
| Docker | `docker system df` | `docker system prune` |
| MySQL / MariaDB binlogs | `SHOW BINARY LOGS;` | `PURGE BINARY LOGS BEFORE ...` |
| Exhausted inodes | `df -i` | Delete small files in bulk (sessions, cache) |

## Before cleaning up: take a snapshot

A cleanup is quick. So is one deletion too many. Take a [snapshot](https://help.onetsolutions.net/vps/snapshot) from the console before touching binlogs, Docker volumes or kernels. You keep a one-click way back.

[Backups](https://help.onetsolutions.net/vps/backup) remain your baseline protection.

## Diagnose a full disk

### Which filesystem is full

```bash theme={null}
# Size, usage and type of each filesystem
df -hT -x tmpfs -x devtmpfs
```

Find the line at 100% and its mount point (`Mounted on`). Most of the time, it is `/`.

### Blocks or inodes

A disk can be full while `df -h` shows free space. Every file uses one inode, and on ext4 their number is set when the filesystem is created. Millions of small files run out of inodes before they run out of space.

```bash theme={null}
# IUse% column: 100% means no new file can be created
df -i -x tmpfs -x devtmpfs
```

If `IUse%` reads 100%, find the directory holding the most files:

```bash theme={null}
# Inode count per directory, staying on /
sudo du --inodes -x -d 1 / | sort -n | tail -n 10
```

Then go one level down into the top directory (`sudo du --inodes -x -d 1 /var | sort -n | tail`). The usual culprits: PHP sessions, application cache, mail queue.

### Find the largest directories

```bash theme={null}
# The 15 largest top-level directories, staying on /
sudo du -xh -d 1 / 2>/dev/null | sort -h | tail -n 15
```

The `-x` option matters: it stays on the filesystem of `/` and skips other mounts.

For faster navigation, `ncdu` does the same interactively:

```bash theme={null}
# Debian, Ubuntu
sudo apt install ncdu
# AlmaLinux, Rocky (EPEL repository)
sudo dnf install epel-release && sudo dnf install ncdu

sudo ncdu -x /
```

At 100% usage, the install itself may fail. Start with `apt clean` or `dnf clean all` (below) to make some room.

## Deleted files that are still open

**Problem.** You deleted a large log with `rm` and `df` did not move. The file is gone from the tree, but a process still holds it open: the space only comes back when the file is closed. The same applies when `du` reports far less than `df`.

**Fix.** List open files whose last link was removed:

```bash theme={null}
# Install lsof if needed: apt install lsof / dnf install lsof
sudo lsof -nP +L1
```

`COMMAND` gives the process, `SIZE/OFF` the size. Restart the service involved:

```bash theme={null}
sudo systemctl restart nginx
```

If the service cannot be restarted right away, empty the file through its descriptor. The `PID` and `FD` (without the trailing letter, `4w` becomes `4`) come from the `lsof` output:

```bash theme={null}
sudo truncate -s 0 /proc/1234/fd/4
```

**Check.** `sudo lsof -nP +L1` no longer lists the file, and `df -h /` shows the space returned.

## Shrink the systemd journal

**Problem.** `/var/log/journal` takes several gigabytes.

**Fix.** Measure, then shrink:

```bash theme={null}
# Space used by the journal
journalctl --disk-usage

# Keep at most 500 MB of archives
sudo journalctl --vacuum-size=500M

# Or keep only the last two weeks
sudo journalctl --vacuum-time=2weeks
```

Vacuuming only touches archived files. To make it stick, set a cap. By default, journald allows itself 10% of the filesystem. The `journald.conf` man page recommends a separate file in `/etc/systemd/journald.conf.d/` rather than editing the main file:

```bash theme={null}
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/50-size.conf >/dev/null <<'EOF'
[Journal]
SystemMaxUse=500M
EOF
sudo systemctl restart systemd-journald
```

**Check.** `journalctl --disk-usage` stays under the chosen value over the following days.

## Empty and rotate application logs

**Problem.** A file in `/var/log` or in an application directory grows forever.

**Fix.** Find it:

```bash theme={null}
sudo du -xah /var/log 2>/dev/null | sort -h | tail -n 15
```

To empty it without stopping the service writing to it, use `truncate` rather than `rm`:

```bash theme={null}
sudo truncate -s 0 /var/log/myapp/app.log
```

Then give it a rotation. logrotate ships with all four distributions and reads `/etc/logrotate.d/`:

```bash theme={null}
sudo tee /etc/logrotate.d/myapp >/dev/null <<'EOF'
/var/log/myapp/*.log {
    daily
    rotate 7
    # Rotate early if the file grows past 100 MB
    maxsize 100M
    compress
    delaycompress
    missingok
    notifempty
    # For an application that does not reopen its log file
    copytruncate
}
EOF
```

`copytruncate` copies the file, then empties it in place: the application keeps writing to the same file. If it can reopen its logs on a signal, prefer a `postrotate` block that sends it.

**Check.** Debug mode simulates the rotation without changing anything:

```bash theme={null}
sudo logrotate -d /etc/logrotate.d/myapp
```

## Clear the package cache

**Problem.** Packages downloaded during updates stay on disk.

**Fix.**

```bash theme={null}
# Debian, Ubuntu: removes the .deb files in /var/cache/apt/archives
sudo apt clean

# AlmaLinux, Rocky: removes cached metadata and packages
sudo dnf clean all
```

Both are safe: packages are downloaded again when needed.

**Check.** `sudo du -sh /var/cache/apt` or `sudo du -sh /var/cache/dnf` is back to a few megabytes.

## Remove old kernels

**Problem.** Each kernel update installs a new version next to the previous ones, with its modules in `/lib/modules` and its image in `/boot`.

**Fix on Debian and Ubuntu.** Simulate first, then remove:

```bash theme={null}
# Lists what would be removed, without doing anything
sudo apt autoremove --purge -s

sudo apt autoremove --purge
```

APT protects the running kernel and the newest one. Read the list anyway: `autoremove` also removes dependencies that have become orphans.

**Fix on AlmaLinux and Rocky.** dnf keeps a fixed number of versions of "installonly" packages, the kernel among them, set by `installonly_limit` in `/etc/dnf/dnf.conf`. To remove the extra versions now:

```bash theme={null}
# Installed kernels
rpm -q kernel

# Removes old versions beyond installonly_limit,
# never the running kernel
sudo dnf remove --oldinstallonly kernel
```

**Check.** `uname -r` gives the running kernel, which is still listed by `dpkg -l 'linux-image*'` or `rpm -q kernel`.

## Clean up Docker

**Problem.** `/var/lib/docker` keeps growing: replaced images, stopped containers, build cache, container logs.

**Fix.** Measure by category:

```bash theme={null}
docker system df
```

Then clean what is no longer used:

```bash theme={null}
# Stopped containers, unused networks, dangling images, build cache
docker system prune

# Also: every image no container uses
docker system prune -a
```

> [!WARNING]
>
> Do not add `--volumes` before checking the list with `docker volume ls`. A volume often holds a database's data. A deleted volume cannot be recovered, except from a snapshot or a backup.

Container logs are the other frequent source. The default driver, `json-file`, does not rotate. Find the largest:

```bash theme={null}
sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log' | sort -h | tail -n 5
```

Turn on rotation in `/etc/docker/daemon.json` (merge with the existing content if the file exists):

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

```bash theme={null}
sudo systemctl restart docker
```

The Docker documentation is explicit: the setting only applies to containers created afterwards. Recreate the existing ones (`docker compose up -d --force-recreate` for a Compose project).

**Check.** `docker system df` shows a `RECLAIMABLE` column close to zero.

## Purge MySQL and MariaDB binlogs

**Problem.** `/var/lib/mysql` holds dozens of `binlog.000123` or `mysql-bin.000123` files.

**Fix.** Never delete them with `rm`: the server keeps an index of these files. Use SQL:

```sql theme={null}
-- List binlogs and their size
SHOW BINARY LOGS;

-- Remove those older than 7 days
PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;
```

With a replica, first check on it which file it is reading (`SHOW REPLICA STATUS\G`): a stopped or lagging replica still needs the binlogs it has not read.

For automatic expiry, the variable depends on the server:

| Server | Variable | Default |
| - | - | - |
| MySQL 8.0 and 8.4 | `binlog_expire_logs_seconds` | 2592000 (30 days) |
| MariaDB 10.6 and later | `binlog_expire_logs_seconds` or its alias `expire_logs_days` | 0 (no expiry) |
| MariaDB 10.5 and earlier | `expire_logs_days` | 0 (no expiry) |

`expire_logs_days` was removed from MySQL in 8.2: on MySQL 8.4, only `binlog_expire_logs_seconds` exists.

Apply the value live, then make it permanent:

```sql theme={null}
SET GLOBAL binlog_expire_logs_seconds = 604800;
```

```bash theme={null}
# Debian, Ubuntu: /etc/mysql/conf.d/ is read by MySQL and MariaDB
# AlmaLinux, Rocky: use /etc/my.cnf.d/binlog.cnf
sudo tee /etc/mysql/conf.d/binlog.cnf >/dev/null <<'EOF'
[mysqld]
# 7 days
binlog_expire_logs_seconds = 604800
EOF
```

**Check.** `SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';` returns `604800`, and `SHOW BINARY LOGS;` goes back no further than seven days.

## The ext4 root reserve

**Problem.** `df` shows 100% for your applications, yet root can still write. That is by design.

**Explanation.** By default, ext4 reserves 5% of blocks for privileged processes. The `tune2fs` man page gives the reason: limit fragmentation and keep system daemons running once regular users can no longer write. This reserve is what still lets you log in and clean up.

```bash theme={null}
# Partition holding /
findmnt -n -o SOURCE,FSTYPE /

# Reserved blocks (replace /dev/vda1 with the value above)
sudo tune2fs -l /dev/vda1 | grep -i 'reserved block count'
```

On a large data disk, you can lower it. On `/`, only do it to get out of an emergency, then restore the value:

```bash theme={null}
# Reserve at 1%, applies to a mounted filesystem
sudo tune2fs -m 1 /dev/vda1

# Once space is freed: back to 5%
sudo tune2fs -m 5 /dev/vda1
```

> [!NOTE]
>
> The reserve only exists on ext2, ext3 and ext4. `tune2fs` does not apply to XFS: `findmnt` tells you the type.

## When the disk is simply too small

Once the cleanup is done, if `/` climbs back above 85% within a few weeks, your usage has simply grown. Pick a [VPS plan](https://help.onetsolutions.net/vps/offers) with more storage, then [extend the partition](https://help.onetsolutions.net/tutorials/how-to-extend-a-partition) to use the new space.

## When the VPS stops responding

| Situation | Action |
| - | - |
| SSH does not respond, or login fails | Open the [KVM console](https://help.onetsolutions.net/vps/kvm-console) and log in as root |
| The VPS no longer boots | Boot into [rescue mode](https://help.onetsolutions.net/vps/rescue-mode), mount the partition and free some space: our [rescue mode guide](https://blog.onetsolutions.net/en/vps-rescue-mode-repair-server-that-wont-boot.md) walks through it |
| A cleanup deleted something you needed | Restore the [snapshot](https://help.onetsolutions.net/vps/snapshot) taken beforehand |

## Prevent it: a threshold alert

A full disk is visible long before it happens. A short script warns you first, for space and for inodes:

```bash theme={null}
sudo tee /usr/local/bin/disk-alert >/dev/null <<'EOF'
#!/bin/sh
# Alert when a filesystem crosses the threshold (space or inodes)
THRESHOLD=85
DEST=admin@example.com

REPORT=$(
  for OPT in -P -iP; do
    df $OPT -x tmpfs -x devtmpfs -x overlay | awk -v t="$THRESHOLD" -v o="$OPT" '
      NR > 1 { sub("%", "", $5); if ($5 + 0 >= t) print $6, $5 "%", (o == "-iP" ? "inodes" : "space") }'
  done
)

[ -n "$REPORT" ] && printf '%s\n' "$REPORT" | mail -s "Disk almost full on $(hostname)" "$DEST"
exit 0
EOF
sudo chmod 755 /usr/local/bin/disk-alert

# Every 15 minutes
echo '*/15 * * * * root /usr/local/bin/disk-alert' | sudo tee /etc/cron.d/disk-alert >/dev/null
```

Sending relies on the `mail` command (`bsd-mailx` on Debian and Ubuntu, `s-nail` on AlmaLinux and Rocky) and a local mail server. Our [SPF, DKIM, DMARC and reverse DNS guide](https://blog.onetsolutions.net/en/spf-dkim-dmarc-reverse-dns-vps-mail-server.md) covers deliverability.

**Check.** Run the script with a threshold of 1 to get a test message:

```bash theme={null}
sudo sed 's/^THRESHOLD=85/THRESHOLD=1/' /usr/local/bin/disk-alert | sudo sh
```

Add a logrotate rule for every application that writes its own logs, and a cap on the journal: those are the two sources that come back fastest.

## Common pitfalls

**`rm` on an open log.** The space is not returned while the process runs. Use `truncate -s 0`, or restart the service afterwards.

**Deleting journal files by hand.** `journalctl --vacuum-size` only removes archives, never the file being written. Let it do the job rather than running `rm` in `/var/log/journal`.

**Files hidden under a mount point.** Files written to a directory before a disk was mounted on it take space on `/` but stay invisible to `du`. Bind-mount `/` elsewhere to see them:

```bash theme={null}
sudo mkdir -p /mnt/rootfs
sudo mount --bind / /mnt/rootfs
sudo du -xh -d 2 /mnt/rootfs 2>/dev/null | sort -h | tail -n 15
sudo umount /mnt/rootfs
```

**Out of inodes, not space.** `df -h` shows free space and the error persists: look at `df -i`.

**`docker system prune --volumes` in production.** It deletes volumes unused at that moment, including the one of a database whose container is stopped.

**Binlogs deleted by hand.** The server keeps their list in an index file that `rm` does not update. Always go through `PURGE BINARY LOGS`.

## Further reading

* [Snapshots](https://help.onetsolutions.net/vps/snapshot): a restore point before every risky cleanup
* [Backups](https://help.onetsolutions.net/vps/backup): the baseline protection for your data
* [Extend a partition](https://help.onetsolutions.net/tutorials/how-to-extend-a-partition) after moving to a larger plan
* [KVM console](https://help.onetsolutions.net/vps/kvm-console) and [rescue mode](https://help.onetsolutions.net/vps/rescue-mode): regain control of an unresponsive VPS
* [VPS that won't boot: rescue mode](https://blog.onetsolutions.net/en/vps-rescue-mode-repair-server-that-wont-boot.md), step by step
* [Automatic security updates](https://blog.onetsolutions.net/en/automatic-security-updates-linux-vps.md): they need free space to install, too

Have a question about a server hosted with us? [Our team is available](https://help.onetsolutions.net/console/support) from your client area.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.