Dockerfile anatomy, image layers, build cache, CMD vs ENTRYPOINT — the complete reference.
A Dockerfile is a text document containing all the commands needed to build a Docker image. It automates the image creation process and ensures reproducibility.
# Comments start with #
FROM ubuntu:22.04
LABEL maintainer="your-email@example.com"
LABEL description="Simple web application"
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python3", "app.py"]
Sets the base image for subsequent instructions. Must be the first instruction (except ARG).
Adds metadata to the image as key-value pairs.
Executes commands during image build. Creates new layers.
Copies files from host to container filesystem.
Like COPY but with extra features (auto-extract tar, URLs).
Sets working directory for subsequent instructions.
Documents which ports the container listens on.
Sets environment variables.
Default command to run when container starts.
Configures container to run as executable.
Sets the user for subsequent instructions.
Defines build-time variables.
Every Dockerfile must start with a FROM instruction (except ARG for build arguments used in FROM).
# Official image
FROM python:3.11-slim
# Specific version
FROM node:18.16.0-alpine
# Multi-stage build
FROM golang:1.20 AS builder
# Platform-specific
FROM --platform=linux/amd64 ubuntu:22.04
# ARG before FROM
ARG VERSION=3.11
FROM python:${VERSION}-slim
latest) for
production to
ensure reproducibility.
Executes commands during image build. Each RUN creates a new layer.
RUN apt-get update RUN echo "Hello World"
Runs in shell (/bin/sh -c)
RUN ["apt-get", "update"] RUN ["/bin/bash", "-c", "echo Hello"]
Direct execution, no shell processing
&& to reduce layers:
RUN apt-get update && \
apt-get install -y python3 && \
rm -rf /var/lib/apt/lists/*
| Feature | COPY | ADD |
|---|---|---|
| Basic file copy | ✅ Yes | ✅ Yes |
| Auto-extract tar files | ❌ No | ✅ Yes |
| Download from URLs | ❌ No | ✅ Yes (deprecated) |
| Recommended | ✅ Preferred | ⚠️ Use sparingly |
# COPY - simple and predictable COPY package.json /app/ COPY . /app # COPY with wildcard COPY *.py /app/ # COPY with ownership COPY --chown=user:group app.py /app/ ```html # Documentation: Docker COPY Documentation # Debugging_help: Kaniko Issue #1524 ``` # ADD - auto-extract tar ADD archive.tar.gz /app/ # ADD - multiple sources ADD file1.txt file2.txt /app/
# Sets working directory for RUN, CMD, ENTRYPOINT, COPY, ADD
WORKDIR /app
# Creates directory if it doesn't exist
WORKDIR /var/www/html
# Can use environment variables
ENV APP_HOME=/app
WORKDIR ${APP_HOME}
RUN cd /path - WORKDIR persists
across
instructions.
# Single variable
ENV NODE_ENV=production
# Multiple variables (space-separated)
ENV PORT=8080 HOST=0.0.0.0
# Multiple variables (one per line)
ENV API_KEY=abc123 \
DATABASE_URL=postgres://localhost \
DEBUG=false
# TCP port (default) EXPOSE 8080 # UDP port EXPOSE 53/udp # Multiple ports EXPOSE 80 443 # With protocol EXPOSE 8080/tcp 8081/tcp
-p flag in docker run to publish ports.
# Create non-root user RUN groupadd -r appuser && useradd -r -g appuser appuser # Switch to non-root user USER appuser # All subsequent instructions run as this user WORKDIR /home/appuser COPY --chown=appuser:appuser . . # Can switch back to root if needed USER root RUN apt-get install -y package USER appuser
Both define what command runs when a container starts, but they behave differently and serve different purposes.
| Feature | CMD | ENTRYPOINT |
|---|---|---|
| Purpose | Default command with arguments | Main executable for container |
| Overridable | ✅ Easily (docker run args replace it) | ⚠️ Requires --entrypoint flag |
| Common Use | Flexible containers (multiple commands) | Single-purpose containers |
| Arguments | Entire command replaced | Docker run args appended |
| Multiple Allowed | ❌ Only last one used | ❌ Only last one used |
# 1. Exec form (preferred) CMD ["python", "app.py"] # 2. Shell form CMD python app.py # 3. As default parameters to ENTRYPOINT ENTRYPOINT ["python"] CMD ["app.py"]
# Dockerfile FROM python:3.11-slim WORKDIR /app COPY app.py . CMD ["python", "app.py"]
Executes: python app.py
Executes: python test.py
(CMD replaced entirely)
Executes: bash
(CMD ignored)
# 1. Exec form (preferred) ENTRYPOINT ["python", "app.py"] # 2. Shell form (wraps in /bin/sh -c) ENTRYPOINT python app.py
# Dockerfile FROM python:3.11-slim WORKDIR /app COPY app.py . ENTRYPOINT ["python", "app.py"]
Executes: python app.py
Executes: python app.py --debug
(Args appended to ENTRYPOINT)
Executes: bash
(Requires explicit flag)
The most powerful pattern: ENTRYPOINT defines the executable, CMD provides default arguments.
FROM python:3.11-slim WORKDIR /app COPY app.py . # ENTRYPOINT is the executable ENTRYPOINT ["python", "app.py"] # CMD provides default arguments CMD ["--host", "0.0.0.0", "--port", "8080"]
Executes:
python app.py --host 0.0.0.0 --port 8080
Executes:
python app.py --port 3000
(CMD replaced, ENTRYPOINT intact)
Executes:
python app.py --port 8080 --debug
(User provides all args)
CMD ["npm", "start"]
Use when: Flexible container that might run different commands
Example: Development images, base images
ENTRYPOINT ["nginx", "-g", "daemon off;"]
Use when: Container always runs same executable
Example: Service containers (nginx, databases)
ENTRYPOINT ["python", "app.py"] CMD ["--config", "default.yml"]
Use when: Fixed executable with configurable default args
Example: CLI tools, applications with options
COPY entrypoint.sh / ENTRYPOINT ["/entrypoint.sh"] CMD ["python", "app.py"]
Use when: Need initialization before main process
Example: Setup environment, wait for dependencies
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY . . EXPOSE 3000 # Flexible - easy to override for testing CMD ["npm", "start"] # Run normally: docker run myapp # Run tests: docker run myapp npm test # Interactive shell: docker run -it myapp sh
FROM postgres:15 ENV POSTGRES_PASSWORD=secret # Always runs postgres, args can be added ENTRYPOINT ["docker-entrypoint.sh"] CMD ["postgres"] # Run with defaults: docker run mydb # Add postgres opts: docker run mydb postgres -c max_connections=100
FROM python:3.11-slim RUN pip install awscli WORKDIR /work # Fixed executable, flexible arguments ENTRYPOINT ["aws"] CMD ["--help"] # Show help: docker run awscli # List S3 buckets: docker run awscli s3 ls # Specific command: docker run awscli ec2 describe-instances
🔗 Explore Further:
# Basic build (looks for Dockerfile in current directory) docker build . # Build with tag docker build -t myapp:1.0 . # Build with multiple tags docker build -t myapp:1.0 -t myapp:latest . # Specify Dockerfile name docker build -f Dockerfile.dev -t myapp:dev . # Specify build context docker build -t myapp:1.0 /path/to/context # Build with build arguments docker build --build-arg VERSION=1.0 -t myapp:1.0 .
The build context is the set of files at the specified PATH or URL. Docker sends this entire context to the daemon.
myapp/ ├── Dockerfile ├── src/ │ └── app.py ├── requirements.txt └── .dockerignore
Only necessary files included
myapp/ ├── Dockerfile ├── src/ ├── node_modules/ ← 300MB ├── .git/ ← 500MB ├── venv/ ← 200MB └── large-data/ ← 2GB
Sending unnecessary files slows build
Exclude files from build context to speed up builds and reduce image size.
# Version control .git .gitignore .gitattributes # Dependencies (will be installed in image) node_modules venv __pycache__ # IDE .vscode .idea *.swp # Documentation README.md docs/ # Testing tests/ *.test.js coverage/ # CI/CD .github .gitlab-ci.yml Jenkinsfile # Local environment .env.local docker-compose.yml # Build artifacts dist/ build/ *.log
Pass variables at build time to customize image creation.
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
ARG BUILD_DATE
ARG VERSION
LABEL build_date=${BUILD_DATE}
LABEL version=${VERSION}
ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}
RUN echo "Building version ${VERSION}"
# With build args docker build \ --build-arg VERSION=1.2.3 \ --build-arg BUILD_DATE=$(date -u) \ --build-arg APP_ENV=staging \ -t myapp:1.2.3 \ . # Using default ARG values docker build -t myapp:latest .
docker history. Never pass
secrets via ARG. Use Docker secrets or ENV at runtime instead.
Use multiple FROM statements to create smaller, more secure production images.
# Stage 1: Build stage FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Production stage FROM node:18-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 CMD ["node", "dist/server.js"] # Result: Only production dependencies and built code in final image
# Build stage FROM golang:1.20 AS builder WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o myapp # Production stage FROM alpine:3.18 RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /app/myapp . CMD ["./myapp"] # Result: ~5MB final image vs ~800MB with full Go image
FROM python:3.11-slim AS base WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Development stage FROM base AS development COPY requirements-dev.txt . RUN pip install --no-cache-dir -r requirements-dev.txt COPY . . CMD ["python", "app.py"] # Production stage FROM base AS production COPY . . RUN useradd -m appuser && chown -R appuser /app USER appuser CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"] # Build dev: docker build --target development -t myapp:dev . # Build prod: docker build --target production -t myapp:prod .
Docker caches each layer. Understanding cache behavior speeds up builds significantly.
# Instruction unchanged RUN apt-get update # Files unchanged since last build COPY package.json . # Result: Layer reused from cache
# Instruction modified RUN apt-get update && apt-get install -y git # File changed COPY package.json . (modified) # Result: Layer + all subsequent layers rebuilt
# ❌ BAD: Copy everything, then install (cache invalidated often) COPY . /app RUN npm install # ✅ GOOD: Copy dependencies first, then source (better caching) COPY package*.json /app/ RUN npm install COPY . /app # Why? package.json changes rarely vs source code changes frequently
| Option | Description | Example |
|---|---|---|
| -t, --tag | Name and tag for image | -t myapp:1.0 |
| -f, --file | Specify Dockerfile path | -f Dockerfile.prod |
| --build-arg | Set build-time variables | --build-arg VERSION=1.0 |
| --target | Set target build stage | --target production |
| --no-cache | Disable cache, rebuild all | docker build --no-cache . |
| --pull | Always pull base image | docker build --pull . |
| --platform | Set platform (multi-arch) | --platform linux/amd64 |
| --progress | Output type (auto/plain/tty) | --progress=plain |
Each instruction in a Dockerfile creates a new layer. Layers are stacked and cached independently.
Docker reuses layer if:
Docker rebuilds if:
FROM python:3.11-slim WORKDIR /app # Copy everything first COPY . . # Install dependencies RUN pip install -r requirements.txt CMD ["python", "app.py"]
FROM python:3.11-slim WORKDIR /app # Copy dependencies first COPY requirements.txt . # Install dependencies RUN pip install -r requirements.txt # Copy source code last COPY . . CMD ["python", "app.py"]
| Scenario | Poor Caching | Optimal Caching |
|---|---|---|
| First build | 180 seconds | 180 seconds |
| Change app.py | 165 seconds (reinstall deps) | 5 seconds (cache hit) |
| Change requirements.txt | 165 seconds | 155 seconds |
# Rarely changes FROM node:18-alpine WORKDIR /app # Changes occasionally COPY package*.json ./ RUN npm ci --production # Changes frequently COPY . . CMD ["node", "server.js"]
# ❌ Multiple layers
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y pip
# ✅ Single layer
RUN apt-get update && \
apt-get install -y \
python3 \
python3-pip && \
rm -rf /var/lib/apt/lists/*
# ❌ Cleanup in separate layer (doesn't reduce size)
RUN apt-get install -y build-tools
RUN rm -rf /var/lib/apt/lists/*
# ✅ Cleanup in same layer
RUN apt-get install -y build-tools && \
rm -rf /var/lib/apt/lists/*
# .dockerignore node_modules npm-debug.log .git .vscode *.md # Prevents unnecessary cache invalidation
# View image layers and sizes docker history myapp:latest # Detailed JSON output docker image inspect myapp:latest # See layer cache IDs docker build --progress=plain . # Export and examine docker save myapp:latest -o myapp.tar tar -xvf myapp.tar
Docker BuildKit provides advanced caching features for dependencies.
# syntax=docker/dockerfile:1
FROM python:3.11-slim
# Cache pip packages across builds
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Cache apt packages
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y git
# Build with BuildKit
DOCKER_BUILDKIT=1 docker build -t myapp .
FROM ubuntu:22.04 RUN apt-get update RUN apt-get install -y python3 python3-pip git build-essential COPY . /app RUN cd /app && pip install -r requirements.txt WORKDIR /app CMD ["python3", "app.py"]
FROM python:3.11-slim
WORKDIR /app
# Combine and cleanup in single layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git && \
rm -rf /var/lib/apt/lists/*
# Optimize caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Source last
COPY . .
CMD ["python3", "app.py"]
# Create non-root user
RUN groupadd -r appuser && \
useradd -r -g appuser appuser
# Set ownership
COPY --chown=appuser:appuser . /app
# Switch user
USER appuser
# Scan image docker scan myapp:latest # Use Trivy trivy image myapp:latest # Update base images regularly FROM python:3.11-slim
# ❌ NEVER do this ENV API_KEY=secret123 RUN echo "password" > /app/pw.txt # ✅ Use at runtime docker run -e API_KEY=$API_KEY myapp docker secret create api_key key.txt
# python:3.11 = 900MB FROM python:3.11-slim # 130MB FROM python:3.11-alpine # 50MB # node:18 = 1GB FROM node:18-slim # 250MB FROM node:18-alpine # 180MB
# Build stage FROM node:18 AS builder RUN npm run build # Production stage FROM node:18-alpine COPY --from=builder /app/dist ./dist # Result: 180MB vs 1GB
RUN apt-get update && \
apt-get install -y package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
# .dockerignore node_modules .git *.md tests/ .env coverage/
# Rarely changes first COPY package.json . RUN npm install # Frequently changes last COPY . .
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Build from subdirectory docker build -t app ./backend # Exclude with .dockerignore # Reduces context sent to daemon
# ❌ Multiple layers RUN cmd1 RUN cmd2 # ✅ Single layer RUN cmd1 && cmd2
# ❌ Unpredictable FROM python:3 RUN pip install flask # ✅ Reproducible FROM python:3.11.5-slim RUN pip install flask==2.3.2
LABEL maintainer="dev@example.com" LABEL version="1.0.0" LABEL description="API Server" LABEL org.opencontainers.image.source="https://github.com/user/repo"
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
# PostgreSQL client
postgresql-client \
# Image processing
libmagic1
HEALTHCHECK --interval=30s \
--timeout=3s \
--start-period=5s \
--retries=3 \
CMD curl -f http://localhost/ || exit 1
# syntax=docker/dockerfile:1
# Build stage
FROM node:18-alpine AS builder
# Install build dependencies
RUN apk add --no-cache python3 make g++
WORKDIR /app
# Install dependencies (cached layer)
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
# Build application
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy built application from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD node healthcheck.js || exit 1
# Add metadata
LABEL maintainer="devops@company.com" \
version="1.0.0" \
description="Production Node.js API"
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
| Category | Best Practice | Why |
|---|---|---|
| Security | Use non-root USER | Limit container privileges |
| Scan for vulnerabilities | Detect security issues | |
| No secrets in image | Prevent credential leaks | |
| Size | Use slim/alpine bases | Reduce image size 70-90% |
| Multi-stage builds | Exclude build dependencies | |
| Clean up in same layer | Remove temp files properly | |
| Speed | Order by change frequency | Maximize cache hits |
| Use .dockerignore | Reduce build context | |
| Combine RUN commands | Fewer layers | |
| Reliability | Pin versions | Reproducible builds |
| Add HEALTHCHECK | Container health monitoring | |
| Use WORKDIR | Predictable paths |