Skip to main content

Dockerfile Tutorial

From basics to production: master Docker image creation with practical examples and battle-tested practices.

15 min read
All Levels
Production Ready

Quick Start

Build your first Docker image in 5 minutes:

Python Web App

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]

Build & Run

docker build -t my-app . docker run -p 8000:8000 my-app

Pro Tip

Create a .dockerignore to exclude node_modules, .git, *.log. Auto-generate one →

Fundamentals

A Dockerfile is a blueprint for building container images. It's a text file with commands that Docker executes to assemble your app environment.

Think of it as a recipe: install dependencies, copy files, configure settings, define startup—all automated and reproducible.

Key Terms

Image
Read-only template built from Dockerfile
Container
Running instance of an image
Layer
Each instruction creates a cached layer
Build Context
Files available during build (minimize it!)

Syntax

Simple, declarative syntax:

# Comments start with hash
FROM ubuntu:22.04
LABEL maintainer="[email protected]"
RUN apt-get update && apt-get install -y python3
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
  • Instructions are UPPERCASE (convention, not required)
  • Each instruction = new layer
  • Order matters—place frequently changing commands last
  • Use \ for multi-line commands

Core Instructions

Command Purpose Example
FROM Base image (required first) FROM node:18-alpine
RUN Execute commands RUN npm install
COPY Copy files from host COPY . /app
WORKDIR Set working directory WORKDIR /app
EXPOSE Document ports EXPOSE 3000
CMD Default startup command CMD ["npm", "start"]

Advanced Instructions

ENV - Environment Variables

Set variables available at runtime:

ENV NODE_ENV=production \
    PORT=3000

ARG - Build Arguments

Variables only available during build:

ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

# docker build --build-arg NODE_VERSION=20 .

ENTRYPOINT vs CMD

ENTRYPOINT sets main command; CMD provides default args:

ENTRYPOINT ["python"]
CMD ["app.py"]

# Runs: python app.py
# Override: docker run myapp script.py → python script.py

HEALTHCHECK

Monitor container health:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1

USER - Security Essential

Never run as root in production:

RUN addgroup -g 1001 appuser && \
    adduser -S appuser -u 1001
USER appuser

Real-World Examples

Node.js

Production
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

Python FastAPI

Production
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]

Go (Multi-Stage)

Optimized
# Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o main .

# Production
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main .
RUN adduser -D -u 1000 appuser && chown appuser:appuser main
USER appuser
EXPOSE 8080
CMD ["./main"]

Laravel

Production
FROM php:8.2-fpm-alpine
RUN apk add --no-cache libpng-dev libzip-dev zip
RUN docker-php-ext-install pdo pdo_mysql zip gd
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY composer.* ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY . .
RUN chown -R www-data:www-data /var/www
USER www-data
EXPOSE 9000
CMD ["php-fpm"]

Best Practices

Use Minimal Base Images

Alpine images are 5-10x smaller:

✓ ~170MB

FROM node:18-alpine

✗ ~700MB+

FROM ubuntu:latest

Chain Commands & Clean Up

Combine operations, remove temp files in one layer:

RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/*

Use .dockerignore

Exclude unnecessary files:

node_modules
.git
*.log
.env
coverage

Generate .dockerignore →

Optimize Layer Caching

Copy dependencies before source code:

# Dependencies (rarely change) → cached
COPY package*.json ./
RUN npm ci

# Source (changes often) → rebuilt
COPY . .

Pin Specific Versions

Ensure reproducible builds:

✓ Specific

FROM node:18.17.0-alpine3.18

✗ Unpredictable

FROM node:latest

Multi-Stage Builds

Why? Build tools add 100s of MBs. Compile in one stage, copy only artifacts to a minimal runtime image.

# 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
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production && \
    addgroup -g 1001 nodejs && \
    adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]

Single Stage

  • 📦 450MB+
  • 🔨 Build tools
  • 📚 All deps
  • 🐌 Slow

Multi-Stage

  • 📦 120MB
  • ✨ Production only
  • 🎯 Runtime deps
  • 🚀 Fast

Security

Never Run as Root

Create and switch to non-root user:

RUN addgroup -g 1001 appuser && \
    adduser -S appuser -u 1001
USER appuser

Never Hardcode Secrets

Use environment variables at runtime:

✗ Never

ENV API_KEY="secret123"

✓ Runtime

docker run -e API_KEY=$API_KEY app

Scan for Vulnerabilities

Regularly scan images:

# Docker Scout
docker scout cves my-image:latest

# Trivy
trivy image my-image:latest

Minimize Attack Surface

  • Use minimal base images (Alpine, distroless)
  • Remove unnecessary packages
  • No debug tools in production
  • Use multi-stage builds

Optimization

Layer Caching

Order by change frequency:

  1. Base image (rarely)
  2. System packages
  3. App dependencies
  4. Source code (often)

Enable BuildKit

Better caching & parallelization:

DOCKER_BUILDKIT=1 docker build -t app .

Prefer COPY Over ADD

COPY is transparent; ADD has hidden features (auto-extraction).

Clean in Same Layer

Remove temp files immediately:

RUN wget file.tar.gz && \
    tar -xzf file.tar.gz && \
    rm file.tar.gz

Common Mistakes

❌ Using :latest Tag

:latest is unpredictable—breaks reproducibility

Wrong

FROM node:latest

Right

FROM node:18.17.0-alpine

❌ Separating apt-get update

Causes cache issues with outdated packages

Wrong

RUN apt-get update RUN apt-get install -y pkg

Right

RUN apt-get update && apt-get install -y pkg

❌ Installing Dev Dependencies

Bloats production images unnecessarily

Wrong

RUN npm install

Right

RUN npm ci --only=production

❌ No .dockerignore File

Slows builds, increases image size

Generate one →

Wrong

Missing .dockerignore

Right

Create .dockerignore

Troubleshooting

! Build is Very Slow

Causes

  • Large build context
  • Poor layer caching
  • Network issues

Solutions

Create .dockerignore • Copy deps before source • Enable BuildKit • Use --no-cache

Generate .dockerignore →

! Container Exits Immediately

Causes

  • No foreground process
  • App crashes
  • Wrong CMD format

Solutions

Check logs: docker logs <id> • Run interactively: docker run -it image sh • Ensure CMD runs in foreground

! Permission Denied

Causes

  • Non-root user lacks ownership
  • Incorrect permissions

Solutions

Use COPY --chown=user:group • Run chown after copying • Set chmod on directories

! Image Too Large

Causes

  • Full OS image
  • Build dependencies
  • Package caches

Solutions

Use Alpine/distroless • Multi-stage builds • Clean caches • Analyze: docker history

Ready to Build?

Browse production-ready Dockerfile templates for your stack.

Browse Templates