Skip to content
intermediate Phase 8 · Containers & Orchestration

Docker Fundamentals

Build Docker images with Dockerfiles, manage containers, configure volumes, and use Docker Compose for multi-container development.

1h
0 problems
Topic Progress 0%

Dockerfile Syntax and Best Practices

A Dockerfile is a text document containing instructions that Docker uses to assemble an image. Each instruction creates a layer in the image, and layers are cached to speed up subsequent builds.

The FROM instruction specifies the base image. Always use a specific tag rather than latest for reproducibility:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

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/index.js"]

Key best practices include ordering instructions from least to most frequently changing to maximize cache hits. Put COPY package.json before RUN npm install so dependencies are cached when only source code changes. Use .dockerignore to exclude node_modules, .git, and build artifacts from the build context.

The RUN instruction executes commands during image build. Chain related commands with && to reduce layers and clean up cache in the same layer:

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

Multi-stage builds let you use one image for building and a smaller image for the final runtime. A Go application might use golang:1.22 to compile but deploy with alpine or even scratch, reducing the final image from hundreds of megabytes to just a few.

Image Layers and Caching

Docker images are composed of read-only layers stacked on top of each other. Each Dockerfile instruction (FROM, RUN, COPY, etc.) creates a new layer. When you run a container, Docker adds a thin writable layer on top.

Understanding layers is critical for build performance. When you run docker build, Docker checks each instruction against its cache. If a layer hasn't changed, Docker reuses it. The first instruction that changes invalidates the cache for all subsequent instructions.

Consider this Dockerfile:

COPY . .
RUN npm install

Every source code change invalidates the COPY layer, which invalidates npm install. Reorder to:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Now npm install only re-runs when package files change.

Use docker history <image> to inspect layer sizes. Combine related RUN commands and clean up in the same layer:

RUN apt-get update && apt-get install -y build-essential && \
    make install && \
    apt-get purge -y build-essential && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

The --squash flag (experimental) can flatten all layers into one, reducing image size but losing layer caching benefits. For production, focus on multi-stage builds instead. Tag images with semantic versions and commit SHAs for traceability.

Docker Compose for Multi-Container Apps

Docker Compose defines and runs multi-container applications using a YAML file. It's ideal for development environments, testing, and local demonstrations of microservice architectures.

A basic docker-compose.yml for a web application with a database:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src

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

volumes:
  pgdata:

The depends_on with condition: service_healthy ensures the database is ready before the web app starts. Named volumes persist data across container restarts. The bind mount ./src:/app/src enables live reloading during development.

Use docker compose up -d to start in detached mode, docker compose logs -f web to follow logs, and docker compose down -v to tear down including volumes. Profiles let you start optional services: docker compose --profile debug up starts services tagged with the debug profile.

Container Networking and Volumes

Docker provides several network drivers for container communication. The default bridge network allows containers on the same host to communicate via IP addresses or container names. The host network removes network isolation, giving the container direct access to the host's network stack—useful for performance-sensitive applications.

Create custom bridge networks for better isolation and DNS resolution:

docker network create --driver bridge app-network
docker run --network app-network --name api myapi
# From another container on the same network:
# curl http://api:8080

Volumes are the preferred mechanism for persisting data. Bind mounts map a host path into the container and are useful for development. Named volumes are managed by Docker and work better for production:

# Named volume
docker volume create pgdata
docker run -v pgdata:/var/lib/postgresql/data postgres:16

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

The :ro flag makes the mount read-only inside the container. Docker volumes support drivers for cloud storage (EFS, EBS, S3), enabling containers to persist data beyond their lifecycle. Use docker volume ls, docker volume inspect, and docker volume prune to manage volumes.

Quiz

1. What is the primary benefit of multi-stage Docker builds?

Question 1 options

2. Why should you put `COPY package.json` before `RUN npm install` in a Dockerfile?

Question 2 options

3. Which Docker Compose directive ensures a service is ready before starting a dependent service?

Question 3 options

Flashcards

Question

What is a Docker image layer?

Answer

A read-only layer created by each Dockerfile instruction. Layers are stacked and cached, so unchanged layers are reused on subsequent builds.

Question

What is the difference between a bind mount and a named volume?

Answer

A bind mount maps a specific host path into the container (good for development). A named volume is managed by Docker and stored in a Docker-managed directory (better for production persistence).

Question

What does a .dockerignore file do?

Answer

Excludes files and directories from the build context sent to the Docker daemon, reducing build time and preventing sensitive files from being included in the image.

Question

What is the purpose of a healthcheck in Docker Compose?

Answer

Defines a command to verify that a container is functioning correctly. Other containers with depends_on: condition: service_healthy will wait until this check passes.

Revision Notes

Key Takeaways

  • 1. Use multi-stage builds to separate build-time and runtime dependencies for smaller images
  • 2. Order Dockerfile instructions from least to most frequently changing to maximize cache hits
  • 3. Docker Compose simplifies multi-container development environments with YAML configuration
  • 4. Named volumes persist data across container restarts; bind mounts are best for development
  • 5. Always use specific image tags and .dockerignore for reproducible, secure builds

Interview Tips

  • Explain how Docker layer caching works and how to optimize Dockerfile ordering
  • Describe when to use multi-stage builds with a real example from a past project
  • Discuss Docker networking modes (bridge, host, none) and when each is appropriate
  • Explain the difference between CMD and ENTRYPOINT in a Dockerfile

Cheat Sheet

Dockerfile: FROM (base) → WORKDIR → COPY deps → RUN install → COPY source → CMD. Multi-stage: builder stage compiles, final stage copies artifacts. Compose: services define containers, depends_on + healthcheck for ordering, volumes for persistence. Layers are cached—reorder instructions for cache hits.