DEV Community

Cover image for Docker for Absolute Beginners — Part 3
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Docker for Absolute Beginners — Part 3

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:

  1. Author an efficient Dockerfile that caches dependencies smartly.
  2. 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.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • FROM: This is the first instruction in any Dockerfile. 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 an alpine image is a good practice to keep your Docker images small.
WORKDIR /usr/src/app
Enter fullscreen mode Exit fullscreen mode
  • WORKDIR: This sets the working directory for any subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD 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
Enter fullscreen mode Exit fullscreen mode
  • COPY package*.json ./: This command copies the package.json and package-lock.json files from your project directory into the container's working directory (/usr/src/app). We copy these files first and run npm install in the next step to take advantage of Docker's layer caching. If these files don't change, Docker will use the cached node_modules layer, which speeds up subsequent builds.
  • RUN npm i && npm cache clean --force: This command runs npm install (shortened to npm i) inside the container to install the dependencies listed in package.json. We also clean the npm cache to keep the image size down.
COPY . .
Enter fullscreen mode Exit fullscreen mode
  • COPY . .: This copies the rest of your project's source code (like index.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"]
Enter fullscreen mode Exit fullscreen mode
  • CMD: This specifies the command to run when the container starts. Here, we're running the dev script from our package.json, which starts the application using nodemon.

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:
Enter fullscreen mode Exit fullscreen mode
  • 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: .
Enter fullscreen mode Exit fullscreen mode
  • build: This tells Docker Compose to build a Docker image.
  • .: The . specifies that Docker should look for the Dockerfile in the current directory.
    container_name: bind_dev
Enter fullscreen mode Exit fullscreen mode
  • container_name: This sets a custom name for your container, making it easier to identify.
    command: npm run dev
Enter fullscreen mode Exit fullscreen mode
  • command: This overrides the default CMD in the Dockerfile. 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"
Enter fullscreen mode Exit fullscreen mode
  • 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 at http://localhost:3000. The format is "HOST_PORT:CONTAINER_PORT".
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
Enter fullscreen mode Exit fullscreen mode
  • 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 the node_modules directory that was created inside the container during the build, rather than the one from your host machine. This is important because your local node_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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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:

  1. Open your web browser and navigate to http://localhost:3000. You should see the message "Hello World!".
  2. Now, open the index.js file in your code editor and change the message inside res.send(). For example: res.send("Hello from Docker!");.
  3. 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
Enter fullscreen mode Exit fullscreen mode
  • down: This command stops and removes the containers, networks, and volumes created by docker-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)