Docker for Web Developers: A Beginner’s Tutorial With a Real Node.js Example

If you have been writing web apps for a while, you have probably heard your colleagues say “it works on my machine” way too often. Docker is the tool that quietly killed that sentence. In this docker tutorial for web developers, we will skip the theory dumps and go straight into something useful: we will dockerize a real Node.js + Express app, explain the concepts as we hit them, and show you the gotchas nobody warns you about the first time.

By the end of this guide you will have a working Dockerfile, a docker-compose.yml, hot reload during development, and a clear mental model of what containers, images and volumes actually are.

Why Docker matters for web developers in 2026

Web stacks have become heavier. A single project may need Node.js 22, Postgres 16, Redis, a queue worker, maybe a Python script for image processing. Installing all of that on your laptop, switching versions between projects, and onboarding a new teammate is painful.

Docker solves three concrete problems:

  • Consistency: the same app runs the same way on your Mac, your colleague’s Windows machine, and the production Linux server.
  • Isolation: project A uses Node 18, project B uses Node 22, and they do not fight each other.
  • Speed of onboarding: a new developer clones the repo, runs one command, and is coding 5 minutes later.
docker container shipping

The 3 Docker concepts you actually need to know

1. Image

An image is a read only template. Think of it as a snapshot of a filesystem plus the instructions to run a process. node:22-alpine is an image. Your app, once built, becomes an image.

2. Container

A container is a running instance of an image. You can start, stop and delete containers without affecting the image. You can run 10 containers from the same image.

3. Volume

A volume is persistent storage that lives outside the container. When a container is destroyed, its internal filesystem is gone. Volumes are how you keep database data, uploaded files, or share your source code with the container during development.

Concept Analogy Lifetime
Image A class in OOP Permanent until deleted
Container An instance of that class Until you stop or remove it
Volume An external hard drive Independent of containers

Step 1: Install Docker Desktop

Head to docker.com and download Docker Desktop for your OS. After installation, verify it works:

docker --version
docker run hello-world

If you see a friendly hello message, you are ready.

docker container shipping

Step 2: Create a simple Node.js + Express app

Let’s build a tiny API so we have something real to containerize.

mkdir docker-node-demo && cd docker-node-demo
npm init -y
npm install express

Create a server.js file:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ message: 'Hello from inside a container!', time: new Date() });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Update the scripts section of package.json:

"scripts": {
  "start": "node server.js",
  "dev": "node --watch server.js"
}

Step 3: Write your first Dockerfile

Create a file named Dockerfile (no extension) at the root of the project:

# Use an official lightweight Node image
FROM node:22-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package files first to leverage Docker's layer cache
COPY package*.json ./

# Install only production dependencies
RUN npm ci --omit=dev

# Copy the rest of the source code
COPY . .

# Document the port the app listens on
EXPOSE 3000

# Default command
CMD ["node", "server.js"]

Also add a .dockerignore file. This is the single most forgotten file by beginners and it will save you from copying node_modules into your image:

node_modules
npm-debug.log
.git
.env
Dockerfile
docker-compose.yml

Step 4: Build and run the image

Build the image and tag it:

docker build -t docker-node-demo .

Run a container from it:

docker run -p 3000:3000 --name my-api docker-node-demo

Open http://localhost:3000 in your browser. Done. Your Node.js app is running inside a container.

Understanding the port mapping

The -p 3000:3000 flag means host port : container port. The first number is what you type in your browser, the second is what your app listens to inside the container. They do not need to match.

docker container shipping

Step 5: Add docker-compose for a real dev workflow

Running long docker run commands gets old fast. Docker Compose lets you describe your stack in a YAML file. It also makes adding a database trivial.

Create docker-compose.yml:

services:
  api:
    build: .
    container_name: node-api
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - PORT=3000
    volumes:
      - .:/app
      - /app/node_modules
    command: npm run dev

  db:
    image: postgres:16-alpine
    container_name: node-db
    environment:
      - POSTGRES_USER=demo
      - POSTGRES_PASSWORD=demo
      - POSTGRES_DB=demo
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Start everything with one command:

docker compose up

To stop and remove the containers:

docker compose down

What the volumes do here

  • .:/app mounts your local source code into the container, so editing a file on your laptop updates it inside the container instantly. Combined with node --watch, you get hot reload.
  • /app/node_modules is a trick: it tells Docker to keep the container’s own node_modules folder and not let your host folder overwrite it. This avoids the classic “binary built for the wrong OS” error.
  • pgdata is a named volume that survives container restarts. Your database data is safe.

Common gotchas web developers hit the first time

  1. Forgetting .dockerignore: your image ends up huge and slow because node_modules got copied in.
  2. Wrong host binding: if your app listens on 127.0.0.1 instead of 0.0.0.0, Docker cannot expose it. Always bind to 0.0.0.0 inside containers.
  3. Cache busted on every build: copy package.json and run npm ci before copying the rest of the code. This keeps your dependency layer cached.
  4. node_modules conflict between host and container: use the anonymous volume trick shown above (/app/node_modules).
  5. Hardcoded secrets in Dockerfile: never. Use environment variables or a .env file referenced from compose.
  6. Using latest tag: pin your base images to a specific version like node:22-alpine. Future you will thank present you.
  7. File permission issues on Linux: files created by the container may be owned by root. Add a non-root user in your Dockerfile for production images.
docker container shipping

Going to production: a few quick wins

The Dockerfile we wrote is fine for development. For production, consider a multi-stage build to keep the image small:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build || echo "no build step"

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

This pattern keeps build tools out of the final image and runs the app as a non-root user.

Useful Docker commands cheat sheet

Command What it does
docker ps List running containers
docker ps -a List all containers, including stopped
docker images List local images
docker logs -f <name> Follow logs of a container
docker exec -it <name> sh Open a shell inside a running container
docker system prune -a Free disk space by deleting unused data
docker compose up -d Start stack in detached mode
docker compose logs -f api Follow logs of the api service

FAQ

Is Docker good for web development?

Yes. It gives you reproducible environments, makes onboarding new developers fast, and removes the gap between your laptop and your production server. For solo projects it may feel like overhead, but as soon as you add a database or work in a team, the benefits are clear.

Can I learn Docker in 2 days?

You can learn enough to be productive in a weekend. The basics covered in this tutorial (images, containers, volumes, Dockerfile, docker-compose) are 90% of what you will use daily as a web developer. Mastering networking, orchestration and security takes longer.

Do I need Docker if I deploy to a PaaS like Vercel or Render?

Not strictly, but Docker still helps locally for spinning up databases, queues or specific Node.js versions. Many platforms also let you deploy a custom Dockerfile when you outgrow their defaults.

What is the difference between docker run and docker compose up?

docker run starts a single container with options passed on the command line. docker compose up reads docker-compose.yml and starts a full stack of services together, with networking already wired between them.

Should I commit the Dockerfile to git?

Absolutely. The Dockerfile and docker-compose.yml are part of your project. They should be versioned alongside the source code so every teammate gets the exact same environment.

Wrapping up

You now have a working Docker setup for a Node.js + Express app, a clear mental model of images, containers and volumes, and a list of the traps that catch most beginners. Clone this pattern into your next project, add a database service, and watch how much smoother your dev workflow becomes.

At Coding4, we help teams modernize their development workflow and ship faster with containerized stacks. If you want a hand auditing your setup or migrating an existing app to Docker, get in touch.

Leave a Comment

Your email address will not be published. Required fields are marked *