Containers are designed to be ephemeral. When a container is stopped and removed, everything inside its writable layer (the top layer of the OverlayFS) is permanently destroyed.
If you are running a stateless Next.js frontend or a Node.js API, this is perfectly fine. But what happens when you run a PostgreSQL or Redis container? If the container restarts, you lose your entire database.
In this module, we will explore how to manage stateful data in Docker using Bind Mounts and Volumes, understand their performance implications, and learn when to use which.
The Writable Container Layer (Ephemeral Data)
As we learned in Module 1, when a container starts, Docker creates a thin read-write layer on top of the underlying read-only image layers.
Any files written by your application (e.g., logging a file, uploading an image, writing database rows) are stored in this writable layer.
The critical rule: This layer is tightly coupled to the lifecycle of the container.
If the container is stopped (docker stop), the data persists.
If the container is removed (docker rm), the data is deleted forever.
To persist data beyond the lifespan of a container, you must bypass the Union File System and store the data directly on the host machine. Docker provides two primary ways to do this: Bind Mounts and Volumes.
1. Bind Mounts
A Bind Mount maps a specific directory on your host machine to a specific directory inside the container.
bash
(The syntax is -v HOST_PATH:CONTAINER_PATH)
Characteristics of Bind Mounts
Absolute Control: The host machine dictates the exact file path.
Two-Way Sync: Changes made on the host are immediately visible in the container, and vice versa.
Host Dependent: The container relies on the host having a specific directory structure. This makes docker run commands less portable across different developer machines or production servers.
When to use Bind Mounts
Bind mounts are almost exclusively used for Local Development.
By bind-mounting your local source code into the container, you can use your local IDE (like VS Code) to edit files. Frameworks like Next.js or tools like nodemon will detect the file changes across the mount and instantly hot-reload the application inside the container.
[!WARNING]
Performance on macOS/Windows:
Bind mounts on Linux are native and incredibly fast. However, on Docker Desktop for macOS and Windows, the files must cross the boundary between your physical host OS and the hidden Linux VM.
If you bind-mount a massive node_modules folder on a Mac, file I/O operations will be severely penalized, and your app will run incredibly slowly. Never bind-mount node_modules—install them natively inside the image instead!
2. Docker Volumes
A Volume is a managed directory created and maintained by Docker itself, stored deep within Docker's internal data directory (e.g., /var/lib/docker/volumes/ on Linux).
bash
Characteristics of Volumes
Managed by Docker: You don't care where the data lives on the host disk; Docker handles the pathing.
Isolation: The data is isolated from the host machine's normal file system, preventing accidental deletion or permissions issues.
High Performance: Because the data lives entirely inside the Linux VM (even on macOS/Windows), I/O performance is native and extremely fast.
Lifecycle: Volumes survive container deletion. You must explicitly run docker volume rm pgdata to destroy the data.
When to use Volumes
Volumes are the absolute standard for Production Data and Databases.
If you run PostgreSQL, Redis, or an S3-compatible MinIO container, you should always back their data directories with Docker Volumes.
Managing Permissions
One of the most frustrating aspects of storage in Docker is file permissions, particularly when dealing with Bind Mounts.
If your Node.js application runs as a non-root user (e.g., USER node with UID 1000) inside the container, and it tries to write a file to a bind-mounted directory owned by root on your host machine, the application will crash with an EACCES: permission denied error.
Similarly, if the container writes a file to the bind mount, that file will appear on your host machine owned by whatever UID the container was running as (often root), making it difficult to delete or edit the file locally.
The Solution:
Ensure the UID/GID of the process running inside the container matches the UID/GID of the directory owner on the host machine. (We will cover running as non-root deeply in Module 7).
Key Takeaways
Container Layers are Ephemeral: Never store database state or user uploads in the container's writable layer.
Bind Mounts: Used for local development to enable hot-reloading by syncing source code from the host to the container. Suffer from performance penalties on macOS/Windows.
Volumes: Managed by Docker, highly performant, and isolated. The correct choice for persisting database data (Postgres, Redis) in both development and production.
Permissions Matter: Remember that the UID inside the container maps directly to the UID on the host filesystem when using bind mounts.
Knowledge Check
You are setting up a local development environment for a Next.js application using Docker. You want to edit files in VS Code and see the changes hot-reload in the browser. Which storage mechanism should you use?
You launch a PostgreSQL container in production and need to ensure the database files survive container restarts and updates. Why is using a Docker Volume (docker volume create pgdata) superior to using a Bind Mount (-v /opt/pgdata:/var/lib/postgresql/data) for this specific use case?
A Node.js container running as a non-root user (USER node, UID 1000) tries to write a log file to a bind-mounted directory. The directory on the host machine is owned by root. What will happen?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.