Docker Interview Questions (Free Preview)
Free sample of 15 from 69 questions available
Docker Fundamentals
What is Docker and what problems does it solve?
The 30-Second Answer: Docker is a containerization platform that packages applications with their dependencies into isolated, portable containers. It solves the "works on my machine" problem by ensuring consistent environments across development, testing, and production, while being more lightweight than traditional virtual machines.
The 2-Minute Answer (If They Want More): Docker revolutionized application deployment by introducing container-based virtualization. Before Docker, developers faced numerous challenges: applications behaved differently across environments, setting up dependencies was time-consuming and error-prone, and resource utilization was inefficient with traditional VMs.
Docker solves these problems through containerization. Each container packages an application with everything it needs to run - libraries, system tools, code, and runtime - into a standardized unit. This ensures that if it works in development, it will work in production, because the environment is identical.
The platform addresses several key challenges: environment consistency (no more "works on my machine" issues), rapid deployment (containers start in seconds, not minutes), efficient resource utilization (containers share the host OS kernel, using far less resources than VMs), easy scaling (spin up multiple containers quickly), and simplified dependency management (all dependencies are bundled inside the container).
Docker has become the de facto standard for modern application deployment, particularly in microservices architectures and DevOps workflows. It's the foundation for container orchestration platforms like Kubernetes and is widely used in CI/CD pipelines.
Code Example:
# Example Dockerfile showing how Docker packages an application
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port and define startup command
EXPOSE 3000
CMD ["node", "server.js"]
# Build and run the containerized application
docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
# The app now runs identically on any machine with Docker
References:
↑ Back to topWhat is the difference between Docker containers and virtual machines?
The 30-Second Answer: Docker containers share the host OS kernel and virtualize at the application layer, making them lightweight and fast to start (seconds). Virtual machines include a full guest OS and virtualize at the hardware layer, making them heavier but providing stronger isolation with separate kernels for each VM.
The 2-Minute Answer (If They Want More): The fundamental difference lies in the abstraction layer. Virtual machines virtualize the entire hardware stack - each VM runs its own complete operating system on top of a hypervisor (like VMware or VirtualBox). This means each VM includes a full OS kernel, system libraries, and binaries, consuming gigabytes of disk space and significant RAM. VMs typically take minutes to boot because they're starting an entire operating system.
Docker containers, on the other hand, virtualize at the operating system level. All containers on a host share the same OS kernel and isolate processes using Linux features like namespaces and cgroups. A container only includes the application and its dependencies, not a full OS. This makes containers typically megabytes in size (not gigabytes), and they start in milliseconds to seconds because there's no OS to boot.
In terms of isolation, VMs provide stronger security boundaries since each has its own kernel. If one VM is compromised, it's harder to affect others. Containers share the kernel, so they have a smaller attack surface but potentially less isolation. However, modern container security features have significantly improved this.
For resource efficiency, you can run many more containers than VMs on the same hardware. A server that might host 10-20 VMs could easily run hundreds of containers. This efficiency is why containers became the preferred choice for microservices architectures and cloud-native applications. That said, VMs still have their place, especially when you need complete OS-level isolation or need to run different operating systems on the same hardware.
Code Example:
# Container startup (seconds)
$ time docker run alpine echo "Hello"
Hello
real 0m0.347s # Less than half a second!
# Container resource footprint
$ docker stats --no-stream
CONTAINER ID CPU % MEM USAGE / LIMIT MEM %
abc123def456 0.01% 2.5MiB / 16GiB 0.02%
# Multiple containers sharing resources efficiently
$ docker run -d nginx # ~150MB image
$ docker run -d nginx # Reuses layers, minimal additional space
$ docker run -d alpine # ~5MB image
# Architecture Comparison
VIRTUAL MACHINES:
[App A] [App B] [App C]
[Bins/Libs] [Bins/Libs] [Bins/Libs]
[Guest OS] [Guest OS] [Guest OS]
[Hypervisor]
[Host OS]
[Infrastructure]
DOCKER CONTAINERS:
[App A] [App B] [App C]
[Bins/Libs] [Bins/Libs] [Bins/Libs]
[Docker Engine]
[Host OS]
[Infrastructure]
References:
↑ Back to topWhat is a Docker image and how does it relate to a container?
The 30-Second Answer: A Docker image is a read-only template containing the application code, runtime, libraries, and dependencies needed to run an application. A container is a running instance of an image - you can create multiple containers from the same image, similar to how you create object instances from a class in programming.
The 2-Minute Answer (If They Want More): Think of the relationship between images and containers like classes and objects in object-oriented programming. An image is the blueprint (the class), while containers are the running instances (the objects). You define an image once, and you can spawn multiple containers from it, each operating independently.
Docker images are built in layers, which is one of their key features. Each instruction in a Dockerfile creates a new layer, and these layers are cached and reusable. For example, if you have ten different applications all based on the same Node.js base image, Docker only stores that base layer once and shares it across all images. This layering system makes images extremely efficient in terms of storage and transfer speed.
Images are immutable and read-only. Once built, they don't change. When you run a container from an image, Docker creates a thin writable layer on top of the read-only image layers. Any changes made inside the running container (like creating files or modifying data) happen in this writable layer. This is why when you delete a container, your image remains unchanged - the writable layer is discarded, but the underlying image persists.
Images are distributed through Docker registries (like Docker Hub or private registries). You can pull images created by others, use them as base images for your own applications, and push your custom images to share with your team or the public. This sharing mechanism is what makes Docker so powerful for collaboration and deployment consistency.
Code Example:
# Dockerfile - defines how to build an image
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# Build an image from Dockerfile
docker build -t my-python-app:1.0 .
# List images
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# my-python-app 1.0 abc123def456 2 minutes ago 150MB
# Run multiple containers from the same image
docker run -d --name app-instance-1 my-python-app:1.0
docker run -d --name app-instance-2 my-python-app:1.0
docker run -d --name app-instance-3 my-python-app:1.0
# Three independent containers running from one image
docker ps
# CONTAINER ID IMAGE COMMAND
# 111111111111 my-python-app:1.0 "python app.py"
# 222222222222 my-python-app:1.0 "python app.py"
# 333333333333 my-python-app:1.0 "python app.py"
# Inspect image layers
docker history my-python-app:1.0
References:
↑ Back to topDocker Images
What are Docker image layers and how do they work?
The 30-Second Answer: Docker images are built from layers, where each layer represents a filesystem change from a Dockerfile instruction. Layers are read-only and stacked on top of each other, with Docker using a union filesystem to present them as a single coherent filesystem. This layering system enables efficient storage through layer reuse and faster builds through caching.
The 2-Minute Answer (If They Want More): Each instruction in a Dockerfile (FROM, RUN, COPY, etc.) creates a new layer in the Docker image. These layers are immutable and stored as separate filesystem snapshots. When you build an image, Docker checks if it already has cached layers from previous builds with identical instructions and contexts. If a match is found, Docker reuses that layer instead of rebuilding it, dramatically speeding up build times.
Layers are shared between images, so if multiple images use the same base image (like node:18-alpine), they all share those base layers on disk. Only the unique layers for each image need to be stored separately. This is why pulling a new image that shares layers with existing images is much faster than pulling a completely new image.
When you run a container from an image, Docker adds a thin writable layer on top of the read-only image layers. All changes made during container runtime (file modifications, new files, etc.) are written to this container layer. When the container is deleted, this writable layer is removed, but the underlying image layers remain unchanged.
Understanding layers is crucial for optimizing Docker images. The order of instructions matters significantly - placing frequently changing instructions (like COPY of application code) after stable instructions (like installing dependencies) ensures better cache utilization and faster builds.
Code Example:
# Each instruction creates a new layer
FROM node:18-alpine # Layer 1: Base OS and Node.js
WORKDIR /app # Layer 2: Metadata change (no filesystem change)
COPY package*.json ./ # Layer 3: Package files
RUN npm ci --only=production # Layer 4: Dependencies installed
COPY . . # Layer 5: Application code
CMD ["node", "server.js"] # Layer 6: Metadata change (no filesystem change)
# View image layers and their sizes
# docker history myapp:latest
# Inspect detailed layer information
# docker image inspect myapp:latest
References:
↑ Back to topHow do you reduce Docker image size?
The 30-Second Answer: Reduce Docker image size by using smaller base images (alpine), combining RUN commands to minimize layers, using multi-stage builds to exclude build tools from the final image, and carefully selecting what files to COPY. Smaller images deploy faster, use less storage, and have a smaller attack surface.
The 2-Minute Answer (If They Want More):
Image size optimization starts with choosing the right base image. Alpine Linux images are typically 5-10x smaller than full Debian/Ubuntu images. For example, node:18-alpine is around 120MB versus node:18 at 900MB+. However, Alpine uses musl libc instead of glibc, which can occasionally cause compatibility issues with certain packages.
Multi-stage builds are one of the most effective techniques. You use one stage with build tools and dependencies to compile your application, then copy only the compiled artifacts to a minimal runtime image. This excludes compilers, build tools, and development dependencies from your final image. For compiled languages like Go, this can reduce images from 800MB to under 20MB.
Combine RUN commands using && to reduce layers, and chain cleanup commands in the same layer where you install packages. Installing a package and deleting its cache in separate RUN commands doesn't reduce image size because the cache exists in the earlier layer. Use .dockerignore to exclude unnecessary files like .git, node_modules, test files, and documentation from being copied into the image.
Other techniques include removing package manager caches (rm -rf /var/lib/apt/lists/* for apt, apk --no-cache for Alpine), using exact COPY commands instead of copying entire directories, avoiding installing "recommended" packages with apt (--no-install-recommends), and using tools like dive to analyze and identify large layers in your images.
Code Example:
# ❌ BAD: Large image with unnecessary layers (900MB+)
FROM node:18
WORKDIR /app
RUN apt-get update
RUN apt-get install -y python3 build-essential
COPY . .
RUN npm install
CMD ["node", "server.js"]
# âś… GOOD: Optimized single-stage (150MB)
FROM node:18-alpine
WORKDIR /app
# Combine commands and cleanup in same layer
RUN apk add --no-cache python3 make g++
# Copy only necessary files
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY src/ ./src/
CMD ["node", "server.js"]
# âś… BEST: Multi-stage build (120MB)
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# Production stage
FROM node:18-alpine
WORKDIR /app
# Copy only production dependencies and built artifacts
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
USER node
CMD ["node", "dist/server.js"]
# For compiled languages like Go - ultra-minimal
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM scratch # Smallest possible base (0MB)
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
.dockerignore Example:
node_modules
npm-debug.log
.git
.gitignore
.env
*.md
tests/
docs/
.vscode/
.idea/
coverage/
References:
↑ Back to topDockerfile
What is the difference between RUN, CMD, and ENTRYPOINT?
The 30-Second Answer:
RUN executes commands during image build and creates new layers. CMD provides default arguments for the container that can be overridden. ENTRYPOINT defines the main executable that runs when the container starts and is harder to override, making the container behave like an executable.
The 2-Minute Answer (If They Want More):
These three instructions serve different purposes in the container lifecycle. RUN is a build-time instruction that executes commands and commits the results to the image as a new layer. You use it to install packages, create directories, or run build scripts. Each RUN instruction creates a new layer, so it's common to chain multiple commands with && to reduce layers.
CMD is a runtime instruction that specifies default commands or arguments to run when a container starts. There can only be one CMD instruction in a Dockerfile (if multiple exist, only the last one takes effect). Importantly, CMD can be completely overridden by providing arguments to docker run. This makes CMD perfect for providing default behavior that users might want to change.
ENTRYPOINT also runs at container startup but is designed to make your container executable. It's harder to override - you need to use the --entrypoint flag explicitly. The most powerful pattern is using ENTRYPOINT and CMD together: ENTRYPOINT defines the executable, while CMD provides default arguments that can be easily overridden. This combination gives you both flexibility and security.
In practice, use RUN for build steps, CMD for containers that might run different commands (like a base image), and ENTRYPOINT when you want your container to always run a specific application.
Code Example:
# RUN - executed during build, installs packages
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Example 1: CMD only (can be overridden)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# docker run myapp → runs "node server.js"
# docker run myapp npm test → runs "npm test" instead
# Example 2: ENTRYPOINT only (always runs)
FROM node:18-alpine
WORKDIR /app
COPY . .
ENTRYPOINT ["node", "server.js"]
# docker run myapp → runs "node server.js"
# docker run myapp --port 8080 → runs "node server.js --port 8080"
# Example 3: ENTRYPOINT + CMD (best practice)
FROM node:18-alpine
WORKDIR /app
COPY . .
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run myapp → runs "node server.js"
# docker run myapp app.js → runs "node app.js"
References:
- Dockerfile reference: RUN
- Dockerfile reference: CMD
- Dockerfile reference: ENTRYPOINT
- Understand how CMD and ENTRYPOINT interact
What is the USER instruction and why is it important for security?
The 30-Second Answer:
The USER instruction sets which user runs the container process. By default, containers run as root, which is a security risk. Using USER to switch to a non-privileged user follows the principle of least privilege, limiting potential damage if the container is compromised.
The 2-Minute Answer (If They Want More): Containers running as root pose significant security risks. If an attacker exploits a vulnerability in your application and the container runs as root, they have root-level access inside the container. While container isolation provides some protection, running as root increases the attack surface, especially if there are container escape vulnerabilities or if volumes are mounted from the host.
The USER instruction allows you to specify which user (by name or UID) should run subsequent instructions and the container's main process. Best practice is to create a non-privileged user in your Dockerfile and switch to it before running your application. Many official base images (like Node.js, Python, PostgreSQL) already include a non-root user you can use.
You typically need to stay as root for installing packages and setting up the environment, then switch to a non-privileged user as the last step before CMD or ENTRYPOINT. When creating a user, it's important to set both the user and group, avoid creating a home directory if not needed, and ensure file permissions allow the non-root user to access necessary files.
Some orchestration platforms like Kubernetes can enforce that containers don't run as root through security policies. Having USER properly configured in your Dockerfile ensures your containers work in these security-conscious environments. Additionally, if you're mounting host directories, running as a non-root user with a known UID helps avoid permission issues.
Code Example:
# Example 1: Creating and using a non-root user
FROM node:18-alpine
# Create a non-root user and group
RUN addgroup -g 1001 appgroup && \
adduser -D -u 1001 -G appgroup appuser
WORKDIR /app
# Install dependencies as root (needed for npm global installs if any)
COPY package*.json ./
RUN npm ci --only=production
# Copy application files
COPY . .
# Change ownership to the non-root user
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
# Example 2: Using existing user from base image
FROM node:18-alpine
WORKDIR /app
# Install dependencies as root
COPY --chown=node:node package*.json ./
RUN npm ci --only=production
# Copy app with correct ownership
COPY --chown=node:node . .
# Switch to the 'node' user (provided by official Node image)
USER node
CMD ["node", "server.js"]
# Example 3: Python application with non-root user
FROM python:3.11-slim
# Create user with specific UID (useful for Kubernetes)
RUN useradd -m -u 1000 appuser
WORKDIR /app
# Install system dependencies as root
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
CMD ["python", "app.py"]
# Example 4: Handling permissions for mounted volumes
FROM nginx:alpine
# Create non-root user
RUN adduser -D -u 1001 nginxuser
# Configure nginx to run as non-root
RUN chown -R nginxuser:nginxuser /var/cache/nginx && \
chown -R nginxuser:nginxuser /var/log/nginx && \
chown -R nginxuser:nginxuser /etc/nginx/conf.d
# Modify nginx.conf to use unprivileged port
RUN sed -i 's/listen\s*80;/listen 8080;/' /etc/nginx/conf.d/default.conf
USER nginxuser
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
References:
- Dockerfile reference: USER
- Docker security best practices
- Processes In Containers Should Not Run As Root
Docker Containers
How do you run a container in detached mode vs interactive mode?
The 30-Second Answer:
Use docker run -d for detached mode to run containers in the background (like servers and long-running processes), and docker run -it for interactive mode to attach your terminal to the container (like running a shell or debugging). Detached mode returns control to your terminal immediately, while interactive mode keeps you connected.
The 2-Minute Answer (If They Want More):
Detached mode (-d or --detach) runs containers in the background, which is perfect for services like web servers, databases, and API backends. When you start a container in detached mode, Docker prints the container ID and returns control to your terminal. The container continues running in the background, and you can check its status with docker ps and view logs with docker logs.
Interactive mode requires two flags: -i (interactive) keeps STDIN open even if not attached, and -t (tty) allocates a pseudo-TTY. Together, -it creates an interactive terminal session inside the container. This is essential when you need to run a shell, execute commands manually, or debug issues. Without -t, you won't have a proper terminal. Without -i, you can't send input to the container.
The choice between modes depends on your use case. Production services almost always run in detached mode - you don't want your web server to stop when you close your terminal. Interactive mode is for development, debugging, running one-off commands, or working with containers interactively. You can also start a container in detached mode and later attach to it or execute commands in it.
A common pattern is running your main service in detached mode, then using docker exec -it to open a shell in the running container for debugging or maintenance tasks. This gives you the best of both worlds - the service stays running while you interact with it.
Code Example:
# Detached mode - run in background
docker run -d --name web-server -p 8080:80 nginx:latest
# Returns: abc123def456... (container ID)
# Container runs in background, terminal is free
# Check it's running
docker ps
# Interactive mode - attach terminal
docker run -it --name my-ubuntu ubuntu:latest bash
# You're now inside the container with a bash shell
# root@abc123def456:/#
# Run a one-off command interactively
docker run -it --rm python:3.11 python
# >>> print("Hello from Python")
# >>> exit()
# Container is automatically removed after exit (--rm flag)
# Start detached, then attach later
docker run -d --name redis-server redis:latest
docker attach redis-server
# Now you're seeing the live output
# Better approach: exec into a running container
docker run -d --name web-app nginx:latest
docker exec -it web-app bash
# Opens a new shell in the running container
# Container keeps running when you exit this shell
# Interactive mode with specific command
docker run -it --rm alpine:latest sh -c "echo Hello && ls -la"
# Detached with automatic restart
docker run -d --restart unless-stopped --name production-db postgres:15
References:
↑ Back to topWhat are container resource limits and how do you set them?
The 30-Second Answer:
Container resource limits control how much CPU, memory, and I/O a container can use, preventing any single container from consuming all host resources. Set them using flags like --memory (RAM limit), --cpus (CPU cores), and --memory-swap (swap space) with docker run. This is critical for multi-tenant environments and production stability.
The 2-Minute Answer (If They Want More): By default, Docker containers have no resource constraints and can use as much CPU, memory, and I/O as the host has available. While this is fine for development, it's dangerous in production where a memory leak or CPU-intensive process in one container could starve other containers or crash the host system.
Memory limits are set with --memory (or -m) and define the maximum RAM a container can use. If a container exceeds this limit, the kernel's OOM killer will terminate processes inside the container. You should also set --memory-reservation for soft limits - the container can use more if available, but should try to stay under this value. The --memory-swap flag controls total memory (RAM + swap); set it equal to --memory to disable swap for that container.
CPU limits work differently because CPU is a compressible resource (unlike memory). The --cpus flag sets how many CPU cores a container can use (can be fractional like --cpus=1.5). Alternatively, --cpu-shares sets relative weights for CPU scheduling - a container with 1024 shares gets twice as much CPU time as one with 512 shares when both are competing for CPU.
I/O limits control disk read/write bandwidth with flags like --device-read-bps and --device-write-bps. Process limits with --pids-limit prevent fork bombs. These constraints are implemented using Linux cgroups, which Docker configures automatically based on your flags.
Code Example:
# MEMORY LIMITS
# Set maximum memory to 512MB
docker run -d --name app1 --memory=512m nginx:latest
# Set memory with soft reservation
docker run -d --name app2 \
--memory=1g \
--memory-reservation=750m \
nginx:latest
# Disable swap (memory + swap = memory limit)
docker run -d --name app3 \
--memory=512m \
--memory-swap=512m \
nginx:latest
# Set swap to 2x memory
docker run -d --name app4 \
--memory=512m \
--memory-swap=1g \
nginx:latest
# CPU LIMITS
# Limit to 1.5 CPU cores
docker run -d --name app5 --cpus=1.5 my-app:latest
# Limit to 50% of one CPU
docker run -d --name app6 --cpus=0.5 my-app:latest
# Use CPU shares (relative weight)
docker run -d --name high-priority --cpu-shares=1024 app:latest
docker run -d --name low-priority --cpu-shares=512 app:latest
# high-priority gets 2x CPU when both are competing
# Pin to specific CPU cores
docker run -d --name app7 --cpuset-cpus=0,1 my-app:latest
# Only uses CPU cores 0 and 1
# COMBINED LIMITS (production example)
docker run -d \
--name production-app \
--memory=2g \
--memory-reservation=1.5g \
--memory-swap=2g \
--cpus=2 \
--pids-limit=100 \
--restart=unless-stopped \
my-app:latest
# DISK I/O LIMITS
# Limit read speed to 10 MB/s
docker run -d \
--device-read-bps=/dev/sda:10mb \
--name io-limited \
my-app:latest
# Limit write speed to 5 MB/s
docker run -d \
--device-write-bps=/dev/sda:5mb \
--name write-limited \
my-app:latest
# PROCESS LIMITS
# Limit to 200 processes (prevent fork bombs)
docker run -d --pids-limit=200 --name safe-app my-app:latest
# CHECKING RESOURCE USAGE
# View real-time stats
docker stats
# Stats for specific container
docker stats my-app
# Check resource limits on running container
docker inspect my-app | grep -A 10 "Memory\|Cpu"
# UPDATE LIMITS ON RUNNING CONTAINER
# Update memory limit
docker update --memory=1g my-app
# Update CPU limit
docker update --cpus=2 my-app
# Update multiple containers
docker update --memory=512m app1 app2 app3
# DOCKER COMPOSE EXAMPLE
# docker-compose.yml
# services:
# web:
# image: nginx:latest
# deploy:
# resources:
# limits:
# cpus: '1.5'
# memory: 1G
# reservations:
# cpus: '0.5'
# memory: 512M
References:
↑ Back to topDocker Networking
What is Docker DNS and how does container name resolution work?
The 30-Second Answer: Docker runs an embedded DNS server at 127.0.0.11 inside each container that automatically resolves container names to their IP addresses on user-defined networks. When you look up a container name, Docker's DNS returns the current IP address of that container, enabling service discovery without hardcoded IPs.
The 2-Minute Answer (If They Want More): Every container on a user-defined network gets access to Docker's embedded DNS server, which listens on 127.0.0.11:53. The container's /etc/resolv.conf is automatically configured to use this DNS server. When your application tries to connect to another container by name, the DNS query goes to Docker's DNS server, which maintains a mapping of container names to their current IP addresses within each network.
This system supports multiple forms of name resolution. The primary container name works (like "postgres"), network aliases work (you can add multiple names with --network-alias), and service names work in Swarm mode. Docker's DNS also handles round-robin load balancing when multiple containers share the same network alias or service name—each DNS query returns IPs in a rotated order.
The DNS server is network-scoped, meaning name resolution only works for containers on the same network. This provides natural network segmentation—containers on your "frontend" network can't discover or resolve containers on your "backend" network unless explicitly connected to both.
For external DNS queries (like resolving google.com), Docker forwards these to the DNS servers configured on the host system (typically from /etc/resolv.conf on Linux or the network settings on Windows/Mac). You can override the DNS servers using the --dns flag when starting containers.
Code Example:
# Create network and containers
docker network create mynet
docker run -d --name db --network mynet \
--network-alias database \
--network-alias postgres-primary \
postgres
docker run -d --name cache --network mynet redis
# Check DNS configuration inside container
docker exec db cat /etc/resolv.conf
# Shows: nameserver 127.0.0.11
# Test name resolution
docker run --rm --network mynet alpine nslookup db
# Returns db's IP address
docker run --rm --network mynet alpine nslookup database
# Also works - resolves the network alias
docker run --rm --network mynet alpine nslookup cache
# Returns cache's IP address
# Test round-robin DNS (multiple containers, same alias)
docker run -d --name web1 --network mynet --network-alias webapp nginx
docker run -d --name web2 --network mynet --network-alias webapp nginx
docker run -d --name web3 --network mynet --network-alias webapp nginx
# Query multiple times - IPs rotate
docker run --rm --network mynet alpine nslookup webapp
# Use custom DNS servers
docker run --dns 8.8.8.8 --dns 8.8.4.4 alpine nslookup google.com
# DNS doesn't work on default bridge
docker run -d --name test1 nginx # on default bridge
docker run --rm --network bridge alpine nslookup test1
# Fails - no automatic DNS on default bridge
References:
↑ Back to topDocker Volumes and Storage
How do you create and manage Docker volumes?
The 30-Second Answer:
You create volumes with docker volume create volume-name, attach them to containers using -v volume-name:/path/in/container, and manage them with commands like docker volume ls, docker volume inspect, and docker volume rm. Volumes persist independently of containers and can be reused across multiple containers.
The 2-Minute Answer (If They Want More):
Docker provides a complete CLI for volume management. You can create volumes explicitly with docker volume create before running containers, or Docker will automatically create them when you reference a named volume that doesn't exist yet. I prefer explicit creation in production because it makes infrastructure more visible and manageable.
Once created, you attach volumes to containers using the -v or --mount flag. The -v syntax is shorter (-v volume-name:/container/path) while --mount is more explicit and recommended for production (--mount source=volume-name,target=/container/path). You can mount the same volume to multiple containers, which is useful for sharing data, but be careful with concurrent writes—you might need application-level locking.
For inspection, docker volume inspect shows detailed information including the mount point on the host, driver used, and labels. This is helpful for troubleshooting or finding where Docker actually stores your data. docker volume ls lists all volumes, and you can filter by labels or dangling status. Dangling volumes are volumes no longer attached to any container—they accumulate over time and waste disk space.
To clean up, use docker volume rm to remove specific volumes or docker volume prune to remove all unused volumes. Be extremely careful with prune—it permanently deletes data. In production, I always inspect and backup before pruning. You can also use labels to organize volumes and prevent accidental deletion of important data.
Code Example:
# Create a volume explicitly
docker volume create my-app-data
# Create a volume with specific driver and options
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/path/to/share \
nfs-volume
# List all volumes
docker volume ls
# Filter volumes by label
docker volume ls --filter label=environment=production
# Inspect a volume (shows mount point, driver, etc.)
docker volume inspect my-app-data
# Use a volume in a container
docker run -d \
--name myapp \
-v my-app-data:/app/data \
myapp:latest
# Same using --mount (more explicit, recommended)
docker run -d \
--name myapp \
--mount source=my-app-data,target=/app/data \
myapp:latest
# Create volume with labels for organization
docker volume create \
--label environment=production \
--label backup=daily \
prod-db-data
# Remove a specific volume (must not be in use)
docker volume rm my-app-data
# Remove all unused volumes (CAREFUL - permanent deletion!)
docker volume prune
# Remove volumes matching a filter
docker volume prune --filter label=environment=dev
# Copy data from one volume to another
docker run --rm \
-v source-volume:/source \
-v target-volume:/target \
alpine sh -c "cp -av /source/. /target/"
References:
↑ Back to topHow do you backup and restore Docker volumes?
The 30-Second Answer:
Back up volumes by running a temporary container that mounts the volume and creates a tar archive: docker run --rm -v volume-name:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz /data. Restore by reversing the process: extract the tar archive into a new volume using a similar temporary container.
The 2-Minute Answer (If They Want More): The standard approach to backing up Docker volumes is to use a temporary container as a bridge between the volume and your host filesystem. You mount the volume you want to back up and a bind mount to a host directory, then use tar to create a compressed archive. This works because the temporary container has access to both the volume's data and the host filesystem where you want to save the backup.
For production environments, I recommend several best practices. First, stop or pause the container using the volume before backing up to ensure data consistency, especially for databases. For databases, use database-specific backup tools (like pg_dump for PostgreSQL or mongodump for MongoDB) rather than raw file backups—they handle consistency and can create smaller, more reliable backups. Second, automate backups with cron jobs or orchestration tools, and test your restore process regularly. Third, store backups off-site (cloud storage, remote server) to protect against hardware failure.
For restoration, create a new volume and extract the backup archive into it using a similar temporary container approach. If you're restoring a database, you might need to recreate the volume, start the database container, and then use the database's restore tools rather than copying raw files.
Advanced scenarios might use volume plugins that support snapshots (like cloud provider plugins for AWS EBS or Azure Disks), which can create point-in-time copies without stopping containers. Some third-party tools like Velero (for Kubernetes) or docker-volume-backup provide automated scheduling and cloud integration for Docker volume backups.
Code Example:
# BACKUP: Create a tar.gz backup of a volume
docker run --rm \
-v my-app-data:/data:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/my-app-data-backup-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .
# BACKUP: For a running database, use database-specific tools
# PostgreSQL example
docker exec postgres-container \
pg_dump -U postgres mydb > backup-$(date +%Y%m%d).sql
# MongoDB example
docker exec mongo-container \
mongodump --out=/backup/$(date +%Y%m%d)
# RESTORE: Extract backup to a new volume
# First, create the new volume
docker volume create my-app-data-restored
# Then extract the backup into it
docker run --rm \
-v my-app-data-restored:/data \
-v $(pwd):/backup \
alpine \
tar xzf /backup/my-app-data-backup-20240115-143000.tar.gz -C /data
# RESTORE: Database-specific restore
# PostgreSQL
docker exec -i postgres-container \
psql -U postgres mydb < backup-20240115.sql
# MongoDB
docker exec mongo-container \
mongorestore /backup/20240115
# Automated backup script example
# #!/bin/bash
# BACKUP_DIR="/backups"
# VOLUME_NAME="production-db"
# TIMESTAMP=$(date +%Y%m%d-%H%M%S)
#
# # Stop container for consistency (optional but recommended)
# docker stop my-app
#
# # Create backup
# docker run --rm \
# -v ${VOLUME_NAME}:/data:ro \
# -v ${BACKUP_DIR}:/backup \
# alpine \
# tar czf /backup/${VOLUME_NAME}-${TIMESTAMP}.tar.gz -C /data .
#
# # Restart container
# docker start my-app
#
# # Upload to cloud storage (example with AWS S3)
# aws s3 cp ${BACKUP_DIR}/${VOLUME_NAME}-${TIMESTAMP}.tar.gz \
# s3://my-backups/docker-volumes/
#
# # Keep only last 7 days of local backups
# find ${BACKUP_DIR} -name "${VOLUME_NAME}-*.tar.gz" -mtime +7 -delete
# Copy entire volume to another volume (useful for cloning)
docker volume create my-app-data-clone
docker run --rm \
-v my-app-data:/source:ro \
-v my-app-data-clone:/target \
alpine \
sh -c "cd /source && cp -av . /target"
# Backup multiple volumes at once
docker run --rm \
-v app-data:/data/app:ro \
-v db-data:/data/db:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/full-backup-$(date +%Y%m%d).tar.gz -C /data .
References:
↑ Back to topDocker Compose
How do you define services in Docker Compose?
The 30-Second Answer:
Services in Docker Compose are defined under the services: key, where each service gets a unique name and configuration block. You specify either an image: to pull from a registry or build: to build from a Dockerfile, then configure ports, volumes, environment variables, networks, and other container settings.
The 2-Minute Answer (If They Want More): Defining a service in Docker Compose means describing how a container should be created and configured. Each service definition starts with a unique name that becomes the container's hostname on the Docker network. This name is what other services use to communicate with it.
The most fundamental decision is how to get the container image: use image: to pull a pre-built image from Docker Hub or a registry, or use build: to build an image from a Dockerfile in your project. The build option can be a simple path or a detailed configuration with context, Dockerfile name, and build arguments.
Beyond the image source, you configure how the container runs. The ports: section maps container ports to host ports for external access. The volumes: section mounts host directories or named volumes into the container. The environment: section sets environment variables, either inline or from files. The networks: section specifies which networks the container joins.
You can also control container behavior with settings like restart: (restart policy), depends_on: (service dependencies), command: (override default command), healthcheck: (container health monitoring), and deploy: (resource limits and replicas). Each service runs as one or more containers, and Compose handles creating, starting, and networking them together.
Code Example:
version: '3.8'
services:
# Service using pre-built image
redis:
image: redis:7-alpine # Image from Docker Hub
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: always
command: redis-server --appendonly yes # Override default command
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 3s
retries: 3
# Service built from Dockerfile
app:
build:
context: ./app # Build context directory
dockerfile: Dockerfile # Dockerfile name (default)
args: # Build arguments
NODE_VERSION: 18
APP_ENV: production
target: production # Multi-stage build target
image: myapp:latest # Tag the built image
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- API_KEY=${API_KEY} # From .env file
env_file:
- .env # Load from environment file
volumes:
- ./app:/app # Bind mount for development
- /app/node_modules # Anonymous volume
depends_on:
- redis
restart: unless-stopped
networks:
- app-network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
# Service with minimal configuration
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro # Read-only mount
depends_on:
- app
volumes:
redis-data:
networks:
app-network:
References:
↑ Back to topDocker Security
What are Docker security best practices?
The 30-Second Answer: Key Docker security best practices include using minimal base images, running as non-root users, scanning images for vulnerabilities, using multi-stage builds, implementing least privilege with capability dropping, managing secrets properly with Docker Secrets or external vaults, keeping images updated, and enabling security features like seccomp, AppArmor, and SELinux.
The 2-Minute Answer (If They Want More):
Start with minimal base images like Alpine or distroless images to reduce the attack surface. Fewer packages mean fewer vulnerabilities and smaller images. Use specific version tags rather than latest to ensure reproducible builds and avoid unexpected changes. Implement multi-stage builds to exclude build tools and dependencies from the final image.
Scan images regularly using tools like Trivy, Snyk, or Docker Scout to identify known vulnerabilities (CVEs). Integrate scanning into your CI/CD pipeline to catch issues before deployment. Sign and verify images using Docker Content Trust to ensure image integrity and prevent tampering in the supply chain.
Apply the principle of least privilege throughout your container security model. Run containers as non-root users, drop unnecessary Linux capabilities, use read-only root filesystems where possible, and limit resource consumption with memory and CPU constraints. Enable security modules like AppArmor, SELinux, or seccomp to restrict system calls and kernel features.
Never store secrets in Dockerfiles, environment variables, or image layers. Use Docker Secrets for Swarm, Kubernetes Secrets with encryption at rest, or external secret management tools like HashiCorp Vault. Implement network segmentation using Docker networks to isolate containers, and regularly update base images and dependencies to patch security vulnerabilities.
Code Example:
# BEST PRACTICE Dockerfile
# 1. Use specific, minimal base image
FROM node:18-alpine3.18 AS builder
# 2. Set metadata
LABEL maintainer="security@example.com"
LABEL version="1.0"
# 3. Install dependencies in separate layer
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# 4. Multi-stage build - final image
FROM node:18-alpine3.18
# 5. Install security updates
RUN apk upgrade --no-cache && \
apk add --no-cache dumb-init
# 6. Create non-root user with specific UID
RUN addgroup -g 1001 appgroup && \
adduser -D -u 1001 -G appgroup appuser
# 7. Set working directory
WORKDIR /app
# 8. Copy with correct ownership
COPY --chown=appuser:appgroup --from=builder /build/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
# 9. Switch to non-root user
USER appuser
# 10. Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# 11. Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
# 12. Expose only necessary ports
EXPOSE 3000
CMD ["node", "server.js"]
# Docker run with security best practices
docker run \
--name secure-app \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--security-opt=seccomp=./seccomp-profile.json \
--memory=512m \
--cpus=1.0 \
--pids-limit=100 \
--network=app-network \
-e NODE_ENV=production \
myapp:1.0
# Image scanning with Trivy
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--severity HIGH,CRITICAL \
myapp:1.0
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
docker pull myapp:1.0
docker push myapp:1.0
# docker-compose.yml with security settings
version: '3.8'
services:
app:
image: myapp:1.0
read_only: true
security_opt:
- no-new-privileges:true
- seccomp:seccomp-profile.json
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
user: "1001:1001"
tmpfs:
- /tmp:rw,noexec,nosuid,size=100m
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
networks:
- app-network
secrets:
- db_password
secrets:
db_password:
external: true
networks:
app-network:
driver: bridge
internal: true
References:
- Docker Security Best Practices
- NIST Application Container Security Guide
- Snyk Docker Security Best Practices
- Docker Bench for Security
Performance and Troubleshooting
How do you clean up Docker resources (images, containers, volumes)?
The 30-Second Answer: I clean up Docker resources using docker system prune to remove all unused resources at once, or selectively with docker container prune for stopped containers, docker image prune for dangling images, docker volume prune for unused volumes, and docker network prune for unused networks. I regularly schedule cleanup to prevent disk space issues, especially in development environments.
The 2-Minute Answer (If They Want More): Docker accumulates resources over time - stopped containers, unused images, dangling image layers, and orphaned volumes - which can consume significant disk space. I manage this through both manual and automated cleanup strategies, being careful to avoid removing resources that are still needed.
For routine cleanup, I use docker system prune which removes stopped containers, dangling images, and unused networks. Adding the -a flag also removes unused images (not just dangling ones), and --volumes includes unused volumes. I'm cautious with --volumes because it can delete data volumes that might be needed later, so I prefer to manage volumes separately.
For more granular control, I use specific prune commands. docker image prune removes dangling images (layers with no tag and no container reference), while docker image prune -a removes all images not used by at least one container. docker container prune removes all stopped containers, which I run frequently in development. docker volume prune removes volumes not used by any container, which I use more carefully in production.
I implement automated cleanup in development environments using cron jobs or CI/CD cleanup stages. For production, I'm more conservative, typically only removing specific resources after confirming they're truly unused. I use filters to target specific cleanup criteria, like removing images older than a certain date or containers that have been stopped for more than 24 hours.
Before major cleanup operations, I use docker system df to see disk usage breakdown and identify what's consuming space. I also use labels on containers and volumes to prevent accidental deletion of important resources.
Code Example:
# Check disk usage before cleanup
docker system df
docker system df -v # Verbose output
# Remove all stopped containers
docker container prune
# Remove specific stopped containers (stopped > 24h ago)
docker container prune --filter "until=24h"
# Force removal without confirmation
docker container prune -f
# Remove all unused images (not just dangling)
docker image prune -a
# Remove dangling images only
docker image prune
# Remove images older than 48 hours
docker image prune -a --filter "until=48h"
# Remove unused volumes (careful with this!)
docker volume prune
# Remove unused networks
docker network prune
# Remove ALL unused resources (containers, images, networks)
docker system prune
# Remove ALL unused resources including volumes
docker system prune -a --volumes
# Remove everything with force (no confirmation)
docker system prune -a --volumes -f
# Clean up build cache
docker builder prune
# Remove all build cache (including cache from other builders)
docker builder prune -a
# Manual cleanup of specific resources
# Remove specific container
docker rm my-container
docker rm -f my-container # Force remove even if running
# Remove specific image
docker rmi my-image:tag
docker rmi -f my-image:tag # Force remove
# Remove specific volume
docker volume rm my-volume
# Remove all stopped containers (alternative)
docker rm $(docker ps -a -q -f status=exited)
# Remove all dangling images (alternative)
docker rmi $(docker images -f "dangling=true" -q)
# Remove images by pattern
docker images | grep "my-app" | awk '{print $3}' | xargs docker rmi
# Remove containers by name pattern
docker ps -a | grep "test-" | awk '{print $1}' | xargs docker rm -f
# Clean up with labels
docker container prune --filter "label=temporary=true"
docker image prune -a --filter "label=stage=development"
# Check what will be removed (dry run concept)
docker images -f "dangling=true"
docker ps -a -f status=exited
docker volume ls -f dangling=true
# Advanced: Remove images except specific ones
docker images --format "{{.Repository}}:{{.Tag}}" | \
grep -v "my-important-image\|another-important" | \
xargs -r docker rmi
# Automated cleanup script
#!/bin/bash
# cleanup.sh - Run weekly via cron
echo "Starting Docker cleanup..."
# Remove stopped containers older than 72 hours
docker container prune -f --filter "until=72h"
# Remove dangling images
docker image prune -f
# Remove unused images older than 7 days
docker image prune -a -f --filter "until=168h"
# Remove unused networks
docker network prune -f
# Remove build cache older than 7 days
docker builder prune -f --filter "until=168h"
# Report disk usage
echo "Disk usage after cleanup:"
docker system df
# Optional: Alert if usage is still high
USAGE=$(docker system df -v | grep "Images" | awk '{print $4}')
echo "Docker disk usage: $USAGE"
# Setup cron job for automated cleanup (Linux)
# Edit crontab
crontab -e
# Add this line to run cleanup every Sunday at 2 AM
0 2 * * 0 /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1
# Systemd timer alternative (more modern)
# /etc/systemd/system/docker-cleanup.service
[Unit]
Description=Docker Cleanup Service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/docker-cleanup.sh
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/docker-cleanup.timer
[Unit]
Description=Docker Cleanup Timer
Requires=docker-cleanup.service
[Timer]
OnCalendar=Sun 02:00
Persistent=true
[Install]
WantedBy=timers.target
# Enable the timer
sudo systemctl enable docker-cleanup.timer
sudo systemctl start docker-cleanup.timer
sudo systemctl status docker-cleanup.timer
# Docker Compose cleanup (remove project resources)
# Stop and remove containers, networks, images
docker-compose down
# Also remove volumes (careful!)
docker-compose down -v
# Remove images as well
docker-compose down --rmi all
# Remove everything including orphaned containers
docker-compose down -v --rmi all --remove-orphans
// Node.js cleanup script example
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
async function dockerCleanup() {
try {
console.log('Starting Docker cleanup...');
// Remove stopped containers
await execPromise('docker container prune -f --filter "until=72h"');
console.log('Removed stopped containers');
// Remove dangling images
await execPromise('docker image prune -f');
console.log('Removed dangling images');
// Get disk usage
const { stdout } = await execPromise('docker system df');
console.log('Disk usage:\n', stdout);
} catch (error) {
console.error('Cleanup failed:', error);
process.exit(1);
}
}
dockerCleanup();
References:
- Docker System Prune Documentation
- Docker Image Prune Documentation
- Docker Volume Prune Documentation
- Managing Docker Disk Usage