Recover files deleted with rm -rf
You ran rm -rf on the wrong path. Stop writing to that disk now, then work through the recovery options that actually exist on ext4, in order.
What to do in the first sixty seconds
Two things decide whether you recover files deleted with rm -rf, and both of them happen before you open a search engine. Stop writing to that filesystem. Then take it out of use, by unmounting it or by remounting it read-only.
rm does not erase anything. It removes the directory entry, then marks the inode and the file's data blocks as free. The bytes are still sitting on the device. They stay there until the block allocator hands those blocks to something else and that something writes over them. Every second the filesystem stays mounted and busy, a daemon writes a log line or a database flushes a page, and either write can land on the blocks you want back.
So the first commands are the ones that stop writes, not the ones that recover files.
sudo systemctl stop nginx postgresql
sudo umount /mnt/dataIf umount answers umount: /mnt/data: target is busy., find what is holding the filesystem open.
sudo fuser -vm /mnt/data
sudo lsof +D /mnt/dataIf you cannot free it, remount it read-only instead. A read-only mount stops new allocations, which is most of what you need.
sudo mount -o remount,ro /mnt/dataIf the deleted path was on the root filesystem, this is harder. sudo mount -o remount,ro / will usually fail with mount: /: cannot remount /dev/vda1 read-only., because running processes hold files open for writing and the kernel will not force them closed. On a VPS the practical answer is your provider's rescue or recovery mode: it boots a separate live system with your disk attached but not mounted. Every command below then runs against a device nobody is writing to.
One rule applies to the whole of this guide. Never write recovered files, a disk image, or a newly installed tool onto the filesystem you are recovering from. Attach a second volume, or send the output to another machine over SSH.
Why rm -rf recovery on ext4 is mostly hopeless
Set your expectations before you install anything. Confirm which filesystem you are dealing with:
lsblk -fOn ext4, the default on nearly every VPS image, a file's data location lives in its inode as an extent tree. An extent is one record saying that logical block N of this file starts at physical block M and runs for L blocks. Small files keep up to four of those records inside the inode itself. Larger files point at extra blocks holding the rest of the tree.
When the last link to a file goes away, ext4 walks that tree, returns every extent to the block allocator, and clears the tree out of the inode. The inode is then marked free and stamped with a deletion time. The data itself is untouched. The only record of where that data was has been erased.
That is the difference from ext3, where a deleted inode kept enough behind for a tool like ext3grep to follow. You can still list deleted inodes on ext4:
sudo debugfs -R lsdel /dev/vdb1debugfs opens the device read-only unless you pass -w, so this is safe on an unmounted device and it costs nothing to try. Inodes will be listed. Dumping one is where it ends, because the block map that inode used to hold has been cleared, so dump has nothing to follow.
Two tools try to work around this by reading the journal. The journal is a fixed size ring that ext4 uses to keep metadata consistent across a crash, and it can still hold an older copy of the inode from before the delete. extundelete and ext4magic both search it. Check what size you are working with:
sudo dumpe2fs -h /dev/vdb1 | grep -i journalThe journal holds metadata only and it is small, so ordinary write activity cycles straight through it. On a running server the window in which the pre-delete inode still exists is measured in minutes. Neither tool is actively maintained, and neither is packaged on every distribution. Treat both as a long shot, run them against an unmounted device or against a disk image, and do not be surprised when they return nothing.
If lsblk -f reports xfs, the picture is no better, because there is no supported undelete for XFS either. The order of options below does not change.
Is the file still open in a running process?
This is the one recovery on this page with good odds, and it is the reason you should not restart the service that was using the file.
A file is only truly gone when two counts reach zero: the number of directory entries pointing at its inode, and the number of open file descriptors. rm takes the first count to zero. If a process still holds the file open, the second count is not zero, so the inode and its blocks are still allocated and the data is still readable.
Find open files whose link count has dropped to zero:
sudo lsof +L1+L1 means list open files with a link count below 1. Each match shows the process, the file descriptor number, an NLINK of 0, and a path ending in (deleted). Take the PID and the descriptor number to /proc:
sudo ls -l /proc/1234/fdAn entry looks like 3 -> /var/log/app/events.log (deleted). That link still reaches the data. Copy it out to a different filesystem:
sudo cp /proc/1234/fd/3 /mnt/rescue/events.logUse cp, not mv. Opening /proc/1234/fd/3 gives you a fresh handle on the same inode starting at offset zero, so you get the whole file rather than the part after the writer's current position.
There are two limits worth knowing. A deleted directory tree does not come back this way, because only the individual files a process had open are still held. And a database file copied while the engine is mid-write is a crash-consistent copy, so plan to run the engine's own recovery on it instead of treating it as clean. Entries that lsof shows with mem in place of a descriptor number are memory mapped, and those have no /proc/<pid>/fd entry to copy from.
Do you have a snapshot on btrfs, ZFS or LVM?
If the filesystem takes snapshots, the deleted files are already sitting inside one, unchanged. This only helps if a snapshot existed before the delete. Nothing you create now reaches backwards.
btrfs keeps snapshots as subvolumes:
sudo btrfs subvolume list /Browse the snapshot and copy out the paths you need with cp -a. Prefer copying single paths over rolling back a whole subvolume, because a rollback also discards everything written since the snapshot was taken.
ZFS exposes every snapshot as a read-only directory:
zfs list -t snapshot
ls /tank/data/.zfs/snapshot/The .zfs directory is hidden and will not appear in a plain ls of the dataset root, but you can enter it by name. Copy files out from there. zfs rollback moves the whole dataset back and destroys every snapshot newer than the one you name, so keep it as the last resort.
LVM snapshots are copy-on-write volumes with a fixed size:
sudo lvs
sudo mount -o ro /dev/vg0/data-snap /mnt/snapMount it read-only and copy out. Check lvs before you trust it, because an LVM snapshot that fills its allocated space is invalidated by the kernel, and once that has happened its contents are gone.
A snapshot is not a backup. It lives on the same disk or the same pool as the original, so it shares every failure the original has. It is very good at undoing a mistake from two minutes ago, which is exactly the job here.
Carving with PhotoRec, against an image and never the live disk
If nothing above applies, what is left is carving: scanning the raw device for byte patterns that mark the start of a known file type, then writing out whatever follows. Carving reads file data only. Filenames, directory structure, timestamps and ownership are all filesystem metadata, and that metadata is what rm destroyed, so none of it comes back. You get files called f0384512.jpg in a numbered output directory, and you sort them by hand.
Two rules decide whether this works at all.
First, image the device before you point anything else at it. On Debian and Ubuntu the package is gddrescue and the binary it installs is ddrescue.
sudo apt update && sudo apt install -y gddrescue testdisk
sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map/mnt/rescue has to be on a different device, with at least as much free space as the partition holds. lsblk -b prints the exact sizes in bytes. The map file lets an interrupted copy resume instead of starting over. Once you have the image you can try a second tool later against exactly the same bytes, which you cannot do if the first tool wrote over the disk.
Second, point the recovery tool at the image file.
sudo photorec /d /mnt/rescue/recup /mnt/rescue/vdb1.imgphotorec opens a text menu. Choose the partition, then the filesystem type, then the file signatures to search for, then the destination directory. Narrow that signature list to the file types you actually lost before you start, because the default list finds everything and hands you tens of thousands of fragments to sift.
testdisk, from the same package, has its own undelete function, and it covers FAT, exFAT, NTFS and ext2 only. On ext4 that leaves photorec.
Expect fragmented files to come back broken. Carving assumes a file's blocks are contiguous, so a file the allocator split across the disk is either reassembled wrong or missed entirely. Media files carve reasonably well because they have strong headers. Plain text, configuration and source code carve badly, because there is no byte signature that marks the start of a shell script.
The stray space: how the wrong path got deleted
Almost every rm -rf accident is a shell problem. rm receives a list of paths and removes each one in turn. It never sees what you meant.
The classic is a single space:
rm -rf /home/deploy/app /old
rm -rf /home/deploy/app/oldThe first line is two arguments. It deletes the application, then it deletes /old. If /old does not exist, rm prints nothing at all, because -f suppresses the missing-file error. Silence is not confirmation.
The second shape is an unquoted variable holding a space:
dir="/srv/my app"
rm -rf $dirThe shell splits the value on whitespace, so rm receives /srv/my and app as two separate paths. Written as rm -rf "$dir" it is one path.
The third is an empty variable, usually because the command that should have filled it failed:
rm -rf "$TARGET"/*With TARGET unset, that expands to rm -rf /*. GNU rm refuses the bare form: rm -rf / prints rm: it is dangerous to operate recursively on '/' and stops. The glob form gets no such protection, because the shell replaces /* with a list of real top-level paths before rm ever runs, and / is not one of them, so the guard never fires.
Habits that prevent the next one
- Quote every variable used as a path. Write
"$dir"every time, including inside tests and loops. - Fail on empty.
rm -rf "${TARGET:?TARGET is not set}"/*makes the shell stop with your message beforermstarts, wheneverTARGETis unset or empty. Putset -euo pipefailat the top of any script that deletes. - Add
--one-file-system. It tellsrmto skip any directory sitting on a different filesystem from the argument you gave it, so a recursive delete cannot walk into a mounted backup volume or a bind mount. - Do not delete as root. A service account can only destroy what it owns, which is the whole argument for running each service as its own unprivileged user. If you are unsure what a given account can reach, reading the permission bits in an ls listing answers it in one command.
- Print the list before you act on it. In a script, build the paths,
printf '%s\n'them, read the output, then delete on a second pass. - Keep a trash command in reach.
sudo apt install trash-cligives youtrash-put,trash-list,trash-restoreandtrash-empty. Deleted files move to~/.local/share/Trash, andtrash-empty 30clears anything older than thirty days.
Aliasing rm to trash-put sounds like the obvious next step, and it is a trap. The alias trains a reflex that fails on the next server that does not have it, and aliases do not apply inside scripts, which is where the expensive mistakes happen. Type trash-put on purpose instead.
The only recovery that works every time
Everything above is a chance. A backup is not a chance.
Two things make a backup real. It runs on a schedule without you remembering it, and you have restored from it at least once. A repository nobody has ever restored from is a belief, because the things that make one useless (a wrong path in the include list, or a repository password nobody wrote down) only show up on the day you need it.
With restic, a restore is two commands.
restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo restore latest --target /mnt/rescue --include /srv/appdataRestore into an empty directory rather than over the live path, so you can compare the two before anything moves into place. Setting up restic backups on a VPS covers the repository setup and the systemd timer that runs it.
With Borg:
borg list /srv/borg-repo
borg extract /srv/borg-repo::daily-2026-08-08 srv/appdataPaths inside a Borg archive are stored without the leading slash, so srv/appdata matches and /srv/appdata matches nothing. borg extract writes into the current working directory, so cd to a scratch directory first.
If you have not chosen between them yet, the comparison of restic and Borg covers deduplication and append-only repositories, which is the property that stops a compromised server from deleting its own backup history. Either tool is fine. The wrong answer is neither one running.
A new server is the cheapest moment to set this up, before there is anything on it worth losing. The first ten minutes on a new VPS is where that work belongs, alongside the SSH and firewall setup.
Then put a recurring entry in your calendar: restore one directory from the repository into /tmp every month and read the files. That single habit is worth more than every tool on this page.
FAQ
Can I undelete a file on ext4?
Usually no. When the last link to a file goes, ext4 clears the extent tree out of the inode, so nothing on disk records where the data lived. extundelete and ext4magic search the ext4 journal for an older copy of that inode, which only helps if the delete happened minutes ago and the filesystem has been quiet since. Neither project is actively maintained. Run either one against an unmounted device or a disk image, never against a mounted filesystem, and check what you are working with first using sudo dumpe2fs -h /dev/vdb1 | grep -i journal.
A service still has the deleted file open. Can I get it back?
Yes, and this is the best case. While a process holds the file open, its inode and data blocks stay allocated, so the data is still readable. Do not restart the service, because closing the last descriptor completes the delete. Run sudo lsof +L1 to list open files with a link count of 0, note the PID and the file descriptor number, then copy through /proc with sudo cp /proc/1234/fd/3 /mnt/rescue/events.log. Write the copy to a different filesystem. Entries shown with mem instead of a descriptor number are memory mapped and have no /proc/<pid>/fd path to copy from.
Why should I image the disk instead of running the recovery tool on it?
Because every tool has to write its output somewhere, and a write to the filesystem you are recovering can land on the free blocks that still hold your data. Copy the partition to a different device first with sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map, then point photorec at the image file. The image also lets you try a second tool later against exactly the same bytes, which is impossible once something has written over the original.
Does rm -rf / still destroy a Linux system?
The bare command does not. GNU rm refuses it and prints rm: it is dangerous to operate recursively on '/'. The dangerous shapes are the ones that arrive by another route. rm -rf "$TARGET"/* with TARGET unset expands to rm -rf /*, and the shell hands rm a list of real top-level directories, none of which is /, so the guard never triggers. Write "${TARGET:?TARGET is not set}" instead and the shell stops before rm runs.
Is a filesystem snapshot a backup?
No. A btrfs or ZFS snapshot sits on the same pool as the data it protects, so a failed disk or a destroyed pool takes both at once. An LVM snapshot has the extra problem of a fixed size: once it fills, the kernel invalidates it and its contents are gone. Snapshots are excellent for undoing a delete from two minutes ago. For everything else, keep a repository on separate hardware.