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.

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.

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.

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
.:/appmounts your local source code into the container, so editing a file on your laptop updates it inside the container instantly. Combined withnode --watch, you get hot reload./app/node_modulesis a trick: it tells Docker to keep the container’s ownnode_modulesfolder and not let your host folder overwrite it. This avoids the classic “binary built for the wrong OS” error.pgdatais a named volume that survives container restarts. Your database data is safe.
Common gotchas web developers hit the first time
- Forgetting .dockerignore: your image ends up huge and slow because
node_modulesgot copied in. - Wrong host binding: if your app listens on
127.0.0.1instead of0.0.0.0, Docker cannot expose it. Always bind to0.0.0.0inside containers. - Cache busted on every build: copy
package.jsonand runnpm cibefore copying the rest of the code. This keeps your dependency layer cached. - node_modules conflict between host and container: use the anonymous volume trick shown above (
/app/node_modules). - Hardcoded secrets in Dockerfile: never. Use environment variables or a
.envfile referenced from compose. - Using
latesttag: pin your base images to a specific version likenode:22-alpine. Future you will thank present you. - 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.

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.

