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:
- Data Loss: When the container is removed (
docker rm), that layer is deleted. - 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.
2. Bind Mounts: The “Direct Link”
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 --bindsyscall. 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.conffrom 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
Container Filesystem
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();
}
}
}