The way you write your Dockerfile can result in a 10x difference in image size, 20x difference in build time, and 5x difference in vulnerability count. Evolving from "it works" to "production-ready" Dockerfiles is an essential skill for DevOps engineers in 2025.
At Kubo, we manage numerous container workloads on Kubernetes where Dockerfile quality directly impacts cluster-wide performance and security. This article systematically covers Dockerfile best practices based on the latest 2025 insights.
1. Base Image Selection: The Foundation of Lightweight Images
Base image selection has the greatest impact on final image size and security. The Docker official documentation recommends images from trusted sources.
Base Image Comparison
| Base Image | Size | CVEs (approx.) | Shell | Use Case |
|---|---|---|---|---|
ubuntu:24.04 | ~77MB | 30-50 | bash | General (not recommended) |
debian:bookworm-slim | ~52MB | 20-40 | bash | Lightweight Debian |
alpine:3.21 | ~5MB | 5-10 | sh (busybox) | Lightweight + debuggable |
distroless/static | ~2MB | 0-2 | None | Go/Rust static binaries |
distroless/base | ~20MB | 2-5 | None | glibc-dependent apps |
scratch | 0MB | 0 | None | Fully custom |
Google's distroless images contain only your application and its runtime dependencies, with no shell or package manager, minimizing the attack surface.
# Recommended: Choose minimal images for your use case
# Go applications
FROM gcr.io/distroless/static-debian12
# Node.js applications
FROM gcr.io/distroless/nodejs20-debian12
# Python applications
FROM python:3.12-slim
Version pinning is essential for reproducibility:
# Bad: Tag may change
FROM node:20
# Good: Specific version
FROM node:20.18-alpine3.21
# Best: Pinned by digest
FROM node:20.18-alpine3.21@sha256:abc123...
Captain.AI analyzes your project's language and framework to automatically recommend optimal base images.
2. Layer Structure Optimization
Docker uses layer caching to speed up builds. Optimizing instruction order maximizes cache hit rates.
Instruction Order: Least Frequently Changed First
# 1. Base image (rarely changes)
FROM node:20-alpine AS builder
# 2. System dependency installation (infrequent changes)
RUN apk add --no-cache python3 make g++
# 3. Copy dependency definitions (changes when packages added)
WORKDIR /app
COPY package.json package-lock.json ./
# 4. Install dependencies (re-runs when packages added)
RUN npm ci
# 5. Copy source code (frequent changes)
COPY . .
# 6. Build (re-runs on source code changes)
RUN npm run build
With this order, source code changes trigger only steps 5-6, while dependency installation (potentially minutes long) remains cached. ByteScrum's best practices article puts it aptly: "Wrong order = 20-minute rebuilds; correct order = 20-second rebuilds."
Consolidating RUN Instructions
# Bad: Creates 3 layers, intermediate files persist
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*
# Good: Single layer, cache cleaned
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
&& rm -rf /var/lib/apt/lists/*
3. Essential Security Hardening
According to Sysdig's research, 76% of production containers run as root — a critical security risk.
Run as Non-Root User
# Create application user
RUN addgroup -g 10001 -S appgroup && \
adduser -u 10001 -S appuser -G appgroup
# Set ownership of required directories
COPY --chown=appuser:appgroup . /app
# Switch to non-root user
USER appuser
Using UIDs above 10000 avoids ID collisions with host OS system users.
Secret Management
# NEVER do this: Secrets persist in image layers
COPY .env /app/.env
ENV API_KEY=sk-secret-key
# Correct: BuildKit secret mounts
RUN --mount=type=secret,id=api_key \
export API_KEY=$(cat /run/secrets/api_key) && \
./setup.sh
BuildKit secret mounts make secrets available only during build without persisting them in image layers.
Leveraging .dockerignore
# .dockerignore
.git
.gitignore
node_modules
.env
.env.*
*.md
docker-compose*.yml
Dockerfile*
.DS_Store
__pycache__
*.pyc
coverage/
.pytest_cache/
.dockerignore reduces build context size and prevents unintended files — especially secrets — from being included in images. GeeksforGeeks' Dockerfile guide highlights this as an essential practice.
Kubo's security policies recommend non-root execution and .dockerignore usage as standards.
4. Leveraging BuildKit
BuildKit is Docker's next-generation build engine, offering parallel builds, improved caching, and security features.
Enabling BuildKit
# Enable via environment variable
export DOCKER_BUILDKIT=1
# Use docker buildx (recommended)
docker buildx build -t myapp:latest .
Cache Mounts
Share dependency package caches across builds to avoid reinstallation:
# Persist pip cache
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Persist npm cache
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Persist Go module cache
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/server .
# Persist apt cache
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y curl
Heredoc Syntax
# Traditional approach (cumbersome escaping)
RUN echo "server {" > /etc/nginx/conf.d/default.conf && \
echo " listen 80;" >> /etc/nginx/conf.d/default.conf && \
echo " location / { proxy_pass http://app:8080; }" >> /etc/nginx/conf.d/default.conf && \
echo "}" >> /etc/nginx/conf.d/default.conf
# BuildKit heredoc (readable)
COPY <<EOF /etc/nginx/conf.d/default.conf
server {
listen 80;
location / {
proxy_pass http://app:8080;
}
}
EOF
5. Image Size Reduction Techniques
Prefer COPY Over ADD
# Avoid: ADD downloads remote URLs and auto-extracts tarballs
ADD https://example.com/app.tar.gz /app/
# Preferred: COPY is explicit local file copy only
COPY app/ /app/
ADD can cause unexpected behavior — always use COPY for local files.
Eliminate Unnecessary Packages
# Exclude recommended packages with --no-install-recommends
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# On Alpine, disable apk cache with --no-cache
RUN apk add --no-cache curl
Multi-Stage Build Patterns
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/index.js"]
The DevOps Training Institute optimization guide reports that combining these techniques achieves over 80% size reduction.
Regular Image Rebuilds
Rebuild production images at least monthly to apply the latest base image security patches. Automate this with Dependabot or Renovate as Docker officially recommends.
6. Defining Health Checks
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/healthz || exit 1
The HEALTHCHECK instruction allows Docker to determine container health. While Kubernetes replaces this with liveness/readiness Probes, it remains valuable in Docker Compose environments.
Summary: 2025 Dockerfile Checklist
The definitive checklist for production-quality Dockerfiles:
- select minimal base images (alpine - distroless - scratch)
- Pin versions for reproducibility
- Place infrequently changing instructions first (cache optimization)
- Consolidate RUN instructions and clean caches
- Run as non-root user (UID 10000+)
- Minimize build context with .dockerignore
- Manage secrets via BuildKit mounts
- Separate production and development with multi-stage builds
- Prefer COPY over ADD
- Define HEALTHCHECK
- Schedule regular rebuild cycles
Kubo efficiently runs container images that follow these best practices on Kubernetes clusters. With Captain.AI, automated Dockerfile reviews and optimization suggestions elevate your entire team's Dockerfile quality.
For Dockerfile optimization and container operations consulting, please contact us.