Docker Essentials for Developers

"Works on my machine" used to be a punchline. Docker makes it a guarantee.

Core Concepts

Image: A read-only template with your application and dependencies.

Container: A running instance of an image.

Dockerfile: Instructions to build an image.

Registry: Storage for images (Docker Hub, GitHub Container Registry).

Your First Dockerfile

For a Node.js application:

# Start from a base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy package files first (for caching)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Expose the port
EXPOSE 3000

# Run the application
CMD ["node", "server.js"]

Build and run:

docker build -t myapp .
docker run -p 3000:3000 myapp

Layer Caching

Docker caches layers. Order matters:

# Bad: reinstalls deps on any code change
COPY . .
RUN npm install

# Good: deps only reinstall when package.json changes
COPY package*.json ./
RUN npm install
COPY . .

Multi-Stage Builds

Keep production images small:

# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: A fraction of the size.

Docker Compose

Define multi-container applications:

# docker-compose.yml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_PASSWORD=secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Run everything:

docker compose up -d

Essential Commands

# Images
docker build -t name:tag .
docker images
docker rmi image_name

# Containers
docker run -d -p 8080:80 nginx
docker ps                    # Running containers
docker ps -a                 # All containers
docker stop container_id
docker rm container_id

# Logs and debugging
docker logs container_id
docker logs -f container_id  # Follow logs
docker exec -it container_id sh

# Cleanup
docker system prune          # Remove unused data
docker system prune -a       # Remove everything unused

Volumes: Persistent Data

Data inside containers is ephemeral. Use volumes:

# Named volume
docker run -v mydata:/app/data myapp

# Bind mount (development)
docker run -v $(pwd):/app myapp

In Compose:

services:
  app:
    volumes:
      - ./src:/app/src        # Bind mount for hot reload
      - node_modules:/app/node_modules  # Named volume

volumes:
  node_modules:

Environment Variables

# Default value in Dockerfile
ENV NODE_ENV=production

Override at runtime:

docker run -e NODE_ENV=development myapp

Or use .env files with Compose:

services:
  app:
    env_file:
      - .env

Health Checks

Let Docker know if your app is healthy:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
    CMD curl -f http://localhost:3000/health || exit 1

Security Best Practices

Don't run as root

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Use specific image tags

# Bad: unpredictable
FROM node:latest

# Good: reproducible
FROM node:20.10-alpine3.18

Don't store secrets in images

# Bad
ENV API_KEY=secret123

# Good: pass at runtime
docker run -e API_KEY=$API_KEY myapp

.dockerignore

Keep images lean:

node_modules
.git
.env
*.md
Dockerfile
docker-compose.yml
.dockerignore
coverage
.nyc_output

Development Workflow

# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm run dev
docker compose -f docker-compose.dev.yml up

Debugging Containers

# Shell into running container
docker exec -it container_id sh

# Inspect container details
docker inspect container_id

# View resource usage
docker stats

Quick Reference

TaskCommand
Build imagedocker build -t name .
Run containerdocker run -d -p 8080:80 name
View logsdocker logs -f container
Shell accessdocker exec -it container sh
Stop alldocker stop $(docker ps -q)
Clean updocker system prune -a

Docker transforms "it works on my machine" into "it works everywhere."