iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Images Overview

A Docker image is a stack of read-only layers plus metadata (entrypoint, env, ports). Containers are running instances of an image. Smaller, well-cached layers mean faster builds and faster pulls.

Multi-stage, layer cache, slim base

EXAMPLE
# 1) Anatomy of an image — Dockerfile -> image -> container
# Each instruction creates a layer (or a cached hit if inputs unchanged)

# Dockerfile — naive (DON'T ship this)
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Problems: huge base image, copies node_modules + .git, rebuilds deps on every source change.

# 2) Optimized single-stage
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev          # cached when package*.json unchanged
COPY src/ ./src/
COPY public/ ./public/
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "src/server.js"]
# Order matters: copy lockfile and install BEFORE copying source.
# Edit a .js file? Only the source COPY layer rebuilds.

# 3) Multi-stage — best practice for compiled or bundled apps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
RUN npm ci --omit=dev
USER node                       # don't run as root
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Build deps (TypeScript, webpack) never reach the final image.

# 4) .dockerignore — equivalent of .gitignore for build context
node_modules
npm-debug.log
.git
.env*
coverage
.next
dist           # if build runs inside the image
.DS_Store
*.md
# Smaller build context → faster builds and no secret leaks.

# 5) Layer caching rules
#   • A layer's cache key = the instruction + inputs (files for COPY, ARG values for RUN)
#   • Change any input → that layer + every layer AFTER it rebuilds
#   • COPY package*.json BEFORE COPY . . — small file, rarely changes
#   • Lockfile change == reinstall deps; source change == relink dist

# 6) Choosing a base image
#   node:20             — Debian, ~1 GB, has build tools
#   node:20-slim        — Debian slim, ~150 MB, no build tools
#   node:20-alpine      — musl libc, ~50 MB, fast pulls, occasional native-module pain
#   gcr.io/distroless/  — no shell or package manager → fewer CVEs, harder to debug
#   scratch             — empty, for static Go/Rust binaries

# 7) Distroless / static binary (Go example)
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/server /server
USER nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
# Final image: a few MB, no shell, no package manager → minimal attack surface

# 8) Build commands
docker build -t myapp:1.2.3 .
docker build -t myapp:1.2.3 --build-arg NODE_ENV=production .
docker build -t myapp:latest -f Dockerfile.prod .
docker build --platform linux/amd64,linux/arm64 -t myapp:1.2.3 .   # multi-arch (buildx)

# 9) Inspect what you built
docker images
docker history myapp:1.2.3            # layer breakdown — find the fat layer
docker inspect myapp:1.2.3
docker run --rm -it myapp:1.2.3 sh    # poke around (won't work on distroless)

# 10) Image tags + digests
docker pull node:20-alpine            # may move when 20-alpine updates
docker pull node:20-alpine@sha256:abcd1234...    # exact, immutable
# Production: pin by digest for reproducibility.

# 11) Registries
docker login ghcr.io
docker tag myapp:1.2.3 ghcr.io/me/myapp:1.2.3
docker push ghcr.io/me/myapp:1.2.3
# Use semantic version tags + a moving 'latest' or environment tag.

# 12) Security checklist
#   ✓ Non-root user (USER directive)
#   ✓ No secrets in COPY / ARG — secrets via runtime env or mount
#   ✓ Smallest viable base image
#   ✓ Multi-stage so build tools don't ship to prod
#   ✓ Health check
#   ✓ Pin base by digest

# 13) Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD curl -fsS http://localhost:3000/health || exit 1
# Orchestrators (Kubernetes/Compose) use this signal.

# 14) Common bugs
#   • Copying . before npm ci → cache busts on every source edit
#   • Running as root → host escape on container break-out is easier
#   • COPY .env into image → secrets baked into layers (visible via docker history)
#   • Tagging :latest only → no rollback path
#   • Forgetting EXPOSE on a server → confusing default in compose
#   • Using FROM node when node:20-slim or alpine would do — gigabyte images for no reason

Why it matters

Layer order is everything for build speed: copy package*.json and install dependencies as their own layer, then copy source. Multi-stage builds keep compilers and lockfiles out of the runtime image, and pinning bases by digest gives you reproducible rebuilds for the next CVE response.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
docker images          # list local images
docker pull node:20    # download an image
Try it Yourself »

Discussion

Loading…