Dockerfile Best Practices
Complete guide to writing efficient and secure Dockerfiles
Dockerfile Best Practices
What is a Dockerfile?
A Dockerfile is a text file that contains instructions for building a Docker image. It defines the environment, dependencies, and configuration needed to run your application.
Basic Dockerfile Structure
# Use official base image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Define startup command
CMD ["npm", "start"]Dockerfile Instructions
FROM
Specifies the base image to build upon.
# Official images (recommended)
FROM node:18-alpine
FROM python:3.11-slim
FROM nginx:alpine
# Specific versions (recommended for production)
FROM ubuntu:22.04
FROM postgres:15.2WORKDIR
Sets the working directory for subsequent instructions.
WORKDIR /app
WORKDIR /usr/src/appCOPY vs ADD
- COPY: Simple file/directory copying (preferred)
- ADD: Has additional features (auto-extraction, URL support)
# Preferred - simple and predictable
COPY package.json ./
COPY src/ ./src/
# Use ADD only when you need its special features
ADD https://example.com/file.tar.gz /tmp/
ADD archive.tar.gz /opt/ # Auto-extractsRUN
Executes commands during image build.
# Install packages
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Multiple commands in one RUN (reduces layers)
RUN npm install && \
npm run build && \
npm cache clean --forceENV
Sets environment variables.
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgresql://localhost:5432/myappEXPOSE
Documents which ports the container listens on.
EXPOSE 3000
EXPOSE 80 443CMD vs ENTRYPOINT
- CMD: Default command (can be overridden)
- ENTRYPOINT: Always executed (cannot be overridden)
# CMD - can be overridden
CMD ["npm", "start"]
CMD ["python", "app.py"]
# ENTRYPOINT - always runs
ENTRYPOINT ["python", "app.py"]
# Combined usage
ENTRYPOINT ["python", "app.py"]
CMD ["--help"] # Default argumentMulti-Stage Builds
Reduce final image size by using multiple build stages.
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["npm", "start"]Best Practices
1. Use Official Base Images
# ✅ Good - official images
FROM node:18-alpine
FROM python:3.11-slim
FROM nginx:alpine
# ❌ Avoid - unofficial images
FROM someuser/custom-node2. Minimize Layers
# ❌ Bad - creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# ✅ Good - single layer
RUN apt-get update && \
apt-get install -y curl git && \
rm -rf /var/lib/apt/lists/*3. Use .dockerignore
Create a .dockerignore file to exclude unnecessary files:
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.nyc_output4. Don't Run as Root
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Switch to non-root user
USER nextjs
# Or use existing user in base image
USER node5. Optimize for Caching
# ✅ Good - copy dependencies first (better caching)
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ Bad - copy everything first (cache invalidated often)
COPY . .
RUN npm ci6. Use Specific Tags
# ✅ Good - specific versions
FROM node:18.17.0-alpine3.18
FROM postgres:15.3
# ❌ Bad - latest tag (unpredictable)
FROM node:latest
FROM postgres:latestCommon Patterns
Node.js Application
FROM node:18-alpine
# Create app directory
WORKDIR /usr/src/app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy app source
COPY --chown=nextjs:nodejs . .
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start application
CMD ["npm", "start"]Python Application
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Create app directory
WORKDIR /app
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser
# Install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first (better caching)
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
# Start application
CMD ["python", "app.py"]Static Website (Nginx)
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Security Best Practices
1. Scan for Vulnerabilities
# Use Docker Scout (built-in)
docker scout cves myapp:latest
# Use Trivy
trivy image myapp:latest
# Use Snyk
snyk container test myapp:latest2. Use Minimal Base Images
# ✅ Minimal images
FROM alpine:3.18
FROM node:18-alpine
FROM python:3.11-slim
FROM scratch # For static binaries
# ❌ Full OS images (larger attack surface)
FROM ubuntu:latest
FROM centos:latest3. Keep Images Updated
# Pin specific versions but update regularly
FROM node:18.17.0-alpine3.18
# Update base image regularly
RUN apk update && apk upgrade4. Don't Include Secrets
# ❌ Never do this
ENV API_KEY=secret123
COPY .env .
# ✅ Use runtime secrets instead
# Pass via environment variables or mounted secretsDebugging Dockerfiles
Build with Debug Output
# Build with progress output
docker build --progress=plain -t myapp .
# Build specific stage
docker build --target=builder -t myapp:builder .
# Build without cache
docker build --no-cache -t myapp .Inspect Layers
# View image layers
docker history myapp:latest
# Inspect image details
docker inspect myapp:latest
# Run intermediate stage for debugging
docker run -it myapp:builder shPerformance Optimization
1. Order Instructions by Change Frequency
# ✅ Good order (least to most frequently changed)
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # Changes less frequently
RUN npm ci
COPY . . # Changes most frequently
RUN npm run build2. Use Build Cache Effectively
# Use BuildKit for better caching
export DOCKER_BUILDKIT=1
docker build -t myapp .
# Use cache from another image
docker build --cache-from myapp:latest -t myapp:new .3. Minimize Image Size
# Use multi-stage builds
FROM node:18 AS builder
# ... build steps ...
FROM node:18-alpine AS production
COPY --from=builder /app/dist ./dist
# Clean up in same layer
RUN apt-get update && \
apt-get install -y package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*What's Next?
- Docker Compose Guide - Multi-container applications
- Docker Networking - Container networking concepts
- Kubernetes - Container orchestration