N-Docs LogoN-Docs

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.2

WORKDIR

Sets the working directory for subsequent instructions.

WORKDIR /app
WORKDIR /usr/src/app

COPY 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-extracts

RUN

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 --force

ENV

Sets environment variables.

ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgresql://localhost:5432/myapp

EXPOSE

Documents which ports the container listens on.

EXPOSE 3000
EXPOSE 80 443

CMD 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 argument

Multi-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-node

2. 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_output

4. 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 node

5. 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 ci

6. 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:latest

Common 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:latest

2. 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:latest

3. Keep Images Updated

# Pin specific versions but update regularly
FROM node:18.17.0-alpine3.18

# Update base image regularly
RUN apk update && apk upgrade

4. Don't Include Secrets

# ❌ Never do this
ENV API_KEY=secret123
COPY .env .

# ✅ Use runtime secrets instead
# Pass via environment variables or mounted secrets

Debugging 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 sh

Performance 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 build

2. 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?