Docker for Developers: Containers, Images & Docker Compose Guide
Why Docker Matters for Developers
Docker solves the oldest problem in software: "it works on my machine." By packaging an application with its entire runtime environment -- OS libraries, language runtime, dependencies, and configuration -- Docker guarantees that code runs identically on every developer's laptop, in CI/CD, and in production. No more debugging environment mismatches.
Unlike virtual machines, Docker containers share the host OS kernel, making them start in milliseconds and consume a fraction of the memory. A typical development setup with a web server, database, and cache requires three containers totaling 200MB of RAM, compared to 6GB+ for three VMs.
Core Concepts
| Concept | What It Is | Analogy |
|---|---|---|
| Image | Read-only template with code + dependencies | A class definition |
| Container | Running instance of an image | An object (instance of class) |
| Dockerfile | Build instructions for an image | A recipe / Makefile |
| Volume | Persistent storage outside the container | An external hard drive |
| Network | Virtual network connecting containers | A private LAN |
| Registry | Repository for sharing images (Docker Hub) | npm / PyPI for containers |
Essential Docker Commands
# Pull an image from Docker Hub
docker pull node:20-alpine
# Run a container (interactive + terminal)
docker run -it --rm node:20-alpine sh
# Run a web server (detached, port mapping)
docker run -d -p 3000:3000 --name my-app my-image
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View container logs
docker logs my-app
docker logs -f my-app # follow (tail)
# Execute a command inside a running container
docker exec -it my-app sh
# Stop and remove a container
docker stop my-app
docker rm my-app
# Build an image from a Dockerfile
docker build -t my-app:latest .
# Remove unused images and containers
docker system prune -aWriting a Dockerfile: Node.js Example
A Dockerfile is a text file with instructions to build an image. Each instruction creates a layer that Docker caches for fast rebuilds. Order matters -- put instructions that change frequently (like COPY . .) at the end to maximize cache hits.
Basic Dockerfile
# Use slim Node.js image (not full -- saves 300MB)
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy package files first (cache layer for dependencies)
COPY package.json package-lock.json ./
# Install production dependencies only
RUN npm ci --omit=dev
# Copy application source code
COPY . .
# Build the application (if using TypeScript, Next.js, etc.)
RUN npm run build
# Expose the port the app listens on
EXPOSE 3000
# Define the command to run the application
CMD ["node", "dist/server.js"]Multi-Stage Build (Production-Optimized)
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production (only the built output)
FROM node:20-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copy built output from builder stage
COPY --from=builder /app/dist ./dist
# Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Result: ~80MB instead of ~500MB
# No TypeScript, no devDependencies, no source code.dockerignore
# .dockerignore -- exclude from build context
node_modules
.git
.gitignore
*.md
.env
.env.*
coverage
.nyc_output
tests
__tests__
*.test.js
*.spec.js
Dockerfile
docker-compose*.yml
.dockerignoreThe .dockerignore file works like .gitignore -- it prevents unnecessary files from being sent to the Docker daemon during builds. Always exclude node_modules, .git, and test files. Validate your ignore patterns with a Regex Tester.
Docker Compose: Multi-Container Applications
Docker Compose defines multi-container applications in a single YAML file. Instead of running multiple docker run commands with complex flags, you declare your entire stack and start everything with docker compose up.
Full-Stack Development Setup
# docker-compose.yml
services:
# Node.js API server
api:
build: .
ports:
- "3000:3000"
volumes:
- .:/app # hot-reload: mount source code
- /app/node_modules # prevent overwriting container modules
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
command: npm run dev
# PostgreSQL database
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data # persist data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
# Redis cache
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata: # named volume for database# Docker Compose commands
docker compose up # start all services
docker compose up -d # start in background (detached)
docker compose down # stop and remove containers
docker compose down -v # also remove volumes (reset data)
docker compose logs api # view logs for one service
docker compose exec api sh # shell into a running service
docker compose build # rebuild images
docker compose ps # list running servicesDocker Compose uses YAML for configuration. If you work with JSON configs alongside YAML, check our guide on YAML vs JSON to understand the syntax differences. Format your JSON config files with our JSON Formatter.
Volumes: Persistent Data
Containers are ephemeral -- when you remove a container, its filesystem is gone. Volumes solve this by storing data outside the container's lifecycle. There are three types of mounts.
| Mount Type | Syntax | Best For |
|---|---|---|
| Named volume | pgdata:/var/lib/data | Database storage, persistent data |
| Bind mount | ./src:/app/src | Development hot-reload, config files |
| tmpfs mount | tmpfs: /tmp | Temporary data, secrets (RAM only) |
Docker Networking
Docker Compose automatically creates a network for your services. Containers can reach each other by service name (DNS resolution). The api service connects to the database as db:5432, not localhost:5432.
# Container networking in Docker Compose
# From the api container:
# db:5432 -> reaches PostgreSQL (service name = hostname)
# cache:6379 -> reaches Redis
# localhost -> only the api container itself
# Port mapping: host:container
ports:
- "3000:3000" # host port 3000 -> container port 3000
- "8080:3000" # host port 8080 -> container port 3000
# Internal-only service (no host port)
# (no 'ports' key = only reachable from other containers)
db:
image: postgres:16-alpine
# no ports = not exposed to host, only to other servicesDockerfile Best Practices
- Use specific image tags -- always pin versions (
node:20.11-alpine) instead ofnode:latest. Thelatesttag changes unpredictably and can break builds. - Order instructions by change frequency -- put rarely changing layers first (base image, system deps) and frequently changing layers last (source code) to maximize cache hits.
- Use multi-stage builds -- separate build tools and dev dependencies from the production image. This cuts image size by 70-90%.
- Run as non-root -- create a dedicated user with
USER. Running as root inside containers is a security risk. See our guide on Linux file permissions for user management. - Use .dockerignore -- exclude build artifacts, tests, documentation, and version control from the build context.
- One process per container -- run your application as PID 1. Do not run supervisord or multiple processes. Use Docker Compose for multi-process stacks.
- Add health checks -- use
HEALTHCHECKin Dockerfiles or healthcheck in Compose so orchestrators know when your app is ready.
Docker vs Alternatives
| Tool | Use Case | Compared to Docker |
|---|---|---|
| Podman | Drop-in Docker replacement | Daemonless, rootless by default |
| Kubernetes | Production orchestration | Scaling, self-healing, rolling deploys |
| VMs (VirtualBox) | Full OS isolation | Heavier, slower, stronger isolation |
| nix / devbox | Reproducible dev environments | No containers, package-level isolation |
| Dev Containers | VS Code remote development | Docker-based, IDE-integrated |
Frequently Asked Questions
What is the difference between a Docker image and a container?
docker build and stored in registries; containers are created with docker run and exist only while running.How do I reduce Docker image size?
node:20-alpine is 50MB vs node:20 at 350MB). Combine RUN commands to reduce layers. Add a .dockerignore file to exclude node_modules, .git, and test files from the build context. Install only production dependencies (npm ci --omit=dev). These practices can reduce image size by 80-90%.Should I use Docker Compose or Kubernetes?
Format Your Docker Configs
Working with JSON config files, environment variables, or API responses in your containers? Format and validate them instantly with our free developer tools.