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.
Discussion
Loading…