Interview Prep

Interview: Advanced Docker

Multi-stage builds, BuildKit, security, registries, CI/CD, and hardening. Read learning notes.

Advanced Docker

Production-grade image building, security, and deployment patterns.

What are multi-stage builds and why use them?

A Dockerfile with multiple FROM statements defines separate build stages. Each stage has its own filesystem; you copy only the artifacts you need into the final stage with COPY --from=<stage>. This lets you use a full SDK image for compilation while shipping a minimal runtime image - excluding compilers, source code, and dev dependencies. The result is smaller images, a reduced attack surface, and faster deploys.

How do you target a specific build stage?

Name stages with FROM golang:1.22 AS builder and reference them by name or index. Build only up to a stage with docker build --target builder -t myapp:builder . - useful for CI steps that need the compiled binary but not the final slim image. The default output is the last stage in the Dockerfile unless --target specifies otherwise.

What is BuildKit and what advantages does it provide?
  • Parallel layer builds - independent stages and instructions run concurrently.
  • Build cache mounts - RUN --mount=type=cache persists package manager caches across builds without bloating the image.
  • Secret mounts - RUN --mount=type=secret passes credentials at build time without writing them into any layer.
  • SSH mounts - clone private repos during build without embedding keys.
  • Improved Dockerfile frontend - heredoc RUN <<EOF syntax, COPY --link for independent layers.

Enable with DOCKER_BUILDKIT=1 (default in modern Docker) or docker buildx build.

How do you pass build-time secrets safely with BuildKit?

Never put secrets in ARG or ENV - they end up in image history. Instead, use a secret mount:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

Pass the secret at build time: docker build --secret id=npm_token,src=.npmrc .. The secret file is mounted only for that RUN instruction and is never committed to a layer.

How do you secure Docker images and the supply chain?
  • Scan images with Docker Scout, Trivy, or Grype to find CVEs in OS packages and application dependencies.
  • Use minimal base images (distroless, Alpine, slim variants) to reduce the attack surface.
  • Pin base images by digest and rebuild regularly to pick up security patches.
  • Run as non-root - set USER in the Dockerfile and avoid --privileged.
  • Generate SBOMs (Software Bill of Materials) for auditability: docker scout sbom or syft.
  • Sign images with Sigstore/cosign or Docker Content Trust for tamper verification.
What is the difference between a public registry, a private registry, and a mirror?
  • Public registry (Docker Hub, GHCR) - hosts images accessible to anyone (or scoped to an org).
  • Private registry (registry:2 self-hosted, ECR, ACR, GCR) - stores proprietary images behind authentication; required for internal apps and compliance.
  • Registry mirror / pull-through cache - proxies requests to an upstream registry, caching layers locally to speed pulls and reduce rate-limit hits.

Authenticate with docker login; CI pipelines use short-lived tokens or OIDC rather than long-lived passwords.

How do you integrate Docker into a CI/CD pipeline effectively?
  • Use multi-stage builds so CI produces the same artifact shape as local dev.
  • Enable BuildKit cache in CI (--cache-from / --cache-to with a registry or GitHub Actions cache) to avoid rebuilding every layer from scratch.
  • Scan the built image and fail the pipeline on critical CVEs.
  • Push by digest and deploy the digest, not a mutable tag.
  • Use buildx for multi-platform images (--platform linux/amd64,linux/arm64) when deploying to heterogeneous clusters.
  • Keep Dockerfiles in the repo; treat image build as a first-class build artifact alongside unit tests.
What is rootless Docker and what are its trade-offs?

Rootless mode runs the Docker daemon and containers as an unprivileged user, using user namespaces to map container root (UID 0) to a high, unprivileged host UID. This limits the blast radius if a container escape occurs - the attacker does not get host root. Trade-offs: some features are unavailable or restricted (e.g., certain cgroup configurations, --net=host on some setups), and overlay networking uses slirp4netns which can be slower than rootful bridge networking. Ideal for developer workstations and security-sensitive environments.

How do you set CPU and memory limits on containers?
  • docker run --memory 512m --cpus 1.5 sets hard memory and CPU limits via cgroups v2.
  • --memory-reservation sets a soft limit; the kernel reclaims under pressure.
  • --cpu-shares (relative weight) and --cpuset-cpus (pin to specific cores) offer finer control.
  • In Compose: deploy.resources.limits or the shorthand mem_limit / cpus fields.

Without limits, a single runaway container can exhaust host memory and trigger the OOM killer, affecting every workload on the node.

What happens when a container exceeds its memory limit?

When a container's memory usage hits the cgroup limit, the Linux OOM killer terminates processes inside that container's cgroup - typically the main process, which causes the container to exit (often with code 137 = 128 + SIGKILL 9). Docker does not swap by default. Monitor with docker stats and set limits based on observed usage plus headroom. Java and other runtimes need their internal heap tuned to fit within the container limit, not the host's total RAM.

What is docker buildx and when do you need it over plain docker build?

Buildx is Docker's extended build CLI built on BuildKit. Use it when you need multi-platform builds (arm64 + amd64 from a single CI job), remote builders (build on a more powerful machine or in the cloud), advanced cache exporters (registry-backed, inline, or local), or compose build integration with bake files. For simple single-platform local builds, docker build (which uses BuildKit under the hood) is sufficient.

How do you reduce image size beyond multi-stage builds?
  • Choose distroless or Alpine/slim bases instead of full OS images.
  • Combine RUN instructions to reduce layer count and remove package manager caches in the same layer (apt-get clean && rm -rf /var/lib/apt/lists/*).
  • Use .dockerignore aggressively - every copied file can invalidate cache and bloat context upload.
  • Install only production dependencies (npm ci --omit=dev, pip install --no-cache-dir).
  • Squash is rarely needed if multi-stage builds are done correctly; prefer stage separation over docker build --squash.