Introduction to Docker
Why It Matters
Docker packages an application with the operating system-level dependencies it needs to run. For Node.js developers, this reduces the classic "works on my machine" problem. A container can run the same app with the same Node version, environment variables, installed packages, and startup command across development, CI, staging, and production.
Docker is not only a deployment tool. It is also useful for local dependencies such as PostgreSQL, Redis, and message queues. Instead of installing every service directly on your laptop, you can run them as containers and remove them when finished.
Core Concepts
Image
An image is a read-only template used to create containers. It contains a filesystem, dependencies, and metadata such as the default command.
Example:
docker pull node:22-alpineThis downloads a Node.js image based on Alpine Linux.
Container
A container is a running instance of an image.
docker run --rm node:22-alpine node --versionThis starts a container, runs node --version, prints the version, and removes the container after it exits.
Dockerfile
A Dockerfile is a recipe for building an image.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]Each instruction creates or configures part of the image.
Layers
Docker images are built from layers. If a layer has not changed, Docker can reuse it from cache. This is why Dockerfiles often copy package.json and install dependencies before copying the rest of the source code.
Port Publishing
Containers have their own network namespace. If an app listens on port 3000 inside the container, you publish it to a host port:
docker run -p 3000:3000 my-node-appThe left side is the host port. The right side is the container port.
Volumes
Containers are disposable. Data written inside a container can disappear when the container is removed. Volumes persist data outside the container filesystem.
docker volume create postgres-dataVolumes are common for databases and local development workflows.
Syntax and Examples
Run a Node Container
docker run --rm node:22-alpine node -e "console.log(process.version)"This runs Node without installing Node directly on the host.
Run Redis Locally
docker run --name redis-dev --rm -p 6379:6379 redis:7-alpineYour Node app can connect to redis://localhost:6379 from the host.
Build an Image
Given this Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]Build it:
docker build -t node-fundamentals-api .Run it:
docker run --rm -p 3000:3000 -e PORT=3000 node-fundamentals-apiInspect Containers
docker ps
docker logs <container-id>
docker exec -it <container-id> sh
docker stop <container-id>These commands help you list, debug, enter, and stop containers.
Use Cases
Docker is useful for:
- Running consistent Node.js versions across environments.
- Shipping APIs with predictable dependencies.
- Running local databases, caches, and queues.
- Building CI pipelines that match production more closely.
- Creating isolated test environments.
- Deploying to platforms that run containers, such as ECS, Cloud Run, Kubernetes, Fly.io, Render, and many others.
Docker is less useful when:
- A project is a tiny local script with no dependencies.
- The team does not need environment consistency.
- The runtime platform does not accept containers and already provides a managed build system.
Security and Production Implications
Image Size
Smaller images usually download faster and have a smaller attack surface. Alpine images are small, but some native Node packages behave differently on Alpine because it uses musl libc. Debian slim images are larger but often more compatible.
Common base images:
node:22-alpine: small, good for many apps.node:22-slim: larger, more compatible.- Distroless images: minimal runtime, more advanced debugging tradeoffs.
Running as Root
Containers often run as root by default. Prefer a non-root user when possible.
USER nodeThe official Node image includes a node user.
Secrets
Do not bake secrets into images with ENV or COPY. Pass secrets at runtime through environment variables, secret managers, or platform-specific secret features.
Reproducible Installs
Use lockfiles and npm ci in Docker builds. npm install can update lockfile-resolved dependencies in ways that make builds less repeatable.
Container Lifecycle
Containers should be disposable. Store persistent data in databases, object storage, or volumes. Do not rely on files written inside an app container to survive deployment.
Common Mistakes
- Copying the entire project before running
npm ci, causing slow rebuilds. - Forgetting
.dockerignoreand sendingnode_modulesor secrets into the build context. - Binding the app to
localhostinside the container instead of0.0.0.0. - Storing uploaded files inside a disposable container filesystem.
- Baking environment-specific secrets into the image.
- Running database migrations accidentally every time a container starts without coordination.
- Assuming Docker replaces application monitoring, logging, and health checks.
Practical Challenge
Create a small Express app and containerize it:
- Add a
/healthroute. - Create a Dockerfile using an official Node image.
- Use
WORKDIR /app. - Copy package files before source files.
- Install dependencies with
npm ci. - Expose port
3000. - Run the app with
docker run -p 3000:3000.
Then stop and remove the container, rebuild the image after a source change, and confirm Docker reuses dependency layers when package-lock.json has not changed.
Recap
Docker images package application dependencies and containers run those images in isolated environments. For Node.js apps, Docker improves consistency across development, CI, and production. Learn images, containers, Dockerfiles, layers, ports, and volumes before moving to Compose or production deployment. Containers are disposable, so design configuration, secrets, logs, and persistent data accordingly.