Skip to content

Docker — Basics

  • Image — read-only template (layers) with filesystem + metadata.
  • Container — running instance of an image (writable layer on top).
  • Dockerfile — recipe to build an image.
  • Registry — store images (Docker Hub, ECR, GCR, GHCR, GitLab, Harbor).
  • Layer — each instruction creates one. Layers cached + shared.
  • Volume — persistent data outside container layers.
  • Network — virtual network for container comms (bridge, host, overlay).
  • Compose — multi-container local dev (docker-compose.yml).
FROM node:20-alpine # base image
WORKDIR /app
# Cache layer: deps before code
COPY package*.json ./
RUN npm ci --omit=dev
# Then code (changes more often)
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node # non-root
HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:3000/healthz || exit 1
CMD ["node", "dist/server.js"]

Key directives:

  • FROM — base.
  • RUN — execute at build time → new layer.
  • COPY / ADD — files in.
  • WORKDIR — set CWD.
  • ENV / ARG — env vars (ARG only at build).
  • EXPOSE — documentation, not enforcement.
  • ENTRYPOINT vs CMD — entrypoint = the executable; cmd = default args.
  • HEALTHCHECK — container health probe.
  • USER — non-root.
FROM node:20 AS build
WORKDIR /src
COPY . .
RUN npm ci && npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /src/dist ./dist
COPY --from=build /src/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]

Smaller final image, no compilers, no source code.

  • Layers cache on each instruction.
  • Order matters: stable layers first (deps), volatile layers last (code). Otherwise every code change invalidates dep cache.
  • .dockerignore keeps junk out of build context.
  • scratch — empty (Go static binaries).
  • distroless — Google minimal (no shell, no package mgr).
  • alpine — tiny musl libc; some glibc tools fail.
  • debian-slim / ubuntu — full libc, larger.

For Node/Python: alpine or slim. For static Go binaries: scratch or distroless.

  • bridge (default) — internal subnet, NAT to host.
  • host — share host’s network. No isolation. Linux only.
  • none — no network.
  • overlay — multi-host (Swarm/K8s).
  • macvlan — give container its own MAC on LAN.

docker run -p 8080:80 — host:container port mapping.

  • Named volumedocker volume create x → managed by Docker.
  • Bind mount-v /host/path:/in/container.
  • tmpfs — RAM only.

Container fs is ephemeral. Persistent data → volume.

Terminal window
docker build -t myimg:latest .
docker images
docker run --rm -it myimg:latest sh
docker ps; docker ps -a
docker logs -f <id>
docker exec -it <id> sh
docker stop / start / rm <id>
docker rmi <image>
docker system prune -af --volumes # cleanup
docker tag myimg:latest registry/proj/myimg:1.2.3
docker push registry/proj/myimg:1.2.3
services:
app:
build: .
ports: ["3000:3000"]
environment:
- DATABASE_URL=postgres://x:x@db:5432/app
depends_on: { db: { condition: service_healthy } }
db:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: x }
volumes: [ pgdata:/var/lib/postgresql/data ]
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
volumes:
pgdata:

docker compose up -d, down, logs, exec.

  • Use BuildKit (default in Docker 23+): parallel, cache mounts.
  • RUN --mount=type=cache,target=/root/.npm npm ci — persistent cache.
  • --platform=linux/amd64,linux/arm64 for multi-arch.
  • Use buildx for multi-platform.
  • Container: shares host kernel, isolated user-space (cgroups + namespaces). Lightweight, fast start.
  • VM: own kernel, hardware-virtualized. Heavier.

Containers aren’t a strong security boundary — gVisor / Kata / Firecracker add VM-like isolation.

  • Running as root by default (uses host UID 0!).
  • Using :latest tag in production — non-reproducible.
  • Bloated final images with build tools.
  • Logging to file inside container (logs lost). Use stdout/stderr.
  • Storing state inside container without volume.
  • Not using .dockerignore.
  • Hard-coding secrets in image layers (visible via docker history).