Two Types of Volumes
Docker Compose supports two distinct volume patterns, and confusing them is a common source of bugs.
1. Named Volumes — for persistent data that should survive container restarts, declared at the top level and mounted into a service (e.g. Postgres's data directory). Docker stores named volume data in /var/lib/docker/volumes/ on the host. The data persists even if you run docker-compose down. To delete it, you must explicitly run docker-compose down -v.
2. Bind Mounts — for syncing local files into a container in real time, mounting the current directory straight into the container's working directory, often paired with an anonymous volume (a bare container path with no host source) to preserve a container-built directory like node_modules that would otherwise be shadowed by the mount.
The Developer's Live Reload Workflow
Bind mounts are what make Docker Compose genuinely useful for day-to-day development. Here is the exact sequence of events:
- You edit
app.pyin VS Code on your laptop. - The bind mount means the file at
/code/app.pyinside the container is updated instantly — no copy, no rebuild. - Your web framework's development server (Flask's reloader, Node's
nodemon, Django'srunserver) detects the file change via filesystem events. - The server reloads automatically inside the container.
- You refresh your browser and see the change — typically in under a second.
This gives you the consistency of a containerized environment (same OS, same dependencies, same config as production) with the speed of editing files directly on your laptop.
The node_modules Trap
The anonymous volume trick (- /code/node_modules) in the example above solves a common problem: if you mount . into /code, the container's node_modules directory (installed during the Docker build) gets hidden by the host's empty directory. The anonymous volume tells Docker to keep the container's own node_modules intact, preventing the "module not found" errors that trip up developers new to Compose.