Mastering Volumes & Production-friendly Node Images
In Part 3 we hot-reloaded an Express server with a bind mount and an anonymous
node_modules
volume. Now we’ll level-up: learn every volume flavour Docker offers and slim our image withnpm i --omit=dev
. By the end you’ll know exactly **where* to store code, dependencies, and runtime data — and why.*
Learning Aims
- Differentiate bind, anonymous, and named volumes — when to use each and what benefits they bring.
-
Write a Dockerfile that installs production-only dependencies via
npm i --omit=dev
and caches the layer smartly. -
Compose a
docker-compose.yml
that mounts three storage types in one service.
1 Project Snapshot
volume-types-demo/
├─ Dockerfile
├─ docker-compose.yml
├─ package.json # contains devDependencies (nodemon)
└─ index.js # appends timestamp → /data/test_data
A minimal index.js
appends a timestamped line to /data/test_data
then prints the whole file. It’s perfect for watching volume persistence.
2 Dockerfile — Line-by-Line
FROM node:20-alpine # 1
WORKDIR /usr/src/app # 2
COPY package*.json ./ # 3
RUN npm i --omit=dev \
&& npm cache clean --force # 4 ← spotlight line
COPY . . # 5
CMD ["npm", "run", "dev"] # 6
# | Instruction | What it does (plain English) | Why it matters |
---|---|---|---|
1 | FROM node:20-alpine |
Starts from the tiny Alpine Linux image with Node 20 pre-installed. | Shaves 100 MB+ off your pulls compared to node:20 (Debian). |
2 | WORKDIR /usr/src/app |
Sets the working directory for all subsequent commands. | Keeps files organised in one predictable location. |
3 | COPY package*.json ./ |
Copies package.json and package-lock.json into the image. |
Lets Docker cache dependency install separately from source code changes. |
4 | RUN npm i --omit=dev && npm cache clean --force |
① Installs only production dependencies (excludes devDependencies ). ② Immediately removes the npm cache to reclaim ~20 MB. |
• Creates a leaner runtime image — no nodemon, eslint, etc. • Faster container start‑up and lower CVE surface. |
5 | COPY . . |
Copies the remainder of your source code. | Because it’s after the npm layer, editing code doesn’t trigger a fresh npm install . |
6 | CMD ["npm","run","dev"] |
Default command when the container starts; the dev script typically runs nodemon. |
Keeps the container interactive for local development. Compose can override this later. |
Tip: swap
--omit=dev
for--production
if you’re on older npm versions (< 7).
3 docker-compose.yml — Word-by-Word
services:
app:
build: .
container_name: app_dev
command: npm run dev # echoes & exits; replace with nodemon for live reload
ports:
- "3000:3000" # HOST:CONTAINER
volumes:
# 1️⃣ Bind mount → live-reload source
- .:/usr/src/app:cached
# 2️⃣ Named volume → dependency isolation
- app_node_modules:/usr/src/app/node_modules
# 3️⃣ Named volume → runtime data persistence
- app_data:/data
volumes:
app_node_modules: # Docker manages location + lifecycle
app_data:
Key | Value | Detailed beginner-friendly explanation |
---|---|---|
services |
Root of every Compose file. Each key under it describes one container (here app ). |
|
build: . |
Dot = “look in this folder for a Dockerfile and context.” Compose auto‑rebuilds if missing. | |
container_name |
Gives your container a friendly fixed name (app_dev ) instead of a random one. |
|
command |
Overrides the Dockerfile’s CMD at run‑time. Handy for trying different start scripts without rebuilding. |
|
ports |
3000:3000 publishes container port 3000 on host port 3000 so http://localhost:3000 works. Format = HOST:CONTAINER . |
|
volumes |
List of storage mounts. Order matters only to humans; Docker treats each line independently. | |
– .:/usr/src/app:cached
|
Bind mount: left side is a host path (. = current folder). Right side is container path. :cached hints macOS/Windows to favour host‑write speed. |
|
– app_node_modules:/usr/src/app/node_modules
|
Named volume managed by Docker. Keeps Linux‑compiled binaries (bcrypt, sqlite, etc.) separate from your host’s node_modules . |
|
– app_data:/data
|
Another named volume to persist log files & uploads. Keeps runtime data even if you delete and rebuild the image. | |
volumes: (root) |
Declares the named volumes so Compose can create/manage them. No config needed for default settings. |
4 Choosing the Right Volume Type
Use-case | Bind Mount (hostPath:containerPath ) |
Named Volume (name:/path ) |
Anonymous Volume (/path ) |
---|---|---|---|
Live-editing source code | ✅ instant reload | 🚫 need restart | 🚫 |
OS-specific binaries (node_modules , venv ) |
⚠️ risk host ≠ container OS | ✅ isolated | ✅ but unnamed |
Databases / uploads / logs | ⚠️ easy to delete accidentally | ✅ easy backup | ✅ throw‑away |
Quick one-off experiments | ✅ zero Docker volume clean‑up | 🚫 extra steps | ✅ auto-clean |
Rules of thumb
- Bind mount when you need immediate feedback in a dev loop.
-
Named volume when the data must outlive containers and you want easy CLI access (
docker volume ls
). - Anonymous volume for throw‑away state you don’t care to name.
5 Running & Observing Persistence
# 1. Start the stack
docker compose up
# 2. Append another line: stop + start again
CTRL+C
docker compose up
# 3. Inspect stored data
docker volume inspect volume-types-demo_app_data
Every run grows test_data
, proving app_data
survives container recreation.
Clean up everything, including volumes:
docker compose down -v
6 Conclusion — What We Learned
✅ Three volume types and the sweet spot for each.
✅ Building a production-ready Node image with npm i --omit=dev
.
✅ Compose tricks: overriding commands, combining mounts, and why :cached
speeds things up on non‑Linux hosts.
Stay tuned — your Docker toolbox is growing fast! 🚀