How to Back Up and Restore Uploaded Files, Not Just Your Database | Deployxa

Your rehearsed database restore won't bring back invoice PDFs or profile photos. Map every file your SaaS writes, move uploads to object storage, and drill a restore before customers find out.

← Back to Dispatch Articles
Engineering Log

How to Back Up and Restore Uploaded Files, Not Just Your Database

Your rehearsed database restore won't bring back invoice PDFs or profile photos. Map every file your SaaS writes, move uploads to object storage, and drill a restore before customers find out.

Picture the drill you already ran: database backup restored into a scratch instance, rows spot-checked, laptop closed, homework done. Now picture the Tuesday that follows. A bad deploy or a storage incident takes the app down, the database returns clean from its automated backup, and customers report something your runbook has no answer for: their invoice PDFs are gone. So are the profile photos, the signed contracts, last quarter's exports. The restore you rehearsed covers only the database — because that is the only thing you taught it to cover.

This blind spot is structural, not careless. The database is one obvious service with one obvious backup story, and most managed platforms make it automatic. User files are the opposite: scattered across a dozen write paths, written by different features at different times, with no assigned owner. If you want to back up uploaded files, SaaS object storage is the standard answer — but only once you know what your app writes, where it lands today, and what losing it would cost.

This guide walks the path in order: where user files actually live and why the app container is the most fragile home, a file inventory exercise for every write path you own, a step-by-step migration to object storage, a backup strategy that survives accidents and mistakes, a file restore drill, the orphan problem between database and buckets, and the access-control and cost basics.

Where User Files Actually Live (and Why the Container Is the Worst Home)

In a small SaaS, user files typically live in one of three places: the app container's local disk (an /uploads directory the framework writes to), an attached volume mounted to one running instance, or object storage — a separate, API-accessed service (anything S3-compatible) where files live as objects in a bucket with no dependency on any single app process.

The first option is the most fragile, and it fails for a reason worth understanding: containers are replaceable by design. A container is an instance created from an image; everything written inside it at runtime lives in that instance's writable layer and nowhere else. Deploy a new version and the platform starts a fresh container from the image — the writable layer comes up empty. Restart, reschedule, scale down, crash: same outcome. Even a flawless blue/green release, where the new version runs in a standby slot until health checks pass, produces a container whose disk was born after your users' files were. The deploy button, pressed confidently, is the trigger.

Attached volumes are better but still fragile for a small team: usually a single copy tied to one machine in one zone, easy to forget during a region move, and awkward to share between instances. Object storage is the only home independent of your deploys, redundant by default, reachable from any instance, and supported by a mature library in every language. The rule is simple: anything a customer would miss does not belong on the container's disk.

The File Inventory Exercise: Map Every Write Path

Before you back anything up, know what exists. Set aside an hour, open the codebase, and list every place the application writes a file: upload handlers, PDF generators, export jobs, image processors, import parsers, temp files. For each, record four things: what it is, where it lands, whether it is needed after an incident, and the backup approach. A spreadsheet with these example rows is a fine start:

  • File type: Profile photos, avatars — Written where (typical): /uploads on the app container — Needed after an incident?: Yes — product data; making users re-upload is a trust hit — Backup approach: Private bucket, versioning on, synced to a second copy
  • File type: Invoice and contract PDFs — Written where (typical): Generated in a worker's local tmp, then "archived" on the same disk — Needed after an incident?: Yes — financial records customers rely on — Backup approach: Object storage from the moment of creation; lifecycle tiering after 90 days
  • File type: CSV and Excel exports — Written where (typical): Written on request, link emailed to the user — Needed after an incident?: Sometimes — keep 30–90 days, then expire — Backup approach: Object storage with lifecycle expiry instead of deletion code
  • File type: Imported spreadsheets — Written where (typical): /tmp inside the container, parsed then abandoned — Needed after an incident?: Rarely — the data lands in the database; the file is scratch — Backup approach: No backup; delete after processing; never retain raw imports longer than needed
  • File type: Thumbnails, resized images — Written where (typical): Generated cache next to the originals — Needed after an incident?: No — derivable from originals — Backup approach: Store originals; treat derivatives as a disposable cache

Two rules fall out of the exercise. Originals need backups; derivatives only need a way to regenerate; scratch files need deletion, not storage. The inventory is also a living document: every future feature that writes a file gets a row, because an unlisted write path is exactly the file you will miss during a restore. Keep the output to one page.

The Local-Disk Escape Plan: Move to Object Storage Step by Step

If your inventory shows files living on the container, the fix is a migration, and it is smaller than it feels. Do it in this order:

  1. Create a private bucket. Pick any S3-compatible provider, create one bucket, block public access. User files are customer data; a public bucket is a breach waiting for a crawler. Scope credentials to this bucket only.
  2. Add the SDK behind one module. Install the storage library for your language and wrap it in a single storage module — save, fetch, signed URL. Every future storage call goes through it, which keeps the provider swappable and gives logging one home.
  3. Change one write path and ship it. Start with the lowest-drama path (avatars are a good candidate), flip it to object storage, and verify in production before touching anything else.
  4. Update retrieval and URLs. Files in a private bucket are served through short-lived signed URLs, not copied into your web root. Short expiry, one URL per request, no directory listings.
  5. Bulk-copy the existing files. Run a sync tool from the local disk to the bucket, preserving paths. Verify: object count should match the database's file-row count, and a random sample should checksum clean. Run the sync again after cutover day to catch files written while the first copy ran.
  6. Switch reads, then delete the local writes. Point retrieval at the bucket, watch error rates for a few days, then remove the local write code entirely. If it stays, someone will eventually use it, and you will have two homes for files again.
  7. Update the inventory and the runbook. The row that said "app container disk" now says "object storage, bucket X." Future-you reads that during an incident.

Nothing here requires a maintenance window. The migration is code review work plus one patient copy job.

How to Back Up Uploaded Files with SaaS Object Storage

Moving to object storage gets your files out of the blast radius of deploys. It does not, by itself, back them up — a single bucket is storage, not a backup. The strategy has three layers.

Turn versioning on first. With versioning enabled, an overwrite or delete retains the previous version instead of destroying it. This is the cheapest protection available and the first thing to check on any bucket you own. Add lifecycle rules so versions do not accumulate forever: keep a few noncurrent versions for 30–60 days and expire incomplete uploads.

Keep a second copy in a different account. Schedule a sync from the live bucket to a backup bucket under a separate account — and a separate provider for anything you would struggle to reconstruct. The separation is the point: a leaked production key or a buggy script with delete permissions can destroy a live bucket and its same-account "backup" in one command. Two accounts force a mistake to be unlucky twice.

Know what does not count as a backup:

  • A single bucket with versioning off. An overwrite or delete is permanent — that is storage, not a backup.
  • Replication without versioning. Replication is excellent at copying changes — including deletes.
  • Provider durability claims. Durability describes the platform losing your object; it says nothing about your code deleting it.
  • Whole-server snapshots. They restore everything at once at the wrong granularity and typically exclude the volumes your files live on.

Scaled to a small SaaS, the classic 3-2-1 rule becomes: two copies, two accounts, one copy that production credentials cannot delete. No tape library required.

Rehearse the File Restore Before You Need It

A backup you have never restored is a rumor, and file backups deserve the same skepticism you already apply to database backups. Run the drill on a calm afternoon:

  1. Pick a sample. Fifty objects across every file type in your inventory, including the oldest and the newest. A sample exercises the path; a full restore exercises your patience.
  2. Restore into a scratch location. A new scratch bucket or a local directory — never over the live bucket. The drill must be incapable of making things worse.
  3. Verify integrity two ways. Compare checksums against a manifest to prove the copy matches the source, then open-test: actually render the PDF, load the image. Checksums prove fidelity; the open test proves usability, which is what the customer experiences.
  4. Measure the time. Extrapolate the sample to the full bucket. If a full restore takes a week, you want to know now — and maybe reconsider retention — rather than discover it mid-incident.
  5. Document one page. Trigger, commands, required credentials, verification steps, who executes. Re-run quarterly so the page never rots.

The integrity check looks like this:

# verify a restored sample against its manifest (fictional values)sha256sum -c restore-manifest.sample.txt# invoice_1042.pdf: OK# avatar_8813.png: OK# contract_acme_2026-03.pdf: FAILED checksum

That FAILED line, found during a rehearsal, is the drill doing its job — better here than in front of a customer asking for their contract. Before you close the laptop, confirm the drill's checklist: sample drawn from the inventory, scratch target, checksums compared, open-test performed, time measured, runbook updated.

The Orphan Problem: Keeping the Database and Files Consistent

Files and database rows drift apart in two directions, and both hurt.

A row without its file: the database says invoice 1042 exists, but the object is gone. The user clicks a link that worked yesterday and gets a 404 on a financial record. Causes: a manual deletion, a mis-scoped lifecycle rule, or restore skew — a database restored from a newer backup than the files, or the reverse.

A file without its row: an object sits in the bucket that no database row references. Causes: an upload whose transaction rolled back, an abandoned flow, or a delete path that removed the row but not the object. Orphans are a cost leak and a privacy liability — customer data with no owner, resurfacing in future restores after it should have been gone.

You cannot prevent every drift, but you can reconcile it:

  • Order the writes. Upload the object first, then insert the row; if the insert fails, delete the object or let a cleanup job catch it. Delete in row-then-object order, and if the object delete fails, queue it for retry rather than failing the request.
  • Run a reconciliation job. Weekly is plenty at small scale: list objects (or a sample), compare against the database in both directions. Orphaned objects move to a quarantine prefix for thirty days, then get deleted; rows pointing at missing files alert you — that is a bug to fix, not drift to paper over.
  • Expect skew after every restore. Any restore produces two points in time; note the skew in the runbook and let the reconciliation job find the seams.

Access Control: Backups Are Production Data

A backup bucket contains everything production contains — invoices, contracts, photos, whatever your customers uploaded — so it inherits production's security obligations:

  • Encrypt at rest. Provider-side encryption is a checkbox; turn it on for both buckets.
  • Give the backup job its own least-privilege credentials: read the live bucket, write the backup bucket, and — critically — no permission to delete from the backup bucket.
  • Restrict human access to the same bar as production database access. "Anyone on the team, because it's just backups" is how backups leak.
  • Keep credentials in your secrets manager, not in code, chat threads, or AI prompts.

Versioning, a separate account, and a deny-delete policy add up to a useful property: one compromised key cannot erase both copies of your data in one action. A backup nobody can delete with one command is a backup nobody can ransom with one command.

Cost Basics: Storage Classes and Lifecycle Rules

Object storage is cheap per gigabyte and bills for what you keep — live objects, retained versions, and the second copy. At small scale the totals are modest, but the habits are free:

  • Match the storage class to access patterns. Frequent-access for anything users request; infrequent-access tiers for objects untouched in 60–90 days, like last year's invoices and completed exports. Archive tiers restore with hours of delay — most small SaaS never needs them.
  • Let lifecycle rules do the moving. Automatic transitions after an age threshold, plus version expiration, mean old data gets cheaper without a cleanup sprint.
  • Watch transfer costs on cross-provider syncs. Negligible at a few gigabytes; worth measuring before you schedule a nightly terabyte.

The one place not to economize is the second copy — storage savings are the smallest numbers on any post-incident invoice.

Where Deployxa Fits — and What Stays Yours

There is a reason this article keeps circling container ephemerality: it is the normal behavior of modern deployment, not a misconfiguration. Deployxa deploys Git repositories or local projects as containerized applications — with automatic framework and runtime detection for common Node.js, Python, Go, PHP, Rust, and .NET projects — and those workloads are ephemeral by design: that is what makes deploys repeatable and rollback clean, and exactly why persistent user files belong in object storage rather than on the container's disk.

On the other half of the story, Deployxa's managed PostgreSQL and MySQL workflows include automated backups and restore for managed databases, and isolated tenant networks keep customer workloads separated. Current plans are on the pricing page, and the docs cover the deployment side.

The honest limits: no platform backs up your buckets for you. Choosing a storage provider, wiring the SDK, running the syncs, and rehearsing file restores remain the owner's job on every platform, including this one. What changes with a managed platform is that your deploys stop threatening the files — and the database half of the backup story runs on a schedule you did not have to build.

The File-Backup Checklist

Run this against your product today; every unchecked box is a to-do or a written decision to defer:

  • [ ] A one-page inventory lists every write path in the codebase
  • [ ] No customer-original file is written only to the app container's local disk
  • [ ] Uploads live in a private bucket with public access blocked
  • [ ] Retrieval uses short-lived signed URLs for anything user-sensitive
  • [ ] Versioning is on, with lifecycle rules capping retained versions
  • [ ] A second copy syncs to a different account, ideally a different provider
  • [ ] Backup credentials cannot delete from the backup bucket
  • [ ] A file restore has been rehearsed into a scratch location this quarter
  • [ ] The rehearsal included checksum verification and an open-test
  • [ ] The runbook documents time-to-restore and who executes it
  • [ ] A reconciliation job (even a manual sample) hunts orphans at least monthly
  • [ ] Old uploads transition to cheaper storage classes automatically

Run the Inventory Today, Move One Path This Week

Start with the smallest useful step. Today, spend one hour on the file inventory exercise and count the rows that say "app container disk." Then pick one — avatars for the easy win, invoices for the important one — and move that single upload path to object storage this week, following the migration order above. When it is live, treat the rest of the list the same way, turn on versioning, and put the file restore drill on the calendar. Your database backups cover the data you structured; this work covers the files your customers sent you — both halves have to exist before either counts as a backup.

Ready to deploy with Deployxa?

Deploy your apps globally with automatic SSL and AI diagnostics.

Start Free Now