Live-Reload Node.js Dev with Bind & Anonymous Volumes
In Part 2 we containerised a tiny Express “/health” service and pushed it to Docker Hub. This time we’ll swing into full developer‑comfort mode: automatic code reload, lightning‑fast rebuilds, and zero **node_modules* clutter on your host.*
Learning Aims
By the end of this article you will be able to:
- Author an efficient Dockerfile that caches dependencies smartly.
-
Write a docker-compose.yml that wires up
- a bind mount for live code editing,
- an anonymous volume for dependency isolation, and
- environment flags that make nodemon poll reliably inside Docker.
- Run the stack with one command, watching changes hot‑reload in the browser.
1 Project Snapshot
bind-volume-demo/
├─ Dockerfile
├─ docker-compose.yml
├─ package.json # scripts:{ "dev":"nodemon --legacy-watch index.js" }
└─ index.js
index.js
:
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
app.get("/", (_req, res) => {
res.send("Hello World! ");
});
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
});
The Dockerfile
A Dockerfile
is a text document that contains all the commands a user could call on the command line to assemble an image.
FROM node:20-alpine
-
FROM
: This is the first instruction in anyDockerfile
. It specifies the base image to build upon. -
node:20-alpine
: This is the base image we are using. It's an official Node.js image, version 20, based on the lightweight Alpine Linux distribution. Using analpine
image is a good practice to keep your Docker images small.
WORKDIR /usr/src/app
-
WORKDIR
: This sets the working directory for any subsequentRUN
,CMD
,ENTRYPOINT
,COPY
, andADD
instructions. If the directory doesn't exist, Docker will create it. Here, we're telling Docker to use/usr/src/app
as the default directory inside the container.
COPY package*.json ./
RUN npm i && npm cache clean --force
-
COPY package*.json ./
: This command copies thepackage.json
andpackage-lock.json
files from your project directory into the container's working directory (/usr/src/app
). We copy these files first and runnpm install
in the next step to take advantage of Docker's layer caching. If these files don't change, Docker will use the cachednode_modules
layer, which speeds up subsequent builds. -
RUN npm i && npm cache clean --force
: This command runsnpm install
(shortened tonpm i
) inside the container to install the dependencies listed inpackage.json
. We also clean the npm cache to keep the image size down.
COPY . .
-
COPY . .
: This copies the rest of your project's source code (likeindex.js
) into the container's working directory. This is done after installing dependencies, so that changes to your source code don't cause Docker to reinstall all the npm packages every time you build the image.
CMD ["npm", "run", "dev"]
-
CMD
: This specifies the command to run when the container starts. Here, we're running thedev
script from ourpackage.json
, which starts the application usingnodemon
.
The docker-compose.yml
File
Docker Compose is a tool for defining and running multi-container Docker applications. The docker-compose.yml
file is a YAML file that configures your application's services.
services:
dev:
-
services
: This is the root key where you define the different services that make up your application. -
dev
: This is the name we've given to our service.
build: .
-
build
: This tells Docker Compose to build a Docker image. -
.
: The.
specifies that Docker should look for theDockerfile
in the current directory.
container_name: bind_dev
-
container_name
: This sets a custom name for your container, making it easier to identify.
command: npm run dev
-
command
: This overrides the defaultCMD
in theDockerfile
. While it's the same command in this case, it's useful to know you can change the container's startup command here.
ports:
- "3000:3000"
-
ports
: This maps ports between your host machine and the container. -
"3000:3000"
: This maps port 3000 on your host machine to port 3000 inside the container. This allows you to access the application in your browser athttp://localhost:3000
. The format is"HOST_PORT:CONTAINER_PORT"
.
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
-
volumes
: This is where we define our data volumes. -
- .:/usr/src/app
: This is the bind mount. It mounts the current directory (.
) on your host machine to the/usr/src/app
directory inside the container. This is the magic that enables live-reloading. -
- /usr/src/app/node_modules
: This is an anonymous volume. It tells Docker to use thenode_modules
directory that was created inside the container during the build, rather than the one from your host machine. This is important because your localnode_modules
might contain dependencies compiled for your host OS, which could be incompatible with the container's OS (Alpine Linux).
environment:
- CHOKIDAR_USEPOLLING=true
- CHOKIDAR_INTERVAL=1000
-
environment
: This sets environment variables inside the container. -
CHOKIDAR_USEPOLLING=true
:nodemon
uses a library called Chokidar to watch for file changes. In some environments, the default file watching mechanism doesn't work well with Docker's bind mounts. Setting this variable tells Chokidar to use a polling mechanism, which reliably detects changes. -
CHOKIDAR_INTERVAL=1000
: This is an optional setting that tells Chokidar to check for file changes every 1000 milliseconds (1 second).
Bind Mount vs Anonymous Volume
For | Bind mount | Anonymous volume |
---|---|---|
Editing code live | ✅ | ❌ |
OS‑correct binaries | 🚩 may clash | ✅ |
Typical Node dev | Use for source | Use for deps |
4 Running the Stack
docker compose up
-
docker compose
— CLI for Compose v2. -
up
— build images (if needed) and start containers.
Add -d
to detach, --build
to force rebuild.
Stop with Ctrl+C
or docker compose down
.
To test the project:
- Open your web browser and navigate to
http://localhost:3000
. You should see the message "Hello World!". - Now, open the
index.js
file in your code editor and change the message insideres.send()
. For example:res.send("Hello from Docker!");
. - Save the file and refresh the page in your browser. You should see the updated message instantly, without needing to restart the container. This is the power of the bind mount!
To stop the project:
docker-compose down
-
down
: This command stops and removes the containers, networks, and volumes created bydocker-compose up
.
5 Conclusion — What We Learned
✅ Cache‑friendly Dockerfile layering.
✅ Bind mount for hot reload, anonymous volume for clean dependencies.
✅ Compose orchestration with environment tweaks for nodemon polling.
Stay tuned! 🚀
Top comments (0)