Part 10 focused on deployment containers, where the image usually contains a fixed snapshot of your app. In this chapter, we switch to a development container: a container you keep running while you edit code on your own machine. That creates a new problem. You want source files to update live from the host, but you do not want macOS- or Windows-built dependencies to get mixed into a Linux container.
Imagine this common mistake first:
- You run
npm installon your host machine. - You mount the whole project with
.:/app. - The container now sees the host copy of
node_modules. - A package with a native binary was built for the host OS, not for the Linux container.
That is why development containers often mix multiple mount types. A broad mount can share the whole project, and a more specific mount can keep one subfolder inside a Docker volume. Once that pattern makes sense, bind mounts, named volumes, and anonymous volumes stop feeling like separate memorization topics.
What this post covers
- What each volume type does in practice
- A Node.js dev container lab that uses all three
- Why
/app/node_modulesneeds its own volume - How cleanup and persistence actually work
- Which volume choice fits each situation
Reading card
- Estimated time: 15 minutes
- Prereqs: Docker Compose basics and npm experience
- After reading: you can explain when to split bind mounts from anonymous volumes.
Three volume types at a glance
- Bind mount: connect a specific host folder to a container path, for example
.:/app. - Named volume: let Docker store data in its own volume area and reference it by name, such as
mydata:/var/lib/mysql. - Anonymous volume: let Docker create a volume for a path without giving it a human-friendly name, for example
/app/node_modules.
Choose based on the data’s purpose:
- Source code needs immediate edits → bind mount.
- Databases or cache files need to survive container recreation → named volume.
- OS-specific folders like
node_modulescan live inside the container → anonymous or named volume.
One more idea matters before the lab: volumes keep their data when you stop and start containers again. If you run docker compose down -v, Docker removes the project containers and the attached volumes. For anonymous volumes, that command is the easiest cleanup path.
This mental model helps:
| Data | Best first choice | Why |
|---|---|---|
| Project source | Bind mount | Edit on the host and see changes in the container right away |
| npm cache | Named volume | Reuse downloaded packages across new container runs in the same project |
node_modules |
Anonymous or named volume | Keep Linux-built dependencies inside Docker |
Lab: Node.js development container
Start with the smallest useful pattern first. Save this file as compose.yaml in a Node project.
services:
dev:
image: node:20-alpine
working_dir: /app
volumes:
- .:/app # Bind mount
- node-cache:/root/.npm # Named volume
- /app/node_modules # Anonymous volume
ports:
- "4321:4321"
command: sh -c "npm install && npm run dev"
volumes:
node-cache:
In this file, . means your current project folder on the host, and /app is the working directory inside the container.
.:/appmirrors your local code into the container./app/node_modulescreates a second, more specific mount for only that subfolder.node-cachestores the npm cache so new dev containers in the same Compose project can reuse downloaded packages.
How the overlapping mounts work
The important part is the overlap. Docker uses the more specific container path when mounts overlap:
.:/appshares the whole project from your computer./app/node_modulesthen replaces just that one subfolder with Docker-managed storage.- Result: your code stays live-synced, but dependencies stay inside the container.
This is the core pattern to remember: bind mount the code, isolate node_modules. The npm cache volume is optional but recommended when you rebuild or recreate the dev container often.
Run docker compose up dev, edit a file, and confirm your dev process notices the change. docker volume ls should show the named volume (node-cache) and also one anonymous volume for /app/node_modules, usually with a generated ID-like name.
Why isolate node_modules?
Some Node packages include native addons or platform-specific binaries. When macOS, Windows, and Linux mix, host-built dependencies often stop matching the container environment:
- native addons built with tools such as
node-gypusually need to be compiled for the container OS - prebuilt binaries downloaded on one OS may not run on another OS
- host and container users can produce confusing ownership or permission problems on Linux
So the main reason is not that every package is dangerous. The real issue is that node_modules can contain platform-specific files, while your source code usually does not.
That is why a good default split looks like this:
- Source folders → bind mount with the host.
node_modules→ anonymous or named volume inside the container.- npm cache → named volume for faster reinstalls.
If you prefer a named volume for dependencies, adjust the file:
services:
dev:
volumes:
- .:/app
- node-cache:/root/.npm
- node-modules:/app/node_modules
volumes:
node-cache:
node-modules:
The advantage is manageability. A stable name makes the volume easier to inspect and remove on purpose. It also makes it clearer that the same dependency volume will be reused for later docker compose up runs in this project.
Choose between anonymous and named volumes like this:
| Situation | Better choice | Why |
|---|---|---|
| You want the simplest setup and easy project-scoped cleanup | Anonymous volume | docker compose down -v clears it with the rest of the project |
| You want a stable dependency volume between repeated dev sessions | Named volume | Easier to inspect, keep, and remove deliberately |
Cleanup and common mistakes
When you want a clean reset for this project, start with docker compose down -v. That removes the containers and volumes created by this Compose project, including the anonymous or named dependency volume. Use docker volume prune only if you really want to remove all unused Docker volumes on your machine.
The most common mistake is assuming a bind mount behaves like .dockerignore. It does not. If you mount .:/app, the container sees that whole directory.
Tips that prevent problems
- Permission mismatches: on native Linux, host and container users can disagree about file ownership. If that happens, run the container with your host UID/GID or use a project-specific user setup instead of assuming
user: "1000:1000"always fits. - Volume cleanup: anonymous volumes usually appear as generated IDs in
docker volume ls. Remove them with project-scoped commands such asdocker compose down -v, or usedocker volume rm <ID>when you know the exact volume. - Hot-reload speed: file watching through bind mounts can feel slower on macOS than on Linux. If that affects your tools, mount only the paths you need and check Docker Desktop file-sharing performance options.
- Image contents: a bind mount hides whatever the image already had at that path. If your image copied app files into
/app,.:/appreplaces that view during development. - Windows paths: in Compose files, prefer the normal project-relative style such as
.:/app. If you use Windows directly, avoid mixing backslash-heavy host paths into the same learning example. - Team alignment: sharing one Compose file helps standardize Node and npm behavior across the team.
Volume strategies you can apply now
For development containers, start with these defaults:
- Source code → bind mount
- Databases and other persistent data → named volumes
- OS-dependent folders like
node_modules→ anonymous or separately named volumes
Those three rules already cover most dev Compose files. In Part 12, the series closes by asking where Docker and Compose are enough on their own and what infrastructure topic to learn next.
💬 댓글
이 글에 대한 의견을 남겨주세요