Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Docker Volumes and Persistent Data

By Kokil Thapa | Last reviewed: September 2026

Restart a Docker container and every file inside its writable layer disappears. That is fine for stateless PHP-FPM workers or Nginx fronts. It fails fast when MySQL, Redis, or Laravel storage holds uploads you cannot lose. Docker volumes and persistent data solve exactly that gap. They keep database files, logs, and user assets on the host or a remote driver while containers stay replaceable. Our Docker networking and volumes overview covers how containers share networks but not disks by default. This guide goes deeper on storage types, Compose patterns, backups, and the production mistakes I see on real deployments.

What are Docker volumes and why do containers need persistent data?

Containers are designed to be ephemeral. Their union filesystem stacks a read-only image layer with a thin writable layer. Anything written there vanishes when you run docker rm or replace the container during a deploy. For a local Laravel dev stack with Sail and Docker, that means your MySQL tables, Redis keys, and storage/app uploads would reset on every rebuild unless you mount persistent storage.

Docker gives you three mount types. Each solves a different problem. Picking the wrong one is a common source of data loss and permission headaches on production servers.

Docker Volumes and Persistent DataContainerWritable layerLost on removeNamed VolumeManaged by DockerSurvives rebuildMySQL Data/var/lib/mysqlRedis AOFAppend-only fileLaravel StorageUser uploadsMount point connects container path to durable storage
Docker volumes and persistent data keep state outside the ephemeral container writable layer

On a production Laravel application I maintain, the pattern is predictable. The app container is disposable. MySQL, Redis, and the storage directory live on named volumes or bind mounts. Deployments swap the app image without touching customer data. That separation is the core reason teams adopt containers without giving up durability.

When ephemeral storage is enough

Stateless services do not need volumes. Nginx serving static assets from the image, a queue worker with no local cache, or a one-off Artisan command container can run without mounts. If losing the filesystem on restart causes zero business impact, skip the volume and keep ops simpler.

What is the difference between bind mounts, tmpfs mounts, and Docker volumes?

All three attach host storage to a container path. They differ in who manages the path, performance traits, and portability across machines. The official Docker storage documentation defines each type clearly. In practice, most teams use named volumes for databases and bind mounts for developer source code.

Mount typeManaged byBest forSurvives container removal
Named volumeDocker engineMySQL, PostgreSQL, Redis, app uploadsYes, until docker volume rm
Bind mountYou (host path)Live code sync in dev, config filesYes (host files remain)
tmpfs mountKernel memorySecrets, temp cache, sensitive scratch dataNo (RAM only)
Three Docker Storage TypesNamed Volume/var/lib/docker/volumesProduction DB dataBind MountHost path you chooseDev source codetmpfs MountRAM filesystemShort-lived secretsDecision ruleNeed data after container delete? Use volume or bindNeed zero disk trace? Use tmpfs
Choosing between named volumes, bind mounts, and tmpfs for Docker persistent data

Named volumes

Docker creates named volumes under its own directory, usually /var/lib/docker/volumes/ on Linux. You reference them by name, not by absolute host path. That makes them portable across hosts when you use volume drivers or backup/restore workflows. For running PostgreSQL in Docker for development, a named volume is the right default.

Bind mounts

Bind mounts map a specific host directory into the container. Your Laravel project folder mounted at /var/www/html is the classic dev setup. Edits on the host appear instantly inside the container. On production, bind mounts tie you to a fixed server path. That can complicate migration unless you document paths carefully.

tmpfs mounts

tmpfs stores data in memory. Nothing hits disk. Use it for session scratch space or credentials you do not want written to a block device. Data disappears when the container stops. It is not a substitute for Redis caching and data structures that need durability across restarts.

How do you create and manage Docker volumes from the CLI?

The CLI is straightforward once you know the lifecycle commands. These work on any Linux host where you followed our Install Docker on Ubuntu guide.

Create and inspect a volume

docker volume create db_data

docker volume ls

docker volume inspect db_data

The inspect output shows Mountpoint on the host. That path is managed by Docker. Do not edit files there while the database container is running unless you know the engine tolerates it.

Run a container with a named volume

docker run -d \
  --name mysql_app \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=laravel \
  -v db_data:/var/lib/mysql \
  mysql:9.7

The syntax is -v VOLUME_NAME:CONTAINER_PATH. The left side is the volume. The right side is where the application expects files inside the container. MySQL 9.7 is current; MySQL 8.4 LTS remains common on managed hosting if you need wider compatibility.

Mount a single host file

Sometimes you need one config file, not a whole directory. Docker supports bind-mounting a file directly:

docker run -d \
  --name nginx_app \
  -v /etc/letsencrypt:/etc/letsencrypt:ro \
  -v ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro \
  nginx:alpine

The :ro flag makes the mount read-only. Use it for TLS certificates and static configs the container must not modify.

Remove volumes safely

docker stop mysql_app
docker rm mysql_app
docker volume rm db_data

Never run docker volume prune on a production host without checking the list first. That command deletes every unused volume. I have seen staging databases vanish because someone ran prune after a failed deploy left volumes detached but still needed.

  1. Create the volume before first container start, or let Compose create it automatically.
  2. Mount at the path the application expects, not a convenience path you invent.
  3. Label volumes in Compose with clear names like app_mysql_data.
  4. Back up before upgrades that change major database versions.
  5. Document which volumes belong to which service in your runbook.

How do you configure Docker volumes in Docker Compose?

Most Laravel and WordPress stacks I ship use Compose for local dev and small production setups. Compose declares volumes at the top level and references them in each service. See also our Docker Compose multi-container apps walkthrough for full stack wiring.

services:
  app:
    build: .
    volumes:
      - app_storage:/var/www/html/storage/app
      - ./:/var/www/html:cached
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:9.7
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:8.10
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:
  app_storage:

The top-level volumes: block tells Compose to create named volumes. Compose prefixes them with the project name by default. A project called booking yields a volume like booking_mysql_data. Override the name when you need stable identifiers across environments.

Docker Compose Volume Flowcompose.ymlvolumes blockNamed Volumesmysql_data redis_dataContainersapp mysql redisMount mapping examplesmysql_data → /var/lib/mysqlredis_data → /data./project → /var/www/html (bind mount)
Docker Compose declares volumes once and attaches them to multiple services for persistent data

External volumes for shared data

When two Compose projects must share a database volume, mark the volume as external:

volumes:
  shared_uploads:
    external: true
    name: production_app_storage

Create the external volume once with docker volume create production_app_storage. Both stacks can then mount it. This pattern appears on trek booking platforms with Laravel and Livewire where a separate worker container processes uploads.

Override files for dev versus prod

Keep production bind mounts out of the base Compose file. Use an override for local development only. Our Docker Compose for local Laravel dev article shows the split. Production should mount named volumes, not your laptop source tree.

How do you backup and restore Docker volume data?

Containers are easy to recreate. Volume data is not. Backup strategy belongs in the same checklist as SSL renewal and database dumps. The Docker volume backup guide uses a temporary container to stream a tarball. That approach works on every Linux host without extra tools.

Backup a named volume

docker run --rm \
  -v mysql_data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/mysql_data_$(date +%F).tar.gz -C /source .

Store the tarball off the server. S3, Backblaze, or a second VPS is fine. A backup on the same disk as the volume protects against container mistakes, not disk failure.

Restore into a new volume

docker volume create mysql_data_restored

docker run --rm \
  -v mysql_data_restored:/target \
  -v $(pwd):/backup \
  alpine sh -c "cd /target && tar xzf /backup/mysql_data_2026-09-01.tar.gz"

Point the database container at mysql_data_restored and verify tables before cutting traffic over. For logical backups, mysqldump or pg_dump inside the running container is often safer across version upgrades.

Volume Backup and RestoreLive Volumemysql_dataTemp Containeralpine + tarTar ArchiveOff-site copyRestore pathCreate new volume → extract tarball → swap mountTest before production cutover
Backup Docker volumes with a throwaway container and restore to a fresh volume for safe migrations

Schedule nightly backups with cron on the host or a CI job in GitLab. Pair volume tarballs with logical dumps for MySQL 9.7 or PostgreSQL 18. Test restores quarterly. An untested backup is wishful thinking.

What are common Docker volume mistakes in production?

Most volume disasters are operational, not Docker bugs. Permissions, prune commands, and silent empty mounts cause more downtime than engine failures.

UID and GID mismatches

Laravel expects www-data to write to storage/. A bind mount owned by root on the host blocks uploads. Fix ownership on the host or set a user in the Dockerfile. On Ubuntu 22/24 servers I administer, aligning UID 33 inside and outside the container resolves most upload failures.

Anonymous volumes shadowing your data

Declaring -v /var/lib/mysql without a name creates an anonymous volume. It persists, but the random ID makes ops painful. Always use named volumes in production Compose files. Anonymous volumes also appear when an image declares a VOLUME instruction and you omit an explicit mount. Docker creates hidden storage you may not know exists.

Empty bind mount hiding image content

Mounting an empty host directory over /var/www/html/public hides built assets from the image. The site loads with no CSS. Mount only directories that need persistence or live sync. Keep compiled Vite 8.x assets inside the image for production.

Forgetting volume scope during migration

Moving containers to a new VPS without copying volumes gives you a fresh empty database. Plan volume export before DNS changes. Our website migration service always includes a storage audit for containerised apps. Treat volumes as first-class migration assets alongside DNS and SSL.

  • Run docker system df -v monthly to see volume disk usage.
  • Set log rotation on apps writing logs to bind mounts.
  • Use read-only root filesystem plus explicit writable volumes for tighter security.
  • Pin image tags; do not pull latest on production database containers.
  • Keep Compose project names stable so volume names do not drift.

For reverse-proxy setups, Traefik or Nginx configs often use bind mounts for certificates. Our Traefik reverse proxy for Docker guide covers read-only cert mounts. Pair that with named volumes for application state.

Resource limits matter too. A runaway log volume can fill the disk and crash MySQL. See limit Docker container resources for cgroup settings. Monitoring disk at the host level catches volume growth before services fail.

Teams building on Laravel 13.x with PHP 8.3 or PHP 8.5 should treat storage and bootstrap/cache as persistent or image-baked paths. Never assume a rebuilt container preserves locally generated keys. Mount storage or inject secrets via environment variables and CI.

If you outgrow single-host volumes, Kubernetes persistent volumes are the next step. Our Kubernetes persistent volumes and storage article covers CSI drivers and reclaim policies. Docker volumes teach the same concepts at smaller scale.

For JSON config stored in volumes, validate files after deploy with a JSON formatter tool before restarting dependent services. A trailing comma in a mounted config has taken down more staging stacks than any Docker upgrade.

Production support often means fixing volume permissions at 2 a.m. after a client ran the wrong command. Document volume names, backup paths, and restore steps in your internal wiki. Good support and maintenance processes prevent repeat incidents.

On booking systems like those built for grocery eCommerce with delivery zones, order history lives in MySQL volumes. Cart sessions may use Redis. Losing either volume during a deploy without backup is a business outage, not a dev inconvenience.

Compare this approach with traditional Deployer 7 symlink releases on bare metal. Both separate code from data. Containers just make the boundary explicit through mount declarations. Many sister sites I maintain still use symlinked releases while newer stacks use Compose on a single EC2 instance.

Whether you choose Docker or systemd plus PHP-FPM, the rule holds. Code is replaceable. Data is not. Volumes are how you enforce that boundary in a container world.

Key Takeaways

  • Use named Docker volumes for databases, Redis AOF files, and Laravel storage that must survive container replacement.
  • Reserve bind mounts for development source sync and read-only config; avoid empty host dirs shadowing image assets in production.
  • Declare volumes explicitly in Compose with stable names and document them before running prune or migration scripts.
  • Back up volumes with a temporary Alpine container and tar archive, then store copies off the host and test restores regularly.
  • Fix UID/GID ownership on mounted paths and monitor disk usage with docker system df -v to prevent silent fill-ups.
  • Treat Docker volumes and persistent data as migration assets equal to DNS and SSL when moving between servers.

People Also Ask

Do Docker volumes persist after the container is deleted?

Yes. Named volumes and bind mounts survive docker rm. Only the container writable layer is destroyed. You must run docker volume rm or docker volume prune to delete a named volume. That is why prune commands are dangerous on production hosts with detached but important volumes.

What is the difference between a Docker volume and a bind mount?

A Docker volume is managed by the engine and lives under Docker storage paths. A bind mount points to any host directory you specify. Volumes port cleanly across hosts with backup tools. Bind mounts give direct filesystem access, which helps local development but couples production to specific server paths.

Can multiple containers share the same Docker volume?

Yes, if the workload supports shared file access. Multiple read-only mounts are safe for static assets. Multiple writers to the same database data directory will corrupt MySQL or PostgreSQL. Share volumes for uploads only when the application coordinates file locks or uses object storage instead.

How do I see how much disk space Docker volumes use?

Run docker system df -v for per-volume size breakdown. On the host, du -sh /var/lib/docker/volumes/ shows total footprint. Set alerts when volume growth exceeds expected rates, especially for log and upload directories on small VPS plans common in Nepal hosting at Rs 1,500–3,000/month (~USD 11–22).

Ship durable container stacks with confidence

Docker volumes and persistent data are not optional extras for stateful apps. They are the contract that lets you rebuild containers daily without losing customer records, uploaded documents, or payment logs. Start with named volumes in Compose, add tested backups, and document every mount before your first production deploy. Need help containerising a Laravel app, migrating existing data, or hardening a Compose stack on Ubuntu? Contact us or explore our Linux system administration service and custom software development options. You can also browse the portfolio for production apps that rely on persistent storage every day.

Frequently Asked Questions

Yes. Named Docker volumes survive container removal until you explicitly delete them with docker volume rm. Container writable layers are ephemeral; volumes keep MySQL tables, Redis AOF files, and Laravel storage/app uploads on the host outside the disposable container filesystem.

All three attach host storage to a container path but differ in management and durability. Named volumes are Docker-managed under paths like /var/lib/docker/volumes/ and suit databases and uploads. Bind mounts map a specific host directory you control, common for dev code sync and read-only configs. tmpfs stores data in RAM only; it disappears when the container stops and suits scratch or sensitive temp data, not durable caches.

Containers stack a read-only image with a thin writable layer. Anything written there vanishes on docker rm or image replacement during deploy. Without volumes, a Laravel dev stack loses MySQL tables, Redis keys, and storage/app uploads on every rebuild. Volumes keep state outside the ephemeral layer so the app container stays replaceable while customer data survives.

Run docker volume create db_data, then docker volume ls to list volumes and docker volume inspect db_data to see details including the host Mountpoint. Create the volume before the first container start, or let Docker Compose create it automatically. Mount with -v VOLUME_NAME:CONTAINER_PATH, for example db_data:/var/lib/mysql for MySQL 9.7.

Skip volumes when losing the filesystem causes zero business impact. Stateless services like Nginx serving static assets from the image, queue workers with no local cache, or one-off Artisan command containers do not need mounts. If restart data loss is acceptable, keeping ops simpler without volumes is the right call.

Declare volumes in a top-level volumes block and reference them in each service. A typical pattern mounts app_storage at /var/www/html/storage/app, mysql_data at /var/lib/mysql for MySQL 9.7, and redis_data at /data for Redis 8.10 with appendonly enabled. Compose prefixes volume names with the project name by default, so label them clearly like mysql_data in your runbook.

Use a temporary Alpine container to stream a tarball without extra host tools. Run docker run --rm with the volume mounted read-only at /source and the host backup directory at /backup, then tar czf to create an archive like mysql_data_2026-09-01.tar.gz. Store copies off the server on S3, Backblaze, or a second VPS. A tarball on the same disk protects against container mistakes, not disk failure.

Create a fresh volume with docker volume create mysql_data_restored, then run a throwaway Alpine container mounting that volume at /target and your backup directory at /backup. Extract the tarball into /target with tar xzf. Point the database container at the restored volume, verify tables, then cut traffic over. For major version upgrades, logical dumps with mysqldump or pg_dump inside the running container are often safer than raw file restore.

Yes, using external volumes. Mark the volume as external: true in Compose and set an explicit name like production_app_storage. Create it once with docker volume create production_app_storage before either stack starts. Both projects can then mount the same named volume, useful when a separate worker container processes uploads from a Laravel app on another Compose project.

docker volume prune deletes every unused volume on the host. On production, someone running prune after a failed deploy can wipe staging or detached databases still needed for recovery. Always run docker volume ls and confirm which volumes belong to which service before pruning. Document volume names in your runbook and never prune blindly on a live host.

Declaring -v /var/lib/mysql without a name creates an anonymous volume with a random ID. It persists but makes operations painful because you cannot identify it easily. Anonymous volumes also appear when an image declares a VOLUME instruction and you omit an explicit mount. Always use named volumes in production Compose files with clear labels like app_mysql_data.

Laravel expects www-data to write to storage/. A bind mount owned by root on the host blocks uploads even when the container runs correctly. On Ubuntu 22/24 servers, aligning UID 33 inside and outside the container resolves most failures. Fix ownership on the host or set the user in the Dockerfile rather than chmod 777, which creates security problems on production.

Mounting an empty host directory over a container path like /var/www/html/public replaces image content with the empty host folder. The site loads with no CSS because compiled assets inside the image are hidden. Mount only directories that need persistence or live sync. Keep compiled Vite 8.x assets inside the image for production rather than bind-mounting over public paths.

Docker creates named volumes under its own directory, usually /var/lib/docker/volumes/ on Linux. You reference them by name in Compose or docker run, not by absolute host path. The docker volume inspect command shows the exact Mountpoint. Do not edit files there while the database container is running unless you know the engine tolerates concurrent access.

Moving containers to a new VPS without copying volumes gives you a fresh empty database despite identical Compose files. Plan volume export before DNS changes and treat volumes as first-class migration assets alongside DNS and SSL. Run docker system df -v monthly to monitor disk usage, test restores quarterly, and document backup paths. An untested backup is wishful thinking on booking or eCommerce stacks where order history lives in MySQL volumes.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: