Volumes vs Bind Mounts: The Battle for Persistence

Containers are ephemeral by design. When a container dies, its read-write layer dies with it. To build stateful applications (Databases, Queues), we must punch a hole through the container isolation to reach the host's durable storage.

1. The Ephemeral Trap

By default, all files created inside a container are stored on a writable container layer. This has two critical flaws:

  1. Data Loss: When the container is removed (docker rm), that layer is deleted.
  2. Performance: Writing to the container layer requires a Storage Driver (like Overlay2), which uses Copy-on-Write (CoW). This is significantly slower than native disk writes.

[!IMPORTANT] Production Rule: Never store database data in the container’s writable layer. Always use a Volume.


A Bind Mount maps a file or directory on the host machine to a file or directory in the container.

The Mechanism

It is a direct mapping at the kernel VFS (Virtual File System) level.

  • Linux: It uses the mount --bind syscall. The container and host share the exact same inode.
  • Mac/Windows: Since Docker runs in a VM, files must be synchronized across the OS boundary. This uses gRPC FUSE or VirtioFS.

When to use

  • Development: Mapping source code (./src:/app/src) for Hot Reloading.
  • Configuration: Injecting nginx.conf from the host.

The “VirtioFS” Reality Check (Mac/Windows)

On Linux, bind mounts are native speed. On Docker Desktop (Mac/Windows), they have historically been slow because every filesystem operation had to cross the VM boundary. VirtioFS (enabled in newer Docker Desktop versions) solves this by sharing memory between the Host and the VM, drastically improving I/O performance.


3. Volumes: The “Managed Storage”

A Volume is a directory within the Docker area on the host filesystem (/var/lib/docker/volumes/).

The Mechanism

Docker manages these directories. Non-Docker processes should not touch them.

  • Isolation: Safe from accidental host modifications.
  • Portability: Volumes can be backed up or migrated easily using Docker CLI.
  • Performance: On Mac/Windows, Volumes live inside the Linux VM. This means native Linux filesystem performance (ext4) with zero overhead.

When to use

  • Databases: Postgres, MySQL, Redis data directories.
  • Production: Any persistent state that the application generates.

4. Interactive: The Mount Visualizer

Explore how data flows between the Host and Container in different modes.

Host Filesystem

/home/user/project
📄 config.json
--bind

Container Filesystem

/app/config
Empty
Mode: Bind Mount. Host and Container share the same inode. Changes are instant.

5. Code Example: Persistence in Practice

How to access mounted data from your application code.

package main

import (
    "fmt"
    "os"
)

// In Dockerfile: VOLUME /data
// Run: docker run -v my-vol:/data my-app
func main() {
    // 1. Write to the persistent volume
    data := []byte("User Preference: Dark Mode")
    err := os.WriteFile("/data/settings.txt", data, 0644)
    if err != nil {
        panic(err)
    }

    // 2. Read from a bind-mounted config
    // Run: docker run -v $(pwd)/config.json:/app/config.json my-app
    config, err := os.ReadFile("/app/config.json")
    if err != nil {
        fmt.Println("No config found, using defaults")
    } else {
        fmt.Printf("Loaded config: %s\n", config)
    }
}
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.io.IOException;

public class PersistenceDemo {
    public static void main(String[] args) {
        try {
            // 1. Write to persistent volume
            // Path must match the -v mount point
            var volPath = Paths.get("/data/settings.txt");
            Files.writeString(volPath, "User Preference: Dark Mode",
                StandardOpenOption.CREATE);

            // 2. Read from bind-mounted config
            var configPath = Paths.get("/app/config.json");
            if (Files.exists(configPath)) {
                String config = Files.readString(configPath);
                System.out.println("Loaded config: " + config);
            } else {
                System.out.println("No config found, using defaults");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}