"It works on my machine" used to be a punchline. Docker made it a guarantee. By packaging your application with its entire runtime environment — OS libraries, language runtime, dependencies, config files — Docker ensures that the app runs identically on your laptop, your colleague's machine, a CI server, and production.
Containers vs Virtual Machines
Virtual machines run a complete operating system on top of a hypervisor. Each VM has its own kernel, its own filesystem, and its own memory space. A single VM might consume 2GB+ of RAM before you even start your app.
Containers share the host OS kernel. They isolate the application's filesystem, network, and processes using Linux namespaces and cgroups — but without the overhead of a full OS. A container typically starts in under a second and uses megabytes, not gigabytes.
Core Concepts
- Image: A read-only template containing the application code, runtime, libraries, and dependencies. Think of it as a snapshot.
- Container: A running instance of an image. You can start, stop, and destroy containers without affecting the underlying image.
- Dockerfile: A text file with instructions for building an image, step by step.
- Registry: A repository for storing and distributing images. Docker Hub is the default public registry.
Writing a Dockerfile
# Use official Node.js LTS as base image FROM node:20-alpine # Set working directory inside container WORKDIR /app # Copy dependency manifests first (for layer caching) COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy application source code COPY . . # Expose the application port EXPOSE 3000 # Define the command to start the app CMD ["node", "server.js"]
Essential Docker Commands
# Build an image from a Dockerfile docker build -t my-app:1.0 . # Run a container from an image docker run -d -p 3000:3000 --name my-app my-app:1.0 # List running containers docker ps # View container logs docker logs my-app # Stop and remove a container docker stop my-app && docker rm my-app # List local images docker images # Remove an image docker rmi my-app:1.0
Docker Compose for Multi-Service Apps
Most web apps need more than one service — an application server, a database, maybe a cache or message queue. Docker Compose lets you define and run all these services together with a single YAML file.
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
# Start all services (detached) docker compose up -d # View logs for all services docker compose logs -f # Stop and remove all containers docker compose down # Rebuild images after code changes docker compose up --build -d
Best Practices for Production
- Use multi-stage builds to keep final images small. Build in one stage, copy only the output to a minimal runtime stage.
- Use
.dockerignoreto excludenode_modules,.git, and other unnecessary files from the build context. - Pin base image versions — use
node:20.11-alpineinstead ofnode:latestfor reproducible builds. - Run as non-root user inside containers for security.
- Leverage layer caching — copy
package.jsonand runnpm installbefore copying source code so dependencies are cached. - Use health checks so orchestrators (Docker Swarm, Kubernetes) know when your app is ready.
Try Our Free Developer Tools
Minify, beautify, and format your code instantly in your browser.