Learn Dockerfile from A to Z: concepts, structure, essential instructions, and best practices. A comprehensive guide to writing optimized Dockerfiles with real-world examples for developers.

No table of contents available for this article
A Dockerfile is a text file containing a set of instructions for Docker to automatically build a Docker Image. Think of a Dockerfile as a "recipe" — listing each step and ingredient needed to create the final "dish," which is the Docker Image.
A Dockerfile has no file extension. The default filename is Dockerfile (with a capital D), placed in the project's root directory.
my-project/
├── src/
├── package.json
├── Dockerfile ← This file
└── .dockerignoreInstead of manually installing each library and configuring each environment variable, you define everything once in the Dockerfile. Every time you run docker build, Docker automatically performs all the steps.
Dockerfiles ensure everyone on the team and every environment (development, staging, production) builds identical Images. No more "it works on my machine" problems.
Dockerfiles are code, so you can:
Store them in Git
Review changes via Pull Requests
Rollback to previous versions when needed
Track change history
The Dockerfile itself documents how the application is set up. New team members can read the Dockerfile to understand what the application needs to run.
When you run docker build, Docker will:
Read the Dockerfile from top to bottom
Execute each instruction in order
Create a new layer after each instruction
Cache layers for reuse in subsequent builds
Output the final Docker Image
bash
# Build image syntax
docker build -t <image-name>:<tag> <path-to-dockerfile>
# Example
docker build -t my-app:1.0 .The . at the end means Docker will look for the Dockerfile in the current directory.
A basic Dockerfile typically has this structure:
dockerfile
# 1. Base image - starting point
FROM node:18-alpine
# 2. Metadata (optional)
LABEL maintainer="dev@example.com"
# 3. Environment setup
ENV NODE_ENV=production
WORKDIR /app
# 4. Copy files and install dependencies
COPY package*.json ./
RUN npm install --production
# 5. Copy source code
COPY . .
# 6. Expose port
EXPOSE 3000
# 7. Command to run when container starts
CMD ["node", "server.js"]FROM is a required instruction and must come first (except for ARG). It specifies the base image you'll build upon.
dockerfile
# Syntax
FROM <image>:<tag>
# Examples
FROM ubuntu:22.04
FROM node:18-alpine
FROM python:3.11-slim
FROM golang:1.21Notes on choosing base images:
alpine — Ultra-lightweight image (a few MBs), based on Alpine Linux. Ideal for production.
slim — Version with unnecessary packages removed, lighter than the full version.
buster/bullseye — Based on Debian, more feature-complete.
No tag — Defaults to latest, not recommended as it may break when the image updates.
dockerfile
# ❌ Not recommended - can break anytime
FROM node
# ✅ Recommended - pinned version
FROM node:18.19-alpineWORKDIR sets the working directory for subsequent instructions (RUN, CMD, COPY, ADD...). If the directory doesn't exist, Docker creates it.
dockerfile
WORKDIR /app
# All following commands run in /app
COPY . . # Copies to /app
RUN npm install # Runs npm install in /appWhy use WORKDIR instead of RUN cd?
dockerfile
# ❌ Not recommended
RUN cd /app && npm install
# ✅ Recommended
WORKDIR /app
RUN npm installRUN cd only affects that single layer. WORKDIR affects all subsequent instructions.
Both copy files from host to image, but they differ:
COPY — Simple copy, recommended for use.
dockerfile
# Syntax
COPY <src> <dest>
# Examples
COPY package.json /app/
COPY . /app/
COPY ["file with space.txt", "/app/"]ADD — Has 2 additional features:
Automatically extracts tar files
Can download from URLs
dockerfile
# Auto-extract
ADD archive.tar.gz /app/
# Download from URL (not recommended)
ADD https://example.com/file.txt /app/Best practice: Always use COPY unless you need ADD's special features.
RUN executes commands during image build and creates a new layer.
dockerfile
# Shell form - runs through shell (/bin/sh -c)
RUN apt-get update && apt-get install -y curl
# Exec form - runs directly, without shell
RUN ["apt-get", "update"]Combine multiple RUNs to reduce layers:
dockerfile
# ❌ Not optimized - creates 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
# ✅ Optimized - creates only 1 layer
RUN apt-get update && \
apt-get install -y \
curl \
wget && \
rm -rf /var/lib/apt/lists/*Both define commands to run when a container starts, but serve different purposes:
CMD — Default command, can be overridden when running the container.
dockerfile
CMD ["node", "server.js"]bash
# Run with default CMD
docker run my-app
# → Executes: node server.js
# Override CMD
docker run my-app node other-script.js
# → Executes: node other-script.jsENTRYPOINT — Fixed command, cannot be overridden (unless using --entrypoint).
dockerfile
ENTRYPOINT ["node"]
CMD ["server.js"]bash
# CMD is appended to ENTRYPOINT
docker run my-app
# → Executes: node server.js
# Only the CMD part changes
docker run my-app other-script.js
# → Executes: node other-script.jsWhen to use which?
CMD — When you want to allow users to change the command
ENTRYPOINT — When the container is a fixed executable
Both combined — ENTRYPOINT is the command, CMD provides default arguments
dockerfile
# Syntax 1
ENV NODE_ENV=production
# Syntax 2 (multiple variables)
ENV NODE_ENV=production \
PORT=3000 \
DB_HOST=localhostEnvironment variables can be used in subsequent instructions:
dockerfile
ENV APP_HOME=/app
WORKDIR $APP_HOME
COPY . $APP_HOMEARG defines variables that only exist during the build process (not in the running container).
dockerfile
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
ARG BUILD_DATE
LABEL build-date=$BUILD_DATEbash
# Pass values when building
docker build --build-arg NODE_VERSION=20 --build-arg BUILD_DATE=$(date +%Y-%m-%d) .EXPOSE declares which port the container will listen on. This is only documentation, it doesn't actually open the port.
dockerfile
EXPOSE 3000
EXPOSE 3000/tcp
EXPOSE 3000/udpTo actually map the port, use the -p flag when running the container:
bash
docker run -p 8080:3000 my-app
# Host port 8080 → Container port 3000dockerfile
VOLUME ["/data"]
VOLUME /data /logsCreates a mount point to persist data or share data between containers.
By default, containers run as root. For security reasons, switch to a non-root user:
dockerfile
# Create new user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Switch to that user
USER appuser
# Subsequent commands run as appuser
CMD ["node", "server.js"]dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1interval — Time between health checks
timeout — Maximum time for each check
start-period — Wait time before starting checks
retries — Number of retries before marking unhealthy
Multi-stage builds allow you to use multiple FROM instructions in one Dockerfile, separating build and runtime environments.
Problem: Image contains build tools unnecessary for production.
dockerfile
# ❌ Single stage - Large image with unnecessary stuff
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
# Result: ~1GB ImageSolution: Multi-stage build
dockerfile
# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
# Result: ~150MB ImageExplanation:
Stage 1 (builder): Installs all dependencies, builds the application
Stage 2 (production): Only copies necessary files from stage 1
Final image only contains runtime and built code, no devDependencies or source code
dockerfile
FROM node:18-alpine
# Create non-root user
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs
WORKDIR /app
# Copy package files first to leverage cache
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]dockerfile
FROM python:3.11-slim
# Prevent Python from writing pyc files and buffering stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY . .
# Create non-root user
RUN useradd --create-home appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]dockerfile
# Stage 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Stage 2: Runtime
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]dockerfile
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: Runtime
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Create a .dockerignore file to exclude unnecessary files:
node_modules
npm-debug.log
.git
.gitignore
.env
*.md
Dockerfile
.dockerignore
dist
coverage
.nyc_outputOrder instructions from least to most frequently changed:
dockerfile
# ✅ Optimized caching
FROM node:18-alpine
WORKDIR /app
# Rarely changes - cache is utilized
COPY package*.json ./
RUN npm install
# Frequently changes - rebuild from here
COPY . .
CMD ["node", "server.js"]Each container should run a single process. If you need multiple services, use Docker Compose.
dockerfile
# ❌ Not recommended
CMD service nginx start && node server.js
# ✅ Recommended - split into 2 separate containersdockerfile
# ❌ Not recommended
FROM node:latest
FROM node
# ✅ Recommended
FROM node:18.19.0-alpine3.18dockerfile
# ❌ Many layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*
# ✅ Single layer
RUN apt-get update && \
apt-get install -y curl wget && \
rm -rf /var/lib/apt/lists/*dockerfile
# ❌ Doesn't reduce size - cache remains in previous layer
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ Actually reduces size
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserUse tools like docker scan or Trivy to check for security vulnerabilities:
bash
docker scan my-image:latest
trivy image my-image:latestCOPY failed: file not found in build contextCause: File is excluded in .dockerignore or doesn't exist.
Solution: Check .dockerignore and file paths.
Causes:
Not using multi-stage build
Base image too heavy
Not cleaning up cache
Solutions:
Use multi-stage build
Choose alpine or slim images
Clean up in the same RUN layer
Cause: Instruction order not optimized.
Solution: Place rarely-changing instructions first.
Cause: Files copied with root permissions, but container runs as different user.
Solution:
dockerfile
COPY --chown=appuser:appgroup . .Dockerfile is the foundation of Docker — understanding how to write optimized Dockerfiles will help you:
Build images faster by leveraging cache
Create compact, secure images for production
Easily maintain and debug
Work more efficiently in teams
Start with simple examples, then gradually apply multi-stage builds and best practices to enhance your Docker skills.
Dockerfile Reference: https://docs.docker.com/engine/reference/dockerfile/
Best practices for writing Dockerfiles: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
Multi-stage builds: https://docs.docker.com/build/building/multi-stage/
Docker Security Best Practices: https://docs.docker.com/develop/security-best-practices/