Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Troubleshooting

Uploaded Images Disappear After Deploy: The Ephemeral Filesystem Problem

Sean

Platform Writer

Aug 27, 2026
8 min read

Files written to a container’s local filesystem are gone the next time the container is replaced. Every deploy replaces it. This is not a bug in your code or a fault of the platform - it is how the deployment model works.

Uploaded Images Disappear After Deploy: The Ephemeral Filesystem Problem

The symptom is unmistakable and alarming: uploads work perfectly, the images display, everything is fine for days, and then a deploy goes out and every uploaded file is a broken image. The database rows are still there, pointing at paths that no longer exist.

Table of contents

Why the files vanish

Modern deployment platforms run your application in a container built from an image. When you deploy, a new container is created from a new image and the old one is destroyed. The filesystem inside the old container goes with it.

This is deliberate and it is what makes deploys reliable. Every instance starts from an identical known state, no instance accumulates drift, and rolling back means running the previous image rather than undoing changes. The cost of that guarantee is that nothing written at runtime survives.

Deploys are not the only trigger. A container is also replaced when the platform restarts your service, when it moves to different hardware, when it scales out or in, and when it crashes and is rescheduled. Files can therefore disappear on a day when you did not deploy anything, which makes the problem look intermittent and random.

Horizontal scaling makes it visibly worse. With two instances behind a load balancer, an upload lands on the filesystem of whichever instance handled the request. Half of subsequent requests go to the other instance, which has never heard of that file. The symptom is images that work on refresh and break on the next refresh, which is a genuinely confusing thing to debug.

Confirming that is what is happening

A few checks turn the hypothesis into a certainty.

# Does the file exist where the database says it does?
ls -la /app/uploads/ 2>/dev/null || echo "directory missing"

# How long has this container been alive?
uptime

# What is the write path actually resolving to?
df -h /app/uploads

If the directory is missing entirely, or is empty while the database has hundreds of rows, that is the answer. If the container uptime is shorter than the age of the missing uploads, that is also the answer.

A quick empirical test: upload a file, confirm it displays, trigger a deploy or restart, and check again. If it is gone, the diagnosis is complete.

The tell in your code is a write to a relative or local path.

// Node - writes to the container filesystem
const dest = path.join(__dirname, 'uploads', filename)
fs.writeFileSync(dest, buffer)
# Django - MEDIA_ROOT on local disk
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'

Both of these are the default in their framework’s tutorial, which is why so many applications ship with them. They are correct for local development and wrong for a containerised deploy.

The fix: object storage

User-uploaded files belong in object storage, not on the application’s filesystem. The application receives the upload, streams it to the store, and saves the resulting key or URL in the database.

// Node with an S3-compatible client
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const s3 = new S3Client({
  region: process.env.S3_REGION,
  endpoint: process.env.S3_ENDPOINT, // set for non-AWS providers
})

async function storeUpload(file) {
  const key = `uploads/${crypto.randomUUID()}-${file.originalname}`
  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: file.buffer,
    ContentType: file.mimetype,
  }))
  return key // store this in the database, not a filesystem path
}

For Django, the equivalent is a storage backend that writes to object storage rather than local disk, configured once so that every model field using file storage benefits without code changes.

# settings.py
STORAGES = {
    "default": {"BACKEND": "storages.backends.s3.S3Storage"},
    "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}
AWS_STORAGE_BUCKET_NAME = os.environ["S3_BUCKET"]
AWS_S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT")

Store the key, not a full URL. URLs contain the bucket, region, and possibly a CDN hostname, all of which can change. A key plus configuration survives a migration between providers; a hardcoded URL in a hundred thousand rows does not.

For private files, generate a presigned URL at read time rather than making the bucket public. It expires, it is per-object, and it means the store is not an open directory.

Persistent disks, and when they are the right answer

Many platforms offer a persistent volume that survives redeploys. On RunxBuild, persistent storage attaches to a service. That is a legitimate option and it is worth being clear about when it fits.

It fits when the data is genuinely local to one process: a SQLite database, a search index, a cache directory that is expensive to rebuild, or a working area for a job that reads and writes intermediate files.

It fits less well for user uploads that are served to browsers, for two reasons. A volume normally attaches to one instance, so scaling horizontally reintroduces the problem of files existing on one instance and not another. And serving user files through your application means every image request occupies an application process that could be handling real work, when object storage plus a CDN does it better and cheaper.

The rough rule: if the files are read by your application code, a volume is reasonable. If the files are served to end users, object storage is the right answer.

Migrating what you still have

If uploads have been vanishing for a while, some files are simply gone - there is no recovery for data that was never stored anywhere durable. Recover what remains and fix forward.

  1. Get the current container’s files out first, before the next deploy destroys them. Copy them out of the running instance.
  2. Reconcile against the database. Find rows whose files no longer exist and decide whether to null them, mark them, or leave them.
  3. Upload the surviving files to object storage, keeping the same identifiers so existing rows can be updated predictably.
  4. Update the rows to store keys rather than filesystem paths.
  5. Change the write path so new uploads go to the store.
  6. Deploy, and verify by uploading something and redeploying immediately.
# Reconcile - which rows point at files that are gone
psql "$DATABASE_URL" -t -A -c \
  "SELECT id, image_path FROM products WHERE image_path IS NOT NULL" \
| while IFS='|' read -r id p; do
    [ -f "/app/$p" ] || echo "missing: id=$id path=$p"
  done

Do the reconciliation before the fix, not after. It gives you an honest count of what was lost, which is information you need if the files belonged to customers who will eventually ask.

Once uploads are in object storage, this class of problem is over. Deploys stop being events that risk data, containers become genuinely disposable, and scaling out no longer changes what any given request can see.

How this fits the rest of the stack

The version of this that stops recurring is one where the container holds no state at all: code from the repository, configuration from the platform, uploads in object storage, and records in a managed database. On RunxBuild a service deploys from a repo with a build log, a live route, runtime logs, and rollback, with persistent storage available when a volume genuinely is the right shape. The RunxBuild hosting calculator shows the service, the managed database, and storage as separate line items.

Useful related references:

FAQ

Why do my uploaded images disappear after deploying?

Because files written to a container’s filesystem are destroyed when the container is replaced, and every deploy replaces it. Restarts, scaling events, hardware moves, and crashes do the same, which is why files sometimes vanish on days you did not deploy.

Where should user uploads be stored instead?

In object storage. Your application streams the upload to the store and saves the resulting key in the database. The store is independent of any container, survives every deploy, and is shared across all instances of your service.

Why do images work on some page loads and not others?

You are running more than one instance. The upload landed on the filesystem of whichever instance handled that request, and requests routed to the other instance find nothing. It looks intermittent because the load balancer alternates between them.

Is a persistent volume a good fix for user uploads?

Usually not. Volumes normally attach to a single instance, so scaling out reintroduces the same problem, and serving user files through your application ties up processes that could handle real requests. Volumes suit data your code reads - a SQLite file, a search index, a cache.

Should I store the full URL or the object key in my database?

The key. URLs embed the bucket, region, and possibly a CDN hostname, all of which can change. A key plus configuration survives a provider migration; a hardcoded URL repeated across many rows becomes a data migration.

#uploaded images disappear after deploy#ephemeral filesystem#object storage#file uploads#paas