How to Make Storage Persistent on Docker and Docker Compose with Container Volumes

This guide covers Docker volumes, bind mounts, and Docker Compose configurations for maintaining data across container restarts.

Publish date: 7/3/2025

Ever deployed a containerized database only to discover all your data vanished after a restart? You're not alone. One of the most common misconceptions about Docker is that containers can store data permanently by default. They can't, and that's actually by design.

Docker containers are ephemeral. When you stop and remove a container, everything inside it disappears into the digital void. This feature makes containers lightweight and portable, but it also means you need a strategy for data that should stick around.

Understanding Docker's ephemeral nature

Docker containers operate like temporary workspaces. Each container gets its own writable layer on top of the read-only image layers. When you write files inside a running container, they exist only in that writable layer. Remove the container, and that layer goes with it.

Think of it like writing notes on a whiteboard. The board (your container) is useful while you're using it, but once you erase it (remove the container), your notes are gone forever. This behavior is perfect for stateless applications, but problematic for databases, file uploads, or any data you want to keep.

Docker storage options explained

Docker provides three main ways to persist data beyond the container lifecycle: volumes, bind mounts, and tmpfs mounts. Each serves different purposes and comes with its own trade-offs.

Docker volumes

Volumes are Docker's preferred mechanism for persistent storage. Docker manages these storage areas completely, storing them in a specific directory on the host system (usually /var/lib/docker/volumes/ on Linux). You don't need to know the exact path because Docker handles all the details.

Creating a volume is straightforward:

docker volume create mydata

You can then mount this volume to a container:

docker run -v mydata:/app/data myimage

The beauty of volumes lies in their portability and ease of management. They work consistently across different operating systems and can be easily shared between containers.

Bind mounts

Bind mounts connect a specific directory on your host system directly to a container. Unlike volumes, you have full control over the exact location on the host filesystem.

docker run -v /home/user/project:/app myimage

Developers often prefer bind mounts during development because they can edit files on their host system and see changes immediately reflected in the container. However, bind mounts tie your container to the host's directory structure, making them less portable than volumes.

tmpfs mounts

The third option, tmpfs mounts, stores data in the host's memory rather than on disk. This option is perfect for sensitive information you don't want persisted or for improving performance with temporary data.

docker run --tmpfs /app/temp myimage

Implementing persistent storage in Docker

Let's walk through practical examples of implementing persistent storage. We'll start with a simple PostgreSQL database that needs to maintain its data.

Basic volume usage

First, create a named volume for your database:

docker volume create postgres_data

Now run PostgreSQL with the volume attached:

docker run -d \
  --name postgres_db \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -v postgres_data:/var/lib/postgresql/data \
  postgres:17

The PostgreSQL data now persists in the postgres_data volume. You can stop, remove, and recreate the container without losing your database.

Using bind mounts for development

During development, you might want to mount your application code into a container. Here's how to run a Node.js application with live code reloading:

docker run -d \
  --name node_app \
  -v $(pwd):/usr/src/app \
  -w /usr/src/app \
  node:22 npm start

This command mounts your current directory into the container, allowing you to edit files locally while the container runs your application.

Docker Compose volume configuration

Docker Compose simplifies multi-container applications and makes volume management more declarative. Instead of remembering complex command-line options, you define everything in a YAML file.

Named volumes in Docker Compose

Here's a complete example with a web application and database:

services:
  web:
    image: nginx:alpine
    volumes:
      - web_content:/usr/share/nginx/html
    ports:
      - "80:80"

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secretpassword
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  web_content:
  db_data:

This configuration creates two named volumes that persist independently of the containers. Running docker-compose down removes the containers but preserves the volumes.

Bind mounts in Docker Compose

For development environments, you might prefer bind mounts:

services:
  app:
    build: .
    volumes:
      - ./src:/app/src
      - ./public:/app/public
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development

This setup mounts local directories into the container, perfect for development workflows where you need immediate feedback on code changes.

Advanced volume configuration

Docker Compose supports more sophisticated volume configurations. You can specify read-only mounts, use different volume drivers, or set specific mount options:

services:
  app:
    image: myapp:latest
    volumes:
      - type: volume
        source: app_data
        target: /data
      - type: bind
        source: ./config
        target: /app/config
        read_only: true
      - type: tmpfs
        target: /app/temp
        tmpfs:
          size: 100m

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /mnt/storage/app_data

Best practices for Docker volumes

Managing Docker volumes effectively requires following some established patterns. These practices help avoid common pitfalls and keep your data safe.

Volume naming conventions

Always use descriptive names for your volumes. Instead of data or volume1, use names like wordpress_uploads or postgres_13_data. This naming strategy helps you identify volumes months later when you've forgotten what they contain.

Backup strategies

Regular backups are crucial for any persistent data. Here's a simple backup approach using a temporary container:

docker run --rm \
  -v postgres_data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/postgres_backup_$(date +%Y%m%d).tar.gz -C /source .

This command creates a compressed backup of your volume contents in the current directory.

Volume lifecycle management

Unused volumes can accumulate over time, consuming disk space. Periodically clean up with:

docker volume prune

Security considerations

Volumes inherit the permissions of the first container that mounts them. Set proper ownership and permissions to prevent security issues:

FROM node:18
RUN mkdir -p /app/data && chown node:node /app/data
USER node
VOLUME ["/app/data"]

Troubleshooting common volume issues

Even with careful planning, you might encounter volume-related problems. Here are solutions to frequent issues.

Permission problems

If your application can't write to a mounted volume, check the user ID mismatch between the container and host:

docker exec container_name ls -la /path/to/volume

You might need to adjust ownership or run the container with specific user IDs:

docker run --user $(id -u):$(id -g) -v ./data:/data myimage

Volume not persisting data

Make sure you're mounting to the correct path inside the container. Different images store data in different locations.

Performance issues with bind mounts

On macOS and Windows, bind mounts can be slow due to filesystem translation overhead. Consider using named volumes for better performance, or explore Docker's cached and delegated mount options:

volumes:
  - ./src:/app/src:cached

Migrating volumes between hosts

Sometimes you need to move Docker volumes to a different server, which isn't a problem. The process is pretty straightforward.

Export the volume:

docker run --rm -v myvolume:/source:ro -v $(pwd):/backup alpine \
  tar czf /backup/myvolume.tar.gz -C /source .

Copy the archive to the new host, then import:

docker volume create myvolume
docker run --rm -v myvolume:/target -v $(pwd):/backup alpine \
  tar xzf /backup/myvolume.tar.gz -C /target

Conclusion

Docker's ephemeral nature doesn't mean you can't have persistent storage. Volumes and bind mounts are all ways to keep data safe across container lifecycles.

Start with named volumes for most use cases. They're portable, easy to manage, and work consistently across platforms. Save bind mounts for development scenarios where you need direct filesystem access. And remember to implement regular backups regardless of which approach you choose.

Thanks for reading! If you're looking for reliable infrastructure to run your containerized applications, xTom offers a wide range of solutions. From dedicated servers for maximum performance to colocation options for your existing hardware, and beyond. For scalable container hosting, our sister brand V.PS provides NVMe-powered KVM VPS hosting perfect for Docker deployments of any size.

Frequently asked questions about persistent Docker storage

What happens to Docker volumes when I remove a container?

Named volumes persist independently of containers. When you remove a container, its associated named volumes remain intact unless you explicitly delete them with docker volume rm. Anonymous volumes (created without names) are removed with the container if you use the --rm flag or docker container prune.

Can I share volumes between multiple containers?

Yes, multiple containers can mount the same volume simultaneously. This is useful for scenarios like shared file storage or when using sidecar containers. However, ensure your applications handle concurrent access appropriately to avoid data corruption.

How do I backup Docker volumes to cloud storage?

You can create automated backup scripts that compress volume data and upload to cloud storage. Here's a basic example for AWS S3:

docker run --rm \
  -v myvolume:/source:ro \
  -v ~/.aws:/root/.aws:ro \
  amazon/aws-cli s3 cp - s3://mybucket/backup.tar.gz \
  --expected-size $(docker run --rm -v myvolume:/source:ro alpine du -sb /source | cut -f1) \
  < <(docker run --rm -v myvolume:/source:ro alpine tar czf - -C /source .)

What's the difference between VOLUME instruction in Dockerfile and -v flag?

The VOLUME instruction in a Dockerfile creates a mount point and marks it as externally mounted. It ensures data persists but creates anonymous volumes by default. The -v flag at runtime lets you specify named volumes or bind mounts, giving you more control over where data is stored.

How much disk space do Docker volumes use?

Docker volumes use actual disk space for stored data plus minimal overhead for metadata. You can check volume sizes with docker system df -v. Remember that volumes grow as data is added, so monitor disk usage regularly in production environments.