Container Images: Building, Optimizing, and Distributing

Learn how Docker container images work, layer caching strategies, image optimization techniques, and how to publish your own images to container registries.

published: reading time: 25 min read author: GeekWorkBench updated: May 17, 2026
Quick Summary

A container image carries an application and its runtime dependencies from development through deployment. This guide shows how Docker layers and cache invalidation affect builds, then covers dependency ordering, multi-stage builds, and `.dockerignore`. It walks through registries, Trivy scans, digest pinning, and signature verification, including failure cases such as registry authentication and digest mismatches. These practices help teams produce repeatable images and check what they deploy.

Container Images: Building, Optimizing, and Distributing

Introduction

A container image packages an application and its runtime dependencies into a versioned artifact that can move consistently through development, staging, and production. Its layers affect build speed, image size, and how much code or configuration is exposed in the final runtime.

This guide explains layer behavior, efficient Dockerfiles, multi-stage builds, registries, vulnerability scanning, and image signing. It also covers common build and deployment failures and ways to reduce their impact.

When to Use Container Images

Use an image when an application needs a repeatable runtime with specific libraries or operating-system packages, or when the same artifact should move through development, staging, and production. Pinning and promoting an image by digest also makes rollbacks refer to exact content. Keep environment-specific configuration, secrets, and persistent application data outside the image; inject configuration at deployment and use a volume or external store for data that must survive container replacement.

How Container Images Work

A container image is not a single binary blob. It is a stack of read-only layers, each representing a discrete set of filesystem changes from the layer below it.

When Docker builds an image from a Dockerfile, filesystem-changing instructions such as RUN, COPY, and ADD contribute filesystem changes to the image’s layers. FROM selects a base image, while instructions such as ENV set image configuration rather than adding filesystem contents.

The Union Filesystem

Docker uses a union filesystem (OverlayFS on most Linux systems) to combine these layers into a single coherent view. When a container starts, Docker adds a writable layer on top. Writes go to this top layer, reads check the top layer first and fall through to lower layers.

+------------------+
|  Writable Layer |  (container layer)
+------------------+
|  Layer 3         |  (COPY app /app)
+------------------+
|  Layer 2         |  (RUN npm ci)
+------------------+
|  Layer 1         |  (COPY package*.json ./)
+------------------+
|  Base Image      |  (FROM node:20-alpine)
+------------------+

The writable layer stores changes made while a container runs. If a file from a read-only image layer is modified, the storage driver uses copy-on-write behavior; the exact storage details depend on the driver.

Image Metadata

Beyond the filesystem layers, an image includes metadata:

  • Config: The image configuration blob, containing the default command, environment variables, exposed ports, and working directory.
  • Manifest: The manifest lists the layers and their sizes, plus the config blob reference. This is what the container runtime downloads.
  • Architecture/OS: The platform the image is built for. You cannot run an linux/arm64 image on a linux/amd64 host without QEMU emulation.

Writing Efficient Dockerfiles

The way you write your Dockerfile has a massive impact on build time and image size. Here are the patterns that matter most.

Order Instructions to Maximize Cache Reuse

Docker caches each layer. When you rebuild, Docker checks if it has already built a layer with identical instruction and content. If so, it skips rebuilding and reuses the cached layer.

The catch: if a build step misses the cache, later steps that depend on it must run again.

This means you should order instructions from least to most frequently changing:

# Start with dependencies that change rarely
FROM node:20-alpine

WORKDIR /app

# Copy only package files first (this cache survives most code changes)
COPY package*.json ./

# Install dependencies (cached until package.json changes)
RUN npm ci --only=production

# Copy application code last (changes most frequently)
COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

If you copy everything before running npm ci, every code change blows away the dependency cache and forces a full reinstall.

Minimize Layer Count

Each RUN, COPY, and ADD instruction creates a layer. While layers are deduplicated at runtime, they still add overhead during build, pull, and push operations.

Combine related commands using shell chaining:

# Anti-pattern: separate RUN instructions
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Better: single RUN with cleanup
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

The apt-get clean and rm -rf /var/lib/apt/lists/* remove the package manager cache within the same layer. If you put cleanup in a separate RUN instruction, it becomes a separate layer that does not help image size.

Use Specific Base Image Versions

Avoid latest. Pin to specific versions or tags:

# Anti-pattern: moving target
FROM node:latest

# Better: pinned version
FROM node:20.11.1-alpine3.19

The latest tag can move without warning. Version tags are easier to review, but they can also be repointed; pin a digest when you need an immutable base image reference.

Do Not Copy Unnecessary Files

Your build context includes everything in the directory where you run docker build. Large contexts mean slow uploads to the Docker daemon, especially for remote Docker hosts.

Use a .dockerignore file to exclude files:

.git
node_modules
dist
*.log
.env
.env.*
coverage
README.md

Be explicit about what you need:

# Anti-pattern: copying everything
COPY . .

# Better: explicit paths
COPY package*.json ./
COPY src ./src
COPY public ./public

Multi-Stage Builds for Production

Multi-stage builds solve a specific problem: your build process needs tools and dependencies that your runtime does not. Compiler toolchains, test frameworks, and development libraries bloat the final image and expand the attack surface.

Multi-stage builds use multiple FROM statements. Each FROM starts a fresh stage. You copy artifacts from one stage to another, leaving behind everything the runtime does not need.

When to Use Multi-Stage Builds / When Not To

Use multi-stage builds when:

  • Your application requires compilation or build steps (compiled languages, TypeScript, bundlers)
  • You need to keep production images minimal and free of build artifacts
  • Security and attack surface reduction are priorities
  • You deploy frequently and image size affects deployment speed
  • You need to run as non-root but build as root

Skip multi-stage builds when:

  • Your application is a simple interpreted script
  • You use a pre-built image with all dependencies already included
  • Build time is not a concern and image size does not matter

Two-Stage Build Flow

flowchart TB
    subgraph Builder["Builder Stage (e.g., golang:1.22-alpine)"]
        B1[Copy Source Code]
        B2[Install Dependencies]
        B3[Compile Binary]
        B4[Build Output Ready]
        B1 --> B2 --> B3 --> B4
    end

    subgraph Runtime["Runtime Stage (e.g., alpine:3.19)"]
        R1[Minimal Base Image]
        R2[Copy Binary from Builder]
        R3[Set Non-Root User]
        R4[Configure Entrypoint]
        R1 --> R2 --> R3 --> R4
    end

    Builder --> |COPY --from=builder| Runtime
    Runtime --> FinalImage["Final Image<br/>(~15MB for Go)"]

Go Application Example

# Build stage
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Install build dependencies
RUN apk add --no-cache git

# Copy source and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .

# Runtime stage
FROM alpine:3.19

WORKDIR /app

# Install runtime dependencies only
RUN apk add --no-cache ca-certificates && \
    addgroup -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# Copy binary from builder stage
COPY --from=builder /app/myapp .

# Switch to non-root user
USER appuser

ENTRYPOINT ["./myapp"]

The final image contains only the binary, CA certificates for HTTPS, and the Alpine base. No Go compiler, no git, no source code.

Node.js Application Example

# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production

WORKDIR /app

# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built artifacts
COPY --from=builder /app/dist ./dist

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001 -G nodejs

USER nodejs

EXPOSE 3000

CMD ["node", "dist/server.js"]

Python Application Example

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /install /usr/local

# Copy application code
COPY . .

# Create non-root user
RUN useradd --create-home appuser
USER appuser

CMD ["python", "server.py"]

Image Registries and Distribution

An image registry is a server that stores and serves Docker images. The public Docker Hub is the default registry, but most organizations run private registries for their own images.

Understanding Registry URLs

Image names follow the pattern: registry.example.com/namespace/repository:tag

  • docker.io/library/nginx: Official nginx image on Docker Hub (library is implicit for official images)
  • ghcr.io/myorg/myapp:1.0.0: My container image on GitHub Container Registry
  • gcr.io/project-id/myapp:latest: My app on Google Container Registry

If you do not specify a registry, Docker assumes Docker Hub. If you do not specify a tag, Docker assumes latest.

Pushing Images to a Registry

# Tag your image for the registry
docker tag myapp:1.0.0 registry.example.com/myorg/myapp:1.0.0

# Login to the registry (if private)
docker login registry.example.com

# Push
docker push registry.example.com/myorg/myapp:1.0.0

Pulling Images Efficiently

# Pull specific version
docker pull nginx:1.25.3-alpine

# Pull and run in one command
docker run -d --name nginx nginx:1.25.3-alpine

When you pull an image, Docker downloads only the layers your host does not already have. If another team member already pulled nginx:1.25.3-alpine, subsequent pulls are essentially instant because all layers are cached locally.

Running a Private Registry

For local development or enterprise use, you can run your own registry:

docker run -d -p 5000:5000 \
    --name registry \
    -v registry_data:/var/lib/registry \
    registry:2

Now you can push and pull from localhost:5000:

docker tag myapp:1.0.0 localhost:5000/myapp:1.0.0
docker push localhost:5000/myapp:1.0.0

For production private registries, consider managed services: AWS ECR, Google Artifact Registry, Azure Container Registry, or self-hosted solutions like Harbor.

Security Scanning and Image Signing

Images can contain vulnerabilities. The packages you install, the base image itself, anything copied into the image is a potential attack vector.

Scanning Images with Trivy

Trivy is an open-source vulnerability scanner for containers:

# Scan an image
trivy image nginx:1.25.3-alpine

# Scan and fail on HIGH or CRITICAL vulnerabilities
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:1.0.0

In CI/CD pipelines, you integrate Trivy to block deployments with known vulnerabilities:

# GitHub Actions example
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:1.0.0
    format: sarif
    output: trivy-results.sarif
    severity: HIGH,CRITICAL

- name: Upload scan results
  uses: github/codeql-action/upload-sarif@v2
  with:
    sarif_file: trivy-results.sarif

Image Signing with Cosign

Image signing lets a verifier check that an image digest was signed by a particular key or identity. A valid signature alone does not prove the build process or source code was trustworthy, so deployment policy should also check the expected signer and release provenance.

Cosign signs images using keys stored in your infrastructure:

# Generate a key pair
cosign generate-key-pair

# Sign an image
cosign sign --key cosign.key registry.example.com/myorg/myapp:1.0.0

# Verify before deployment
cosign verify --key cosign.pub registry.example.com/myorg/myapp:1.0.0

Cosign stores signature data in the registry associated with the image. A deployment policy can verify the signature and expected signer before accepting an image; signing makes unauthorized changes detectable, but does not prevent changes or guarantee that the build itself was trustworthy.

Observability Checklist

  • Track build duration, cache-hit rate, build-context size, and pushed bytes; rising build time often points to invalidated dependency layers or an oversized context.
  • Record compressed and unpacked image size by digest and release so unexpected growth is visible before it slows pulls or consumes node storage.
  • Measure registry push/pull latency and failure rates, then compare pull time with container start and readiness time by image and architecture.
  • Track vulnerability findings by severity, age, and base image; review scan coverage and time to rebuild after a critical fix becomes available.
  • Alert on signature or digest verification failures and failed deployments by image version; these can signal a broken release path or an artifact that differs from the one approved.

Production Failure Scenarios

Container image pipelines fail in ways that are not always obvious. Here are the most common issues and how to diagnose them.

Layer Cache Corruption

If Docker reports a layer digest or checksum error, investigate the local image store and the registry or network path. Image layers are content-addressed, so Docker can detect content that does not match its expected digest.

Symptoms: Docker reports a layer digest or checksum mismatch during a pull or build, or repeated pulls fail for the same layer.

Diagnosis:

# Inspect the layer digests recorded for a local image
docker image inspect myapp:1.0.0 --format '{{json .RootFS.Layers}}'

# Inspect the registry digests recorded for the local image
docker image inspect myapp:1.0.0 --format '{{json .RepoDigests}}'

Mitigation: retry the pull from the trusted registry, compare the image digest with the expected release digest, and inspect Docker daemon and registry logs if the error persists. docker build --no-cache bypasses build cache for a rebuild; it does not repair a corrupted image already stored locally.

Registry Authentication Failures

Private registries require authentication. Tokens expire, credentials rotate, and network policies can block access.

Symptoms: “401 Unauthorized” errors on pull, “Image not found” when the image exists.

Diagnosis:

# Check registry connectivity
docker login <registry-url>

# Verify credentials stored
cat ~/.docker/config.json

Mitigation: Use robot accounts with least privilege, set up credential helpers (docker-credential-*), and implement token refresh logic in CI/CD.

Image Digest Mismatch

A digest mismatch means the fetched content does not match the requested digest. Check for transfer, proxy, or registry storage problems, and confirm that the release references the intended platform-specific image or multi-platform index digest. A mutable tag can point to new content without changing the digest of the original image.

Symptoms: the digest reported by the registry or deployment does not match the digest recorded for the release.

Mitigation: Pin images by digest in production (image@sha256:...), verify signatures against an expected signer, and generate an SBOM to track image contents. A digest identifies content; it does not establish that the content is trusted.

Layer Caching Strategies

Efficient cache use is the difference between builds that take 2 minutes and builds that take 20.

Dependency Caching

The key insight: dependencies change less frequently than application code. Put them early in the Dockerfile, after the base image.

FROM node:20-alpine

WORKDIR /app

# Dependencies change weekly at most
COPY package*.json ./
RUN npm ci

# Code changes multiple times per day
COPY src ./src
COPY public ./public

CMD ["node", "src/server.js"]

Now when only src/ changes, Docker reuses the npm ci layer.

Build Arguments and Cache

Build arguments (ARG) do not persist in the image, but they still affect caching because they change the instruction hash.

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

RUN echo "Building for ${NODE_ENV}"

If you change NODE_ENV between builds, the cache breaks at that layer. Sometimes this is what you want, sometimes not.

Cache Invalidation on COPY

When you copy files, Docker hashes the files, not just the instruction. If the file contents are identical, the layer is cached.

This means copying identical files does not invalidate cache:

COPY package*.json ./
RUN npm ci
COPY . .  # Only invalidates if files differ from last build

But if package.json changes, both it and the subsequent RUN layer invalidate.

Optimizing Image Size

Small images pull faster, deploy faster, and have smaller attack surfaces.

Choose the Right Base Image

Image Size Use Case
ubuntu:24.04 ~77MB When you need a full OS
debian:bookworm-slim ~30MB Smaller Debian, still compatible
alpine:3.19 ~7MB Minimal, musl libc
distroless/nodejs ~15MB Minimal with Node, no shell
scratch 0MB No OS, binary only

Alpine uses musl libc instead of glibc. Most Node.js, Python, and Go programs work fine on Alpine. Some software (notably anything with native modules that compile against glibc) does not.

Trade-off Table: Build Strategies

Aspect Monolithic Multi-Stage Builder Pattern
Image Size Large (includes build tools) Small (runtime only) Small
Build Speed Fast (no copy overhead) Moderate Fast
Complexity Simple Moderate High
Cache Efficiency Poor (any change rebuilds all) Good Excellent
Security Surface Large Minimal Minimal
Reproducibility Variable High High
Best For Debugging, interpreted languages Compiled languages, production Complex builds, monorepos

Remove Documentation and Man Pages

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/* \
    /usr/share/doc/* \
    /usr/share/man/* \
    /var/log/* \
    /tmp/*

Use .dockerignore Aggressively

.git
.gitignore
*.md
docs
tests
.dockerignore
Dockerfile
docker-compose.yml
.env*

Every file you exclude is not uploaded to the Docker daemon, not stored in a layer, not pulled by users.

Anti-Patterns

These patterns cause problems in production. Avoid them.

Copying Entire Context

# Anti-pattern: copying everything
COPY . .

This includes build artifacts, temporary files, and potentially secrets. Always use explicit paths:

COPY package*.json ./
COPY src ./src
COPY public ./public

Not Cleaning Up in the Same Layer

# Anti-pattern: cleanup in separate layer
RUN apt-get update && apt-get install -y curl
RUN apt-get clean

The cleanup does not reduce image size because it happens in a different layer. Combine everything:

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

Using :latest Tag

# Anti-pattern: moving target
FROM node:latest

The latest tag can move without warning. Version tags are easier to review, but they can also be repointed; pin a digest when you need an immutable base image reference. Always pin versions:

FROM node:20.11.1-alpine3.19

Not Using .dockerignore

Without .dockerignore, your build context includes everything:

.git
node_modules
dist
*.log
.env
.env.*
coverage

This slows down builds and can leak secrets or large files into images.

Quick Recap Checklist

  • Order Dockerfile instructions to reuse stable dependency layers.
  • Use multi-stage builds to keep compilers and build tools out of runtime images.
  • Pin base images and copy only the files the application needs.
  • Scan images for vulnerabilities and sign verified artifacts.
  • Deploy by digest and confirm registry access and pull behavior.

Interview Questions

1. How does Docker layer caching work, and why is it important for build performance?

Expected answer points:

  • Filesystem-changing instructions contribute image layers; Docker can reuse cached results when the instruction and its relevant inputs are unchanged
  • When a build step misses the cache, later dependent steps must run again
  • Order instructions from least to most frequently changing for optimal cache reuse
  • Copy package files before source code to leverage dependency caching
  • BuildKit enables parallel layer building and better cache management
2. What are multi-stage builds, and how do they improve container security?

Expected answer points:

  • Multi-stage builds use multiple FROM statements to separate build and runtime environments
  • Final production image contains only runtime artifacts, not build tools
  • Build dependencies (compilers, test frameworks) never enter the production image
  • Reduces attack surface by excluding potential vulnerabilities from build tools
  • Results in smaller images that pull and deploy faster
  • Enables use of minimal base images (Alpine, distroless) while using full build environments
3. What is the difference between COPY and ADD instructions in a Dockerfile?

Expected answer points:

  • COPY is the preferred instruction for basic file copying
  • ADD can extract local tar archives and download files from remote URLs
  • Remote downloads can change over time; verify and pin their content when reproducibility matters
  • ADD auto-extraction can cause unexpected behavior with large archives
  • Recommendation: Use COPY unless you specifically need ADD tar extraction or URL features
4. How do you optimize Dockerfile layer caching?

Expected answer points:

  • Order instructions from least to most frequently changing
  • Copy dependency files first (package.json, requirements.txt), then source code
  • Combine related RUN commands to reduce layer count
  • Clean up in the same layer that creates artifacts (apt-get clean && rm -rf /var/lib/apt/lists/*)
  • Use --chown on COPY to avoid cache busting when ownership changes
  • Use mount caches for package managers to persist cache across builds
5. What are the security best practices for writing Dockerfiles?

Expected answer points:

  • Use specific base image versions, never latest
  • Run as non-root user with USER instruction
  • Use minimal base images (Alpine, distroless) to reduce attack surface
  • Clean up in the same layer that creates artifacts
  • Avoid secrets in build arguments (they get baked into image layers)
  • Use COPY --chown for correct file ownership
  • Add HEALTHCHECK for container health monitoring
6. How does BuildKit improve Docker builds?

Expected answer points:

  • BuildKit enables parallel layer processing for faster builds
  • Uses mount caches for package managers (npm, pip) to persist cache across builds
  • Provides better caching with content-addressable storage
  • Supports inline cache for registry caching
  • Builds are more efficient with lazy pulling of layers
  • Enabled with DOCKER_BUILDKIT=1 or docker buildx
7. What is the purpose of .dockerignore file?

Expected answer points:

  • .dockerignore excludes files from the build context sent to Docker daemon
  • Without it, all files in directory are uploaded, even if not needed
  • Excludes .git, node_modules, .env files, build artifacts, and documentation
  • Speeds up builds and prevents secrets or large files from entering images
  • Also reduces image size by preventing unnecessary files from being copied
8. How do you choose the right base image for a production container?

Expected answer points:

  • Alpine (~7MB): Minimal, musl libc, good for most applications
  • Debian slim (~30MB): Smaller Debian, still glibc compatible
  • Distroless (~15MB): Minimal with runtime, no shell
  • Scratch (0MB): No OS, binary only; for advanced users
  • Consider: glibc vs musl compatibility, shell availability needs, size constraints
  • Never use latest; pin to specific version tags
9. What is image digest pinning and why is it important?

Expected answer points:

  • Image tags (like :1.0.0) can be overwritten with different content
  • Digest pinning (image@sha256:...) provides content-addressable verification
  • Digest identifies the exact image content, not just a matching tag
  • Prevents a moved tag from silently changing the content referenced by a digest-pinned deployment
  • Production deployments should pin by digest, not just tag
  • Cosign and other tools enable automated digest verification
10. How does image signing with Cosign work?

Expected answer points:

  • Cosign generates a key pair for signing images
  • Sign an image with: cosign sign --key cosign.key registry.example.com/myapp:1.0.0
  • Signature attestations are stored alongside the image in the registry
  • Verify before deployment with: cosign verify --key cosign.pub
  • Lets deployment policy verify an image signature and expected signer; signing does not prevent tampering
  • Integrates with container registries that support OCI artifact manifest
11. What are the trade-offs between monolithic and multi-stage builds?

Expected answer points:

  • Monolithic: simpler, faster initial build, but larger image size
  • Multi-stage: smaller images, better security, but more complex Dockerfile
  • Monolithic caches better for simple applications (few dependencies)
  • Multi-stage better for compiled languages requiring build steps
  • Multi-stage provides better cache efficiency with proper ordering
  • Tradeoff: simplicity vs image size and security
12. How do you handle sensitive data in container images?

Expected answer points:

  • Never bake secrets into image layers (they persist in read-only layers)
  • Use Docker secrets or external secrets managers (Vault, AWS Secrets Manager)
  • Mount secrets as files at runtime or use environment variables from secure sources
  • Build arguments can expose secrets during build if not careful
  • Use read-only filesystems and tmpfs for sensitive data at runtime
  • Rotate secrets regularly; images with old secrets should be rebuilt
13. What is the difference between ENTRYPOINT and CMD instructions?

Expected answer points:

  • ENTRYPOINT defines the main executable that always runs
  • CMD provides default arguments that can be overridden at runtime
  • Shell form (/bin/sh -c) does not handle signals properly
  • Exec form (JSON array) runs directly without shell, enabling proper signal handling
  • Use CMD for default arguments; ENTRYPOINT when container should run as specific executable
  • Both can be combined; CMD args are passed to ENTRYPOINT
14. How do you reduce Docker image size?

Expected answer points:

  • Use Alpine or distroless base images instead of full OS images
  • Use multi-stage builds to exclude build tools from production image
  • Combine RUN commands and clean up in same layer
  • Use .dockerignore to exclude unnecessary files from build context
  • Remove documentation and man pages in the same RUN layer
  • Consider using scratch image for static binaries
15. What is content-addressable storage in Docker?

Expected answer points:

  • Docker stores image layers by their SHA256 digest hash
  • Same layer content always produces same digest
  • Enables deduplication: identical layers across images share storage
  • Layer integrity is verified by comparing digests
  • Cache corruption detection through content hash verification
  • BuildKit extends this with content-addressable build cache
16. How does Trivy integrate into a CI/CD pipeline for image scanning?

Expected answer points:

  • Trivy is an open-source vulnerability scanner for containers
  • Scan in CI/CD with: trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:1.0.0
  • Exit code 1 causes pipeline failure when vulnerabilities found
  • Export results in SARIF format for GitHub code scanning
  • Integrates with admission controllers to block deployments with critical CVEs
  • Scan both base image and final image for complete coverage
17. What are the common anti-patterns when writing Dockerfiles?

Expected answer points:

  • Using latest tag instead of specific versions
  • Not using .dockerignore, resulting in large build contexts
  • Copying everything before installing dependencies (breaks cache)
  • Not cleaning up in the same layer that creates artifacts
  • Running containers as root instead of non-root user
  • Using ADD when COPY would suffice
  • Not using multi-stage builds for compiled languages
18. What is the difference between squash and multi-stage builds?

Expected answer points:

  • Squash merges all layers into a single layer; multi-stage creates multiple stages
  • Squash reduces layer count but keeps all build artifacts
  • Multi-stage separates build and runtime, excluding build artifacts entirely
  • Squash is useful for reducing complexity; multi-stage for security
  • Both achieve smaller final images through different mechanisms
  • Can be combined: multi-stage builds with squash on final stage
19. How do you handle layer cache invalidation efficiently?

Expected answer points:

  • A cache miss causes later dependent build steps to run again
  • Order from least to most frequently changing: base image, deps, source
  • Use specific COPY commands instead of COPY . . when possible
  • Avoid RUN commands that download variable content (latest packages)
  • Use --no-cache to force full rebuild when needed
  • Use mount caches for package managers to reduce network fetches
20. What are the considerations for multi-architecture Docker images?

Expected answer points:

  • Images can be built for multiple architectures (amd64, arm64) from same Dockerfile
  • Registry stores manifest list pointing to architecture-specific images
  • Docker automatically pulls correct variant for host architecture
  • Some software requires compilation differences between architectures
  • QEMU emulation allows running foreign architecture images
  • Use --platform flag to specify target architecture during build

Further Reading

Conclusion

Container images are built from layers, and understanding how those layers work lets you build faster and smaller images. Multi-stage builds separate what you build from what you run. Image registries distribute your builds, and security scanning plus signing keeps them safe.

The fundamentals covered here build directly on what you learned in the Docker Fundamentals guide. Next, explore how containers communicate through Docker Networking or how they persist data with Docker Volumes.

For production workloads, the next step is usually Kubernetes, but the image optimization principles apply anywhere containers run.

Category

Related Posts

Container Registry: Image Storage, Scanning, and Distribution

Set up and secure container registries for storing, scanning, and distributing container images across your CI/CD pipeline and clusters.

#containers #docker #registry

Docker Networking: From Bridge to Overlay

Master Docker's networking models—bridge, host, overlay, and macvlan—for connecting containers across hosts and distributed applications.

#docker #networking #containers

Docker Fundamentals

Learn Docker containerization fundamentals: images, containers, volumes, networking, and best practices for building and deploying applications.

#docker #containers #devops