What is Docker?

Docker is an open-source platform that enables developers to build, package, and run applications in isolated environments called containers. Containers are lightweight, portable, and consistent — solving the classic "works on my machine" problem once and for all.

Unlike virtual machines, containers share the host OS kernel, making them far more efficient in terms of memory and startup time.

Core Concepts

  • Image — A read-only template for creating containers (think: class)
  • Container — A running instance of an image (think: object instance)
  • Dockerfile — Instructions to build a custom image
  • Registry — Storage for images (Docker Hub, Amazon ECR, Google GCR)
  • Volume — Persistent storage that survives container restarts
  • Network — Communication channels between containers

Writing Your First Dockerfile

# Start from an official base image
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files first (leverages layer caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy the rest of your source code
COPY . .

# Build the application
RUN npm run build

# Expose the port and start
EXPOSE 3000
USER node
CMD ["npm", "start"]

Tip: Copy package*.json before the rest of your source so Docker can cache the npm install layer. This dramatically speeds up rebuilds when only source files change.

Essential Docker Commands

# Build an image and tag it
docker build -t my-app:latest .

# Run a container in detached mode with port mapping
docker run -d -p 3000:3000 --name my-app my-app:latest

# View running containers
docker ps

# Stream logs in real time
docker logs my-app -f

# Open a shell inside a running container
docker exec -it my-app sh

# Stop and remove a container
docker stop my-app && docker rm my-app

# Remove unused images to reclaim disk space
docker image prune -f

Docker Compose for Multi-Service Applications

Real applications typically need multiple services (database, cache, app server). Docker Compose lets you define and run them together:

# docker-compose.yml
version: "3.8"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
# Start all services
docker compose up -d

# View logs from all services
docker compose logs -f

# Tear everything down (volumes too)
docker compose down -v

Multi-Stage Builds

Multi-stage builds dramatically reduce final image sizes by separating build dependencies from the runtime image:

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

# Stage 2: Production (only what's needed to run)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

A Next.js app might go from a 1.2 GB development image down to under 150 MB using this technique.

Best Practices

  • Never run containers as root in production — use USER node or similar
  • Use .dockerignore to exclude node_modules, .git, .env, and build output
  • Pin specific image versionsnode:20.11-alpine not node:latest
  • Use Alpine-based images for smaller attack surface and faster pulls
  • Scan images for vulnerabilities with docker scout cves my-app:latest
  • Keep secrets out of images — use environment variables or secrets managers, never COPY a .env file

Conclusion

Docker is an essential tool in modern software development. Once you understand the fundamentals, containerising everything from development environments to production deployments becomes second nature. The docker-compose.yml file becomes your project's infrastructure manifest — committed to version control alongside your code.