Stop Losing Data When Your Container Restarts

by Arif Ikhsanudin, Backend Developer

The upload folder that empties itself

Your application lets users upload profile photos. They're stored at /app/uploads inside the container. Users upload photos, they appear immediately, everything looks fine. Then the container restarts due to an OOM event, a deploy, or a node recycle. Every uploaded photo is gone. Users see broken images. The data was never persisted outside the container filesystem.

This is the most common data loss pattern in containerized applications, and it happens because "the container is running" feels like a stable state. It isn't. A container can be stopped and restarted at any time, and without volumes, anything written to the container filesystem during runtime is lost.

What actually persists across a container restart vs removal

There's an important distinction between restart and removal:

Container restart (docker restart, OOM restart, restart: unless-stopped policy): The container process restarts but the container's writable layer is preserved. Data written to the container filesystem survives a restart. This is why you might not notice the problem immediately — restarts look fine, but a container removal and recreation wipes everything.

Container removal and recreation (docker compose down && docker compose up, a deploy that replaces the container, a Kubernetes pod being rescheduled to a different node): The old container is gone, a new container is created from the image. The new container starts fresh — the writable layer from the old container is discarded.

In Kubernetes, pods are ephemeral by definition. A node going down, a node being drained for maintenance, a deployment rollout — all of these create new pods. If your application relies on the container filesystem for persistent data, it will lose data when pods are rescheduled.

Find every place your app writes to the filesystem

Before adding volumes, audit where your application writes data:

Log files: Does your application write log files anywhere in the container? Many frameworks default to writing logs to a file in the working directory or /var/log/. In containers, logs should go to stdout/stderr — let the container runtime collect them.

Uploaded files: User uploads, generated files, exports. These must be on a volume or external storage (S3, GCS, Azure Blob).

Caches: Application caches written to disk. If they're safe to recreate, this isn't a data loss concern — but a large cache rebuild on every pod start can cause performance issues. Consider whether the cache is better stored in Redis.

SQLite or embedded databases: If your application uses SQLite or an embedded H2 database for anything other than ephemeral test data, put the database file on a volume.

JVM artifacts: Thread dumps, heap dumps (-XX:+HeapDumpOnOutOfMemoryError defaults to writing in the working directory), GC logs. These are diagnostic, not business data, but losing them makes production incident investigation harder. Mount a volume for them.

Session data: Application sessions stored to disk. Use Redis or a database for session storage in containerized applications.

# Quick audit: what does your application write at runtime?
docker exec -it your-container inotifywait -m -r /app 2>/dev/null &
# Exercise the application for a few minutes, then check output

Mounting volumes for common write paths

User uploads:

services:
  app:
    volumes:
      - uploads:/app/uploads

volumes:
  uploads:

For production, prefer external object storage over a local volume. Local volumes don't survive node failures and can't be shared between multiple replicas. Use S3/GCS/Azure Blob and generate pre-signed URLs for uploads and downloads — your container becomes stateless.

Log files (when you can't switch to stdout):

services:
  app:
    volumes:
      - app_logs:/app/logs

volumes:
  app_logs:

Configure a log rotation mechanism alongside this, or your logs will fill the disk. Docker has built-in log rotation for stdout/stderr logs (--log-opt max-size=10m --log-opt max-file=3) — another reason to prefer stdout.

JVM diagnostic output:

services:
  app:
    environment:
      JAVA_OPTS: >-
        -XX:+HeapDumpOnOutOfMemoryError
        -XX:HeapDumpPath=/app/dumps
        -Xlog:gc*:file=/app/gc.log:time:filecount=5,filesize=20m
    volumes:
      - jvm_dumps:/app/dumps

volumes:
  jvm_dumps:

SQLite database:

services:
  app:
    volumes:
      - db_data:/app/data

volumes:
  db_data:

The read-only filesystem test

If you can run your container with a read-only filesystem, you've confirmed that no data is being written to paths that would be lost on recreation:

docker run --read-only --tmpfs /tmp your-image:tag

--tmpfs /tmp provides a writable in-memory tmpfs for /tmp, which is typically needed for application temp files. If the container starts successfully and operates correctly under --read-only, your persistent data is all on volumes.

In Kubernetes:

securityContext:
  readOnlyRootFilesystem: true
volumeMounts:
  - name: tmp
    mountPath: /tmp
  - name: uploads
    mountPath: /app/uploads
volumes:
  - name: tmp
    emptyDir: {}
  - name: uploads
    persistentVolumeClaim:
      claimName: uploads-pvc

Making the root filesystem read-only is also a security hardening measure — it prevents an attacker with code execution from writing malicious files to the container filesystem.

Backup strategy for volumes

A named volume that isn't backed up is not persistent storage — it's resilient-until-it-isn't storage. For production data:

#!/bin/bash
# Simple volume backup script
VOLUME_NAME="pg_data"
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

docker run --rm \
  -v "${VOLUME_NAME}:/source:ro" \
  -v "${BACKUP_DIR}:/backup" \
  alpine \
  tar czf "/backup/${VOLUME_NAME}_${TIMESTAMP}.tar.gz" -C /source .

# Keep only last 7 backups
ls -t "${BACKUP_DIR}/${VOLUME_NAME}_"*.tar.gz | tail -n +8 | xargs -r rm

For PostgreSQL specifically, pg_dump inside the running container is better than a raw filesystem backup — it produces a consistent snapshot without requiring the database to be stopped:

docker exec postgres-container \
  pg_dump -U appuser mydb | gzip > "backup_$(date +%Y%m%d).sql.gz"

The operational checklist

For every service in your Compose or Kubernetes config, answer these questions:

  1. Does this service write files to the container filesystem at runtime?
  2. If yes, is that write path mounted to a named volume or external storage?
  3. If the container is removed and recreated right now, what data is lost?
  4. Is that data backed up?

Most teams find at least one answer that surprises them. Fix it before the next unplanned container restart finds it for you.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

The Best Architecture Decision Is the One You Can Explain to Your Team

An architecture that requires deep expertise to understand is an architecture that only one person can safely modify. Explainability is not a soft requirement — it is a hard constraint on how well a system can be maintained.

Read more

Supercell and Nokia Pay Nordic Rates — Helsinki Startups Cannot Compete on Salary Alone

Helsinki's anchor employers have set a compensation floor that most startups can't match. The teams still shipping have stopped trying to win on that ground.

Read more

Designing Thread-Safe Classes in Java — Confinement, Immutability, and Synchronization

Thread safety is not a property you add after the fact — it is a design decision made at the class level. Three strategies cover nearly every case: confinement, immutability, and synchronization. Here is how to reason about which applies and how to apply it correctly.

Read more

How to Identify Risky Software Projects Before You Start

Not all software projects are worth pursuing. Some are risky from the start, and spotting the danger early can save time, money, and frustration.

Read more