All topics
DevOps · Learning hub

Docker notes for developers

Master Docker with a curated set of 4 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore DevOps notes
Docker

Images & Containers

Images & Containers Core Concepts Image — read-only template (layers) built from a Dockerfile Container — running instance of an image with a writable layer on

Images & Containers

Core Concepts

  • Image — read-only template (layers) built from a Dockerfile

  • Container — running instance of an image with a writable layer on top

  • Registry — storage for images (Docker Hub, GHCR, ECR, GCR)

  • Volume — persistent storage managed by Docker, survives container removal

  • Network — bridge/host/overlay — how containers communicate

Essential Commands

# Images
docker pull nginx:1.25                  # pull from registry
docker images                           # list local images
docker image rm nginx:1.25             # remove image
docker image prune -f                  # remove dangling images
docker build -t myapp:1.0 .            # build from Dockerfile in current dir
docker tag myapp:1.0 ghcr.io/user/myapp:1.0  # tag for push
docker push ghcr.io/user/myapp:1.0     # push to registry

# Containers — run
docker run nginx                        # run (foreground)
docker run -d nginx                     # detached (background)
docker run -d -p 8080:80 nginx         # map host:container port
docker run -d --name web nginx         # named container
docker run -d -e NODE_ENV=prod myapp   # env variable
docker run -d -v mydata:/data nginx    # named volume mount
docker run -d -v $(pwd)/config:/app/config:ro nginx  # bind mount read-only
docker run --rm alpine echo "hello"    # auto-remove on exit

# Containers — manage
docker ps                              # running containers
docker ps -a                           # all (including stopped)
docker stop web                        # graceful stop (SIGTERM then SIGKILL)
docker kill web                        # immediate SIGKILL
docker start web                       # restart stopped container
docker restart web                     # stop + start
docker rm web                          # remove stopped container
docker rm -f web                       # force remove running container

# Debugging
docker logs web                        # view stdout/stderr
docker logs -f web                     # follow logs
docker logs --tail 100 web            # last 100 lines
docker exec -it web bash              # interactive shell
docker exec web cat /etc/hosts        # run single command
docker inspect web                    # full JSON metadata
docker stats                           # live resource usage
docker top web                         # processes inside container

Volumes & Bind Mounts

# Named volumes (Docker managed, best for prod data)
docker volume create mydata
docker volume ls
docker volume inspect mydata
docker volume rm mydata
docker volume prune         # remove all unused volumes

# Run with named volume
docker run -d -v mydata:/var/lib/postgresql/data postgres

# Bind mount (host path, good for development)
docker run -d -v /host/path:/container/path myapp
docker run -d -v $(pwd):/app node     # mount current dir

# tmpfs mount (in-memory, not persisted)
docker run -d --tmpfs /tmp myapp

Cleanup

# Remove everything unused (safe)
docker system prune

# Remove everything including volumes
docker system prune --volumes

# Remove all stopped containers
docker container prune

# Remove all unused images
docker image prune -a
Docker

Dockerfile Best Practices

Dockerfile Best Practices Instruction Reference # Base image — use specific tags, never :latest in prod FROM node:20-alpine # Metadata LABEL maintainer="team@co

Dockerfile Best Practices

Instruction Reference

# Base image — use specific tags, never :latest in prod
FROM node:20-alpine

# Metadata
LABEL maintainer="team@company.com"

# Set working directory (creates if missing)
WORKDIR /app

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

# ARG — build-time variable (not in final image env)
ARG BUILD_VERSION=latest

# Copy files — use .dockerignore to exclude node_modules etc.
COPY package*.json ./          # copy package files first (cache layer)
COPY . .                       # then source code

# Run commands — chain with && to minimize layers
RUN apt-get update && apt-get install -y     curl     git  && rm -rf /var/lib/apt/lists/*    # clean up in same layer!

# Install dependencies
RUN npm ci --only=production

# Expose port (documentation only — does NOT publish)
EXPOSE 3000

# Non-root user (security best practice)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --retries=3   CMD curl -f http://localhost:3000/health || exit 1

# Volume mount point
VOLUME ["/app/data"]

# CMD — default command (overridable at runtime)
CMD ["node", "server.js"]

# ENTRYPOINT — fixed executable (CMD becomes its args)
ENTRYPOINT ["node"]
CMD ["server.js"]

Multi-Stage Builds

Use multiple FROM stages to keep the final image small — build tools and dev dependencies don't end up in production.

# 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 image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist   # only copy built output
EXPOSE 3000
CMD ["node", "dist/server.js"]

# Build a specific stage
# docker build --target builder -t myapp:builder .

Next.js Dockerfile Example

FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

.dockerignore

node_modules
.git
.gitignore
*.md
.env*
.next
dist
coverage
.nyc_output
*.log

Layer Caching Tips

  • Copy package.json before source code — deps only reinstall when package.json changes

  • Chain apt-get update && apt-get install && rm -rf in a single RUN

  • Order instructions from least to most frequently changed

  • Use --mount=type=cache in BuildKit to cache package manager downloads

Docker

Docker Compose & Networking

Docker Compose & Networking docker-compose.yml Structure services: app: build: context: . dockerfile: Dockerfile target: runner # multi-stage target args: BUILD

Docker Compose & Networking

docker-compose.yml Structure

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner         # multi-stage target
      args:
        BUILD_VERSION: "1.0"
    image: myapp:latest
    container_name: myapp
    restart: unless-stopped  # no | always | on-failure | unless-stopped
    ports:
      - "3000:3000"          # host:container
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://user:pass@db:5432/mydb
    env_file:
      - .env.production
    volumes:
      - ./uploads:/app/uploads       # bind mount
      - app_data:/app/data           # named volume
    networks:
      - app_net
    depends_on:
      db:
        condition: service_healthy   # wait for healthcheck
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass secretpassword --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - app_net

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    depends_on:
      - app
    networks:
      - app_net

volumes:
  pg_data:
  redis_data:
  app_data:

networks:
  app_net:
    driver: bridge

Compose Commands

# Start / stop
docker compose up -d                   # start all services detached
docker compose up -d app db            # start specific services
docker compose down                    # stop and remove containers
docker compose down -v                 # also remove volumes
docker compose stop                    # stop without removing
docker compose start                   # start stopped containers
docker compose restart app             # restart specific service

# Build
docker compose build                   # build all images
docker compose build app               # build specific service
docker compose build --no-cache app    # ignore cache

# Logs
docker compose logs                    # all logs
docker compose logs -f app             # follow app logs
docker compose logs --tail 50 db       # last 50 lines

# Scale
docker compose up -d --scale app=3    # run 3 app containers

# Exec
docker compose exec app bash           # shell into service
docker compose exec db psql -U user mydb

# Run one-off command
docker compose run --rm app node migrate.js

# Status
docker compose ps                      # service status
docker compose top                     # processes in services

# Config
docker compose config                  # validate + show merged config

Networking

  • Bridge (default) — isolated network, containers communicate by service name as DNS

  • Host — container shares host network stack (Linux only, fastest)

  • None — completely isolated, no networking

  • Overlay — multi-host (Docker Swarm / Kubernetes)

# Service discovery in Compose — use service name as hostname
# From 'app' container, connect to db: postgres://user:pass@db:5432/mydb
# From 'app' container, connect to redis: redis://redis:6379

# Network commands
docker network ls
docker network inspect app_app_net
docker network create mynet
docker network connect mynet container1
docker network disconnect mynet container1

Override Files

# docker-compose.override.yml is merged automatically in dev
# Use for dev-specific settings (volume mounts, debug ports)

# Explicitly specify files:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Common pattern:
# docker-compose.yml          — base config
# docker-compose.override.yml — dev overrides (auto-loaded)
# docker-compose.prod.yml     — prod overrides (explicit)
Docker

Interview Questions

Docker Interview Questions Q: What is the difference between an image and a container? An image is a static, read-only blueprint made of layers. A container is

Docker Interview Questions

Q: What is the difference between an image and a container?

An image is a static, read-only blueprint made of layers. A container is a running instance of that image with an additional writable layer. Multiple containers can run from the same image simultaneously without interfering — they each have their own writable layer.

Q: What is the difference between CMD and ENTRYPOINT?

ENTRYPOINT sets the fixed executable that always runs. CMD provides default arguments that can be overridden at docker run time. When both are set, CMD becomes the default arguments to ENTRYPOINT. Using ENTRYPOINT ["node"] + CMD ["server.js"] means docker run myapp scripts/migrate.js replaces just the CMD.

Q: What are Docker layers and why do they matter?

Each instruction in a Dockerfile creates a new layer that is cached. If nothing in a layer changes, Docker reuses the cached layer — this makes builds fast. Layers are also shared between images using the same base, saving disk space. For this reason, order instructions from stable (infrequently changing) to volatile, and chain related commands in one RUN to minimize layer count.

Q: How do you reduce Docker image size?

  • Use Alpine or Distroless base images

  • Use multi-stage builds — exclude build tools from final image

  • Chain RUN commands to avoid extra layers

  • Clean up in the same RUN layer (rm -rf /var/lib/apt/lists/*)

  • Use .dockerignore to exclude unnecessary files

  • Only install production dependencies (npm ci --only=production)

Q: Named volume vs bind mount — when to use each?

Named volumes are managed by Docker, portable, and are the right choice for persistent data (databases, uploads) in production. Bind mounts map a specific host path and are ideal for development (live code reloading) but are host-dependent. tmpfs mounts are in-memory and are useful for sensitive data or high-performance scratch space.

Q: How does Docker Compose service discovery work?

Docker Compose creates a shared network for all services. Each service is reachable by its service name as a hostname (via Docker's embedded DNS). So if your service is named "db", you connect to it at postgres://user:pass@db:5432/mydb from any other container in the same network.

Q: What is Docker multi-stage build?

A Dockerfile can have multiple FROM instructions, each creating a separate stage. You can COPY --from=<stage> to pull only specific artifacts from a previous stage into the final image. This is used to compile code in a stage with build tools, then copy only the binary/output into a minimal runtime image.

Q: How do containers differ from VMs?

VMs virtualize hardware and run a full OS (hypervisor required, heavy, slow boot). Containers share the host OS kernel using Linux namespaces and cgroups for isolation — they are lightweight, start in milliseconds, and use far less memory. Containers are not as isolated as VMs (same kernel), but the trade-off is acceptable for most use cases.

Keep your Docker knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever