A restore can return every row you have ever written and still leave you with a broken site.
The dump holds the rows. The rows hold paths — /storage/images/featured/pixabay_932113.webp — and a path is a promise about a filesystem the dump never read. Restore the database on a fresh machine and every one of those promises is broken at once, silently, with no error anywhere, because as far as the database is concerned nothing is missing.
Here is that gap measured on a real system.
The files are bigger than the database
database dump 49,768,559 bytes
media on disk 66,258,187 bytes across 2,765 files
A third again as much content as the thing being backed up, and none of it in the backup. This is normal for a content site rather than anything unusual about ours: the database is mostly text, which compresses well, and the media is mostly images, which do not.
The files live in a Docker named volume. mysqldump cannot read a volume and the schema does not describe one, so the backup pipeline is entirely unaware they exist. The backup is not failing to include them. It was never asked to.
What the rows do and do not know
Across the whole database, 695 distinct file paths are referenced by some row, and 682 of them resolve to a real file today. That is the part the database can see, and if you restore the dump onto an empty disk it is also the part that will visibly break: 695 references, zero files, every image on the site a broken rectangle.
The larger number is the one to sit with:
2,765 files on disk
682 that some row points at
2,083 that nothing in the database mentions at all
Three quarters of the media files — thumbnails, derivatives, older uploads, files left behind when the owning record was deleted — have no corresponding row. Every content system accumulates them. A database-only restore cannot report them missing, because nothing in the restored database knows they ever existed. The broken images at least announce themselves. These leave no trace, because the record that would have missed them is not in the backup either.
What could be rebuilt, and what could not
Not all of it is equally lost. Our image rows record which provider an asset came from and its id there, which is in principle enough to fetch it again:
313 asset rows with a stored file
138 that carry a provider and an asset id (44.1%)
So a little under half of the described assets have a route back, and that route runs through somebody else's service, on their retention policy, years later. The rest — anything uploaded rather than fetched, anything generated, anything whose provider row predates that column — has no route back at all.
That 44.1% is what the rows describe, not a re-fetch anybody performed. We did not test whether a stock library still serves a given asset id three years later. The only honest planning assumption is that some of them will not.
Backing up the part that is not in the backup
The fix is not clever, which is why it is so often skipped — there is nothing to design, so nothing prompts anyone to decide it:
docker run --rm \
-v your_volume:/data:ro \
-v "$PWD":/out \
alpine tar czf /out/media.tgz -C /data .
Take it in the same run as the database dump, so the two are close in time. They will never be a consistent pair — a file written between the two is in one and not the other — and for media that is nearly always fine, because an image referenced by a row that does not exist yet is harmless while the reverse is not. Dump the database first, then the files. That ordering may leave you with spare files, but never with missing ones.
Then verify it the same way you verify the database: not by whether tar exited zero, but by counting.
tar tzf media.tgz | wc -l # against the file count on disk
The check that catches real drift is to take every path your rows reference and confirm it exists inside the archive. That is the set your site will visibly break without, and a much smaller group than the archive as a whole.
You do not have to wait for a restore to measure this
Every number above came from walking the database and the disk together — a check cheap enough to run on a schedule. It answers a question you otherwise only ask during an incident: do the files my rows promise exist?
for every path referenced by a row:
does the file exist?
for every file on disk:
does any row mention it?
The first loop finds the failure that shows up as broken images. On our system 695 paths are referenced and 682 resolve. The thirteen that do not are drift between two environments rather than loss, and the check flags the difference — far easier to read in a scheduled report than to discover mid-restore.
The second loop is the one that finds the 2,083. It will always return a large number, because thumbnails and derivatives legitimately have no row, so the useful signal is not the count but its trend — a directory that grows while your content does not is storing something nothing will ever ask for, and a directory that shrinks is losing something quietly.
Neither loop needs a backup, an outage or a maintenance window. They need a filesystem and a database, which is what you have on an ordinary Tuesday.
How this was measured, and what it leaves out
One live system: a 49.8 MB dump, a 66.3 MB media volume of 2,765 files, 695 referenced paths counted by walking the database and the disk together. Every figure is a count.
Media is the example, not the only category. The same reasoning applies to anything a row points at rather than contains: generated PDFs, uploaded attachments, search indexes, the queue, and any cache your application treats as authoritative rather than disposable. A search index rebuilds from the database and is safe to lose; a user's uploaded file is neither. The question to ask of each is not "is this backed up" but "if this were empty tomorrow, what would rebuild it, and from what".
We did not test the media restore. This measured what the database backup omits, not a rehearsal of putting the files back. That is the same mistake in a different place, and it deserves the same treatment as the database: restore it into something disposable and count what arrives.