Docker knowledge is relevant when a role builds images, runs containerized development environments, or operates container workloads. It is not a universal requirement, so align preparation with the job description and interview scope.
This guide covers 35 Docker questions from image and container fundamentals through Dockerfile behavior, BuildKit caching, Compose, storage, networking, and security. The examples use Node.js 24 LTS where a runtime is needed; production builds should also control platform, provenance, and base-image updates.
Table of Contents
- Docker Fundamentals Questions
- Image and Container Questions
- Dockerfile CMD vs ENTRYPOINT Questions
- Layer Caching Questions
- Multi-Stage Build Questions
- COPY vs ADD Questions
- Docker Compose Questions
- Volume and Bind Mount Questions
- Docker Networking Questions
- Environment Variables and Secrets Questions
- Docker Commands Questions
- Dockerfile Best Practices Questions
Docker Fundamentals Questions
Understanding Docker fundamentals is essential for any developer interview involving containerization.
What is Docker and why is it important for modern development?
Docker provides tools for building OCI-compatible images and creating containers from them. On a Linux Docker Engine, Linux containers use host-kernel isolation primitives such as namespaces and cgroups. Docker Desktop normally runs Linux containers inside a Linux VM, and Windows containers follow a different kernel model.
An image can reduce environment drift by packaging a filesystem and runtime configuration, but it does not package the host kernel or every external dependency. Behavior can still vary with CPU architecture, kernel/runtime settings, mounted data, secrets, networks, resource limits, and downstream services.
Key benefits:
- Repeatable packaging: Promote a tested image digest through environments
- Isolation controls: Separate processes, filesystems, networks, and resources according to configuration
- Distribution: Store and transfer content-addressed image layers through registries
- Operational lifecycle: Create, replace, inspect, limit, and remove container instances
Containers are not automatically a strong security boundary. Daemon access, privileged flags, host mounts, capabilities, the kernel, and the image supply chain all affect risk.
What is the difference between Docker and a virtual machine?
Containers isolate processes through an OS kernel and container runtime; virtual machines expose virtual hardware to a guest OS with its own kernel. Docker Desktop itself commonly combines both: Linux containers run inside a Linux VM on macOS and Windows.
Do not memorize fixed size or startup numbers. A large container image can exceed a small VM image, and application initialization can dominate startup. Compare the workload's measured density, isolation boundary, kernel requirements, startup path, operations, and threat model.
| Aspect | Docker Container | Virtual Machine |
|---|---|---|
| Kernel | Uses a compatible host or VM kernel | Guest has its own kernel |
| Unit | Isolated process tree plus image/config | Virtual hardware plus guest OS |
| Isolation boundary | Depends on namespaces, capabilities, daemon and runtime configuration | Hypervisor boundary, also configuration-dependent |
| Compatibility | Image platform must match the runtime environment | Guest OS must match virtualized hardware support |
| Cost/startup | Often lower, but measure the actual workload | Often higher baseline, but not a fixed rule |
Image and Container Questions
The distinction between images and containers is foundational to understanding Docker.
What is the difference between a Docker image and a container?
This is often the first Docker question in an interview. Many candidates give vague answers like "an image is a template and a container is running," but interviewers expect more depth.
An image contains read-only filesystem layers and configuration such as its default command. It can be built from a Dockerfile and distributed through a registry. Image content addressed by a digest is immutable; tags are mutable. A digest improves input reproducibility but does not make runtime behavior independent of platform, configuration, mounts, or external systems.
A container is an instance created from an image and can be created, running, paused, or stopped. It normally has a writable layer above the image. That layer is coupled to the container lifecycle, so durable data belongs in a designed volume, bind mount, or external service; logs may instead go to the configured logging path.
# Image: the blueprint
docker pull node:24-bookworm-slim
# Container: running instance
docker run -d --name my-app node:24-bookworm-slim
# Multiple containers from same image
docker run -d --name my-app-2 node:24-bookworm-slim
docker run -d --name my-app-3 node:24-bookworm-slimWhat are Docker image layers and how do they work?
Docker images use content-addressed layers plus configuration. Filesystem-changing build steps commonly contribute layers, while metadata instructions do not each create a meaningful filesystem layer. Shared blobs can reduce storage and transfer when the image store already has them.
Filesystem-changing build steps contribute content to read-only image layers, which are presented together as one filesystem. Configuration-only instructions update image metadata rather than each adding a distinct filesystem payload.
Layer creation example:
FROM node:24-bookworm-slim # Base image layers
WORKDIR /app # Layer 2: sets working directory
COPY package.json ./ # Layer 3: copies package.json
RUN npm install # Layer 4: installs dependencies
COPY . . # Layer 5: copies source codeWhat is the Docker build context?
The build context is the set of files the builder may access for COPY, ADD, and bind mounts. In docker build ., the dot selects the local directory as context. With BuildKit, transfer and caching are more optimized than the old model of blindly resending every byte, but context scope still affects performance, cache checks, and exposure.
Keep the context intentionally narrow. Sources normally must come from the configured context, named contexts, another build stage, or an image; ../ is not a way to escape a local context. Excluding secrets is defense in depth, not permission to pass them as ordinary build inputs.
The .dockerignore file excludes files from the build context, similar to .gitignore. This speeds up builds and prevents accidentally including sensitive files or large directories like node_modules.
# .dockerignore
node_modules
.git
*.log
.env
dist
Dockerfile CMD vs ENTRYPOINT Questions
Understanding the difference between CMD and ENTRYPOINT is crucial for writing flexible Dockerfiles.
What is the difference between CMD and ENTRYPOINT?
This trips up many developers because both seem to "run a command." The key difference lies in how they handle arguments passed at runtime.
CMD provides a default command or default arguments. Arguments after the image name replace CMD; when an exec-form ENTRYPOINT also exists, those arguments are appended to that entrypoint instead.
FROM node:24-bookworm-slim
CMD ["npm", "start"]# Uses CMD default
docker run my-app
# Overrides CMD entirely - runs npm test instead
docker run my-app npm testENTRYPOINT defines the image's default executable. In the common exec-form combination, arguments passed after the image name replace CMD and are appended to it. Users can still replace the entrypoint explicitly with docker run --entrypoint.
FROM node:24-bookworm-slim
ENTRYPOINT ["node"]
CMD ["app.js"]# Runs: node app.js
docker run my-app
# Runs: node server.js (CMD overridden, ENTRYPOINT stays)
docker run my-app server.jsWhat is the best practice for combining CMD and ENTRYPOINT?
Use CMD alone when replacing the whole command is a normal use case. Use an exec-form ENTRYPOINT plus exec-form CMD when the image behaves like an executable with overridable default arguments. This is a design choice, not a universal best practice.
ENTRYPOINT ["node"]
CMD ["app.js"]This lets users change the script file while keeping node as the process. For production applications, you might use an entrypoint script that handles initialization:
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["start"]An entrypoint script can validate configuration or perform idempotent local initialization, but it must finish with exec "$@" so signals reach the application. Avoid having every replica race to run database migrations; use a separately coordinated migration job when rollout semantics require it.
What is the difference between shell form and exec form?
Dockerfile instructions like CMD, ENTRYPOINT, and RUN can be written in two forms. The exec form uses JSON array syntax and executes commands directly. The shell form uses a plain string and runs commands through a shell.
Exec form avoids an implicit shell, preserves argument boundaries, and normally makes the executable PID 1 so it can receive container signals directly. The application still needs correct PID 1 signal and child-reaping behavior, or a suitable init process.
# Exec form (preferred) - runs node directly as PID 1
CMD ["node", "app.js"]
# Shell form - runs /bin/sh -c "node app.js"
CMD node app.jsShell form is useful for shell expansion and pipelines. For a long-running shell-form entrypoint, use exec in the shell or wrapper so the application replaces it; otherwise shutdown signals may stop at the shell.
Layer Caching Questions
Layer caching is a practical build skill; seniority is not determined by one optimization.
How does Docker layer caching work?
BuildKit records cache results for build instructions and their relevant inputs. On a later build, it can reuse a matching result rather than execute the step again. The exact checksum inputs differ by instruction; for example, file metadata participates in COPY/ADD, while a RUN cache is not refreshed merely because a remote package repository changed.
A changed dependency can invalidate downstream steps, but BuildKit's graph, multi-stage builds, cache mounts, and COPY --link make “everything after this line always rebuilds” too crude as a universal rule. Changing a build secret's contents also does not itself invalidate the cache.
How do you optimize a Dockerfile for layer caching?
Order expensive stable inputs before frequently changed inputs when dependencies allow it, then measure. BuildKit cache mounts can preserve package-manager caches without copying them into the final image.
A common mistake is copying all source files before installing dependencies:
# Bad: cache busted on every code change
FROM node:24-bookworm-slim
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]Every code change copies new files, invalidating the npm install cache. The optimized approach separates dependency installation from source code:
# Optimized: dependencies cached separately
FROM node:24-bookworm-slim
WORKDIR /app
# Dependencies change less often than code
COPY package*.json ./
RUN npm ci
# Code changes frequently - only this layer rebuilds
COPY . .
CMD ["npm", "start"]Now source-only changes can reuse the dependency step. A changed base, build argument, relevant file metadata, cache policy, or explicit invalidation can still cause it to run again.
Why should you use npm ci instead of npm install in Docker?
The npm ci command is designed for automated environments. It performs a frozen install from a compatible lockfile instead of updating dependency resolution, which removes one major source of drift. Full reproducibility still depends on the base image, CPU platform, registry content, lifecycle scripts, native builds, and external inputs.
Unlike npm install, npm ci removes existing node_modules before installing, ensuring a clean state. It also fails if the lock file is out of sync with package.json, catching dependency issues early.
# Reproducible, clean dependency installation
COPY package*.json ./
RUN npm ciMulti-Stage Build Questions
Multi-stage builds are a useful way to separate build-time and runtime concerns; they are not sufficient by themselves to make an image production-ready.
What are multi-stage builds and why use them?
Multi-stage builds use multiple FROM statements in a single Dockerfile, allowing you to use full build toolchains without shipping them in your final image. Each FROM instruction starts a new stage, and you can copy artifacts from earlier stages into later ones.
Single-stage builds can retain build tools and files that the runtime does not need. Measure rather than quoting a universal image size:
# Single-stage example
FROM node:24-bookworm
WORKDIR /app
COPY . .
RUN npm ci && npm run build
CMD ["node", "dist/index.js"]Multi-stage builds separate the build environment from the runtime environment:
# Stage 1: Build
FROM node:24-bookworm AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:24-bookworm-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]How do you create minimal production images with multi-stage builds?
Install only runtime dependencies in the final stage rather than copying a development dependency tree. Keep build and runtime stages platform-compatible when native modules are involved:
# Stage 1: Build
FROM node:24-bookworm AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime dependencies only
FROM node:24-bookworm-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]This keeps unselected build-stage files out of the runtime stage. Verify that the artifact does not contain source maps, embedded secrets, unused packages, or incompatible native modules, and compare the resulting size and vulnerability report.
What are the security benefits of multi-stage builds?
Multi-stage builds can reduce available tools and packages if the final stage copies only required artifacts. That can reduce attack surface, but fewer packages do not prove that the remaining packages or application are secure.
Do not treat compilation as confidentiality: JavaScript bundles, binaries, source maps, licenses, and debug data can reveal source or behavior. Prevent secrets from entering the context and use BuildKit secret/SSH mounts for build-time credentials; deleting a secret in a later layer does not erase it from earlier content.
COPY vs ADD Questions
Understanding when to use COPY versus ADD demonstrates attention to best practices.
What is the difference between COPY and ADD in a Dockerfile?
COPY does exactly what it says—copies files from the build context to the image. It's straightforward and predictable.
COPY package.json ./
COPY src/ ./src/ADD additionally accepts remote URLs and Git repositories and has archive-handling behavior and options such as --checksum for HTTP(S) sources. Its behavior depends on source type and flags, so use it intentionally.
# Extracts the tar into /app
ADD app.tar.gz /app/
# Remote input with an expected SHA-256 digest
ADD --checksum=sha256:<expected-digest> https://example.com/file.txt /app/When should you use COPY versus ADD?
Use COPY for ordinary local or cross-stage copying. Use ADD when its remote, Git, or archive semantics are the feature you deliberately want. This is more precise than saying ADD is always wrong.
For any remote input, control the source and integrity. ADD --checksum can verify supported HTTP(S) inputs. A RUN download can also verify integrity, but use a trusted TLS source, pin a real digest, fail closed, and remove temporary files:
# Better than ADD for downloads
ARG FILE_SHA256
RUN curl --fail --show-error --location https://example.com/file.txt -o /tmp/file.txt \
&& echo "${FILE_SHA256} /tmp/file.txt" | sha256sum -c - \
&& install -m 0644 /tmp/file.txt /app/file.txt \
&& rm /tmp/file.txtAn ARG checksum is configuration, not a secret. Validate where that expected value comes from; a checksum fetched from the same compromised channel adds little protection.
Docker Compose Questions
Docker Compose is one option for defining and running multi-container applications, especially in development, testing, and single-host workflows.
What is Docker Compose and when would you use it?
Docker Compose defines multi-container applications in a single YAML file. Instead of running multiple docker run commands with complex options, you declare your entire application stack and start it with one command.
Compose can make a local dependency topology repeatable, but it does not automatically mirror a production orchestrator's identity, networking, storage, scheduling, or failure behavior. Use it where its application model fits.
# compose.yaml
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/myapp
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:18-bookworm
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:8-bookworm
volumes:
postgres_data:How does service discovery work in Docker Compose?
Docker Compose creates a default network for each project, and services can reach each other by service name. In the example above, the app service connects to the database using db:5432 because Compose's internal DNS resolves db to the database container's IP.
This automatic service discovery eliminates hardcoded IP addresses. You reference other services by their name in the compose file, and Docker handles the networking.
# Start all services
docker compose up -d
# View logs
docker compose logs -f app
# Rebuild after Dockerfile changes
docker compose up -d --build
# Clean shutdown
docker compose down
# Remove volumes too
docker compose down --volumes # Deletes the project's named volumes and their dataWhat is the purpose of depends_on in Docker Compose?
The depends_on option controls startup order—Compose starts dependencies before the dependent service. However, it only waits for containers to start, not for applications inside them to be ready.
Short-form depends_on: [db] orders container startup but does not wait for PostgreSQL readiness. Long form with condition: service_healthy waits for the dependency's healthcheck before creating the dependent service:
services:
db:
image: postgres:18-bookworm
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthyReadiness at startup is not a permanent availability guarantee. The application still needs bounded retries, timeouts, and failure handling when a dependency later becomes unavailable. The top-level Compose version field is obsolete; current Compose uses the latest schema it supports.
Volume and Bind Mount Questions
Storage choice determines ownership, lifecycle, host coupling, backup, and access risk.
What is the difference between volumes and bind mounts?
Both persist data outside the container's writable layer, but they work differently and serve different purposes.
Bind mounts link a host path directly to a container path. The container sees the host filesystem at that location. This is ideal for development because file changes on the host immediately appear in the container.
docker run -v /host/path:/container/path myapp
# or explicitly
docker run --mount type=bind,source=/host/path,target=/container/path myappVolumes are persistent data stores managed by the Docker daemon. A local volume is still host-scoped; it does not move between hosts automatically. Drivers can provide other backends. Use supported mount, backup, restore, and migration workflows rather than editing Docker's internal storage path directly.
docker volume create mydata
docker run -v mydata:/container/path myapp
# or explicitly
docker run --mount type=volume,source=mydata,target=/container/path myappWhen should you use volumes versus bind mounts?
Choose from data ownership, need for direct host access, host/path coupling, permissions, backup/restore, performance, security policy, and the target orchestrator. Development versus production is only one input.
| Use Case | Choice |
|---|---|
| Host-edited source tree | Bind mount is a common option |
| Docker-managed local persistence | Named volume |
| Ephemeral sensitive scratch data | Consider tmpfs |
| Production database | Use storage supported and backed up for the target platform; a local volume alone is not HA |
| CI cache | Runner-native cache or a volume, based on lifecycle and trust boundaries |
What is the anonymous volume pattern in Docker Compose?
When using bind mounts for development, you often need to prevent the host's node_modules from overwriting the container's. The anonymous volume pattern preserves the container's directory:
volumes:
- .:/app # Bind mount for code sync
- /app/node_modules # Anonymous volume preserves container's modulesThe second mount creates an anonymous volume that obscures /app/node_modules from the bind mount. It can avoid host/container binary incompatibility, but its lifecycle is easy to forget and old dependencies can survive image rebuilds. Prefer an explicit development workflow—such as a named dependency volume with documented reset behavior, Compose Watch, or installing dependencies in the container—and treat docker compose down --volumes as destructive to volume data.
Docker Networking Questions
Container networking is fundamental to multi-service architectures.
How do containers communicate with each other?
Docker provides several network modes, each suited to different scenarios. The default bridge network allows containers to communicate using IP addresses, but custom bridge networks enable DNS-based service discovery.
When containers join the same custom network, they can reach each other by container name. Docker's embedded DNS server resolves names to container IP addresses.
# Create custom network
docker network create mynet
# Containers can reach each other by name
docker run -d --name api --network mynet myapi
docker run -d --name web --network mynet myweb
# From 'web', can reach: http://api:3000What are the different Docker network types?
Docker supports several network drivers, each providing different isolation and connectivity characteristics.
Bridge is the default driver for standalone Linux containers. User-defined bridge networks provide scoped connectivity and embedded DNS by name; the legacy default bridge has different name-resolution behavior.
Host shares the host network namespace where supported, so port publishing is ignored and port conflicts apply. Docker Desktop implements host networking through its VM environment, and rootless/platform behavior has caveats. Use it only for a measured requirement with an understood exposure model.
docker run --network host myapp
# App on port 3000 is directly on host:3000None gives the container only loopback networking. It does not provide complete security isolation: mounts, capabilities, the daemon, kernel, IPC/PID options, and other resources still matter.
Overlay enables communication across multiple Docker hosts, used in Swarm mode for distributed applications.
How does port mapping work in Docker?
Port publishing maps a host address/port to a container port. Omitting the host address commonly publishes on all host interfaces, which may expose the service beyond the local machine depending on firewall and routing. Bind explicitly to loopback for a local-only development service.
# Map host port 8080 to container port 3000
docker run -p 8080:3000 myapp
# Map to specific interface
docker run -p 127.0.0.1:8080:3000 myapp
# Random host port
docker run -p 3000 myappA common troubleshooting issue: if you can't connect to a mapped port, verify the application inside the container binds to 0.0.0.0, not localhost. Binding to localhost inside a container means the app only accepts connections from within that container.
Environment Variables and Secrets Questions
Configuration management is critical for containerized applications.
How do you pass environment variables to Docker containers?
Environment variables can be set at build time in the Dockerfile or at runtime when starting containers. Runtime variables are more flexible and should be used for configuration that varies between environments.
# Default in Dockerfile
ENV NODE_ENV=production
ENV PORT=3000# Override at runtime
docker run -e NODE_ENV=development -e PORT=8080 myapp
# From file
docker run --env-file .env myappIn Docker Compose, environment variables can come from the compose file, an env_file, or the host environment:
services:
app:
environment:
- NODE_ENV=development
env_file:
- .envHow do you handle secrets securely in Docker?
Do not bake secrets into images, ordinary build arguments, or image environment variables. Deleting a file in a later layer does not remove it from an earlier layer. BuildKit secret or SSH mounts expose build credentials only to the required build step.
At runtime, use the target platform's secret mechanism and restrict who can inspect the container, process environment, mounted files, logs, and daemon API. Environment injection is sometimes the platform's supported interface, but it has different exposure and rotation properties from a file or workload identity. A local .env file must be excluded from source control, protected, and populated with disposable development values.
Docker Swarm secrets:
docker secret create db_password ./db_password.txtCompose file-backed secret for local development:
services:
app:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtDo not commit that source file. In production, prefer short-lived credentials or workload identity and a managed secret service where available. Kubernetes Secrets require separate RBAC, encryption-at-rest, delivery, and rotation design; the name alone does not make values safe.
What is the ARG instruction and how does it differ from ENV?
ARG defines build variables scoped to the Dockerfile stage; it is not automatically present in the runtime container environment. However, values may affect cache, provenance attestations, history, generated files, or later ENV instructions, so ARG is not a secret channel. ENV persists in image configuration and becomes a default environment value for containers.
# ARG: available to the build stage, not a secret
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-bookworm-slim
# ENV: persists in image and container
ENV NODE_ENV=productionUse ARG for non-secret build configuration such as a version selector. Use runtime configuration rather than baking environment-specific values into the image. You can pass an ARG value at build time:
docker build --build-arg NODE_VERSION=24 -t myapp .Docker Commands Questions
Interviewers often test your practical Docker command knowledge.
What are the essential Docker commands for managing images?
Understanding the image lifecycle commands demonstrates practical Docker experience. Images are built from Dockerfiles, can be tagged for organization, pushed to registries for sharing, and removed when no longer needed.
# Build an image with tag
docker build -t myapp:v1 .
# List local images
docker images
# Remove an image
docker rmi myapp:v1
# Remove unused images
docker image prune
# Pull from registry
docker pull node:24-bookworm-slim
# Push to registry
docker push myregistry/myapp:v1Inspect what prune will remove and understand cache and re-pull cost before confirming it. Tags are mutable; record or verify the digest when promoting a release.
What are the essential Docker commands for managing containers?
Container management involves the full lifecycle: creating, starting, stopping, inspecting, and removing containers. The distinction between create, start, and run is important—run combines create and start.
# Run container (create + start)
docker run -d -p 3000:3000 --name app myapp
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop container
docker stop app
# Remove container
docker rm app
# View logs
docker logs app
# Follow logs in real-time
docker logs -f appHow do you debug a running Docker container?
docker exec can run a command in a container that already contains that executable. Minimal or distroless images may have no shell; use logs, docker inspect, health output, metrics, an ephemeral debug workflow such as docker debug, or a deliberately separate diagnostic image instead of adding tools to production solely for shell access.
# Shell into container
docker exec -it app sh
# Run specific command
docker exec app cat /app/config.json
# View full container details
docker inspect app
# Monitor resource usage
docker stats
# View processes in container
docker top appCommon troubleshooting scenarios:
- Container exits immediately? Check
docker logsand ensure the process runs in foreground - Can't connect to port? Verify port mapping with
docker ps, check if app binds to0.0.0.0 - Build slow? Check
.dockerignore, optimize layer order - Image too large? Inspect layers and copied artifacts; use multi-stage builds and a compatible minimal trusted base
Dockerfile Best Practices Questions
Following best practices demonstrates production experience.
What are the most important Dockerfile best practices?
Production-ready images require threat modeling, supported inputs, correct runtime behavior, provenance, testing, and an update process. These are starting points, not a complete checklist.
| Practice | Example |
|---|---|
| Use trusted, controlled base images | Supported version plus digest for immutable promotion, with an update process |
| Run as non-root user | USER node |
| Use multi-stage builds | Separate build and runtime stages |
| Order for cache efficiency | Dependencies before source code |
| Keep package install and cleanup atomic | Avoid stale package indexes and leftover caches |
| Use .dockerignore | Exclude node_modules, .git, logs |
| Prefer COPY over ADD | Unless you need tar extraction |
| Set explicit WORKDIR | WORKDIR /app |
| Define health semantics where consumed | Include the probe tool and avoid secrets or expensive dependency cascades |
Why should you avoid using the latest tag?
latest is an ordinary mutable tag, not a promise of compatibility or even the newest release in a scheme you expect. It also hides the intended major/runtime line.
A specific version tag communicates more intent, but tags can still be repointed. A digest gives immutable image content. Teams must balance immutable promotion and auditability with an automated, reviewed process that refreshes digests for security fixes.
# Bad: unpredictable version
FROM node:latest
# Better intent, but the tag remains mutable
FROM node:24-bookworm-slim
# For immutable promotion, append a verified registry digest:
# FROM node:24-bookworm-slim@sha256:<verified-digest>How do you run containers as non-root users?
Many images default to UID 0 inside the container. Container root is constrained by namespaces and capabilities in a default setup, but it increases impact when combined with a runtime vulnerability, dangerous capability, daemon access, or writable host mount. Non-root is defense in depth, not a claim that escape automatically grants host root in every configuration.
Best practice is to create and switch to a non-root user. Many official images include a non-root user you can use:
FROM node:24-bookworm-slim
WORKDIR /app
COPY --chown=node:node . .
# Switch to non-root user
USER node
CMD ["node", "app.js"]--chown sets ownership in the image. Also use explicit writable paths, drop unnecessary capabilities, avoid privileged mode and Docker-socket mounts, consider read-only root filesystems/rootless mode, and test the runtime UID against mounted-volume permissions.
How do you implement health checks in Docker?
A Dockerfile HEALTHCHECK runs a command inside the container and records starting, healthy, or unhealthy status. The command must exist in the final image and should test a meaningful local invariant without leaking secrets or overloading downstream dependencies.
FROM node:24-bookworm-slim
WORKDIR /app
COPY . .
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "/app/healthcheck.js"]
CMD ["node", "app.js"]Docker records the status and emits health events; what happens next depends on the consumer. Compose can gate startup with service_healthy, and Swarm can use container health. Kubernetes does not import a Dockerfile HEALTHCHECK; define startup, readiness, and liveness probes in the Pod specification. A readiness failure should remove traffic, while liveness should detect a state that restart can actually repair.
Quick Reference
| Topic | Key Point |
|---|---|
| Image vs Container | Image has read-only layers/config; a created container has lifecycle state and usually a writable layer |
| CMD vs ENTRYPOINT | CMD supplies replaceable defaults; ENTRYPOINT supplies a replaceable default executable |
| Build cache | Match relevant inputs, isolate stable work, use cache mounts, and refresh deliberately |
| Multi-stage | Copy selected runtime artifacts from builder to final stage, then inspect the result |
| COPY vs ADD | Use COPY for ordinary copying; use ADD deliberately for its extra source/archive behavior |
| Volumes vs bind mounts | Choose by ownership, host coupling, access, backup, performance, and target platform |
| Networking | Custom bridge networks enable DNS by container name |
| Secrets | Keep them out of image/ARG/ENV build inputs; use build mounts and runtime secret/identity controls |
Frequently Asked Questions
What is the difference between a Docker image and a container?
An image packages read-only filesystem layers plus configuration. A container is an instance created from an image; it may be created, running, paused, or stopped and usually has its own writable layer. Persistent state should use an explicit mount because the writable layer disappears with the container.
What is the difference between CMD and ENTRYPOINT in a Dockerfile?
CMD supplies default command or arguments and is replaced by command-line arguments. ENTRYPOINT supplies the default executable, with command-line arguments appended in the common exec-form combination, but docker run --entrypoint can replace it. Use CMD alone for an easily replaceable command or ENTRYPOINT plus CMD when the image behaves like an executable.
How do multi-stage builds reduce Docker image size?
Multi-stage builds use multiple FROM stages and copy selected runtime artifacts from a builder into the final stage. Build tools and unused files remain outside the final image only if you do not copy them back. The result may be smaller and simpler, but size and security still depend on the chosen base and copied artifacts.
What is Docker layer caching and how do you optimize for it?
BuildKit can reuse cached results when an instruction and its relevant inputs match a cache record. Put expensive stable work before frequently changing inputs, copy lockfiles before application source, and use cache mounts where useful. Cache does not refresh packages automatically, and secret contents do not invalidate cache.
What is the difference between COPY and ADD in a Dockerfile?
COPY copies files or directories from a build context, stage, named context, or image. ADD also supports remote URLs, Git repositories, and archive handling. Prefer COPY for ordinary copying; use ADD deliberately for a feature you need and apply supported checksum or source controls to remote inputs.
How do Docker volumes differ from bind mounts?
Volumes are persistent data stores managed by the Docker daemon, while bind mounts expose a chosen host path directly. A local volume is still stored on one Docker host unless a driver provides other behavior. Choose by ownership, host access, backup, permissions, performance, security, and orchestration requirements rather than a universal development-versus-production rule.
Official Sources
- Dockerfile reference
- Docker build best practices
- Docker build cache invalidation
- Docker build secrets
- Docker Compose application model
- Docker Compose startup order
- Docker storage and volumes
- Docker network drivers
- Docker port publishing
- Docker Engine security
- Docker Debug
- Kubernetes probes
- Node.js releases
Related Articles
- Complete DevOps Engineer Interview Guide - comprehensive preparation guide for DevOps interviews
- Node.js Advanced Interview Guide - Cluster mode, worker threads, and production patterns
- System Design Interview Guide - Architecture decisions where containers play a key role
- Kubernetes Interview Guide - Container orchestration at scale
- Linux Commands Interview Guide - Essential commands for containers and servers
- CI/CD & GitHub Actions Interview Guide - Building and deploying Docker images in pipelines
