A Dockerfile is a text file of instructions Docker executes to build an image. Every line creates a cached layer — order matters for build speed and image size.

Dockerfile Instructions

Instruction Purpose
FROM Base image to start from
WORKDIR Set working directory inside container
COPY Copy files from host to image
ADD Like COPY but supports URLs and tar extraction
RUN Execute command during build
CMD Default command when container starts
ENTRYPOINT Main executable (harder to override)
ENV Set environment variables
EXPOSE Document which port the app listens on
USER Run subsequent commands as non-root user
ARG Build-time variables (not in final image)

Node.js Application

  # Dockerfile
FROM node:20-alpine

WORKDIR /app

# Copy dependency files first — cached unless they change
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

EXPOSE 3000

USER node

CMD ["node", "src/index.js"]
  

Build and run:

  docker build -t my-api:1.0 .
docker run -d -p 3000:3000 --name api my-api:1.0
curl http://localhost:3000/health
  

Python Application

  FROM python:3.12-slim

WORKDIR /app

# Prevent Python from writing .pyc and buffer stdout
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

RUN useradd -m appuser
USER appuser

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:application"]
  

Go Application — Multi-Stage Build

Multi-stage builds produce tiny production images by discarding build tools:

  # Stage 1: Build
FROM golang:1.22-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app .

# Stage 2: Runtime
FROM alpine:3.19

RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app .

EXPOSE 8080
USER nobody

ENTRYPOINT ["./app"]
  

Final image: ~15 MB instead of ~800 MB with the full Go toolchain.

.dockerignore

Exclude unnecessary files from the build context (faster builds, smaller images):

  node_modules
.git
.env
*.md
Dockerfile
.dockerignore
dist
coverage
__pycache__
*.pyc
.vscode
  

Layer Caching Strategy

Order instructions from least to most frequently changed:

  FROM node:20-alpine
WORKDIR /app
COPY package*.json ./     # Step 1 — changes rarely
RUN npm ci                # Step 2 — cached if package.json unchanged
COPY . .                  # Step 3 — changes every commit
CMD ["node", "index.js"]  # Step 4
  

Changing source code only rebuilds layers from COPY . . onward.

Environment Variables and Secrets

  ENV NODE_ENV=production
ENV PORT=3000

# Build-time only (not in final image layers if multi-stage)
ARG BUILD_VERSION
RUN echo "Building version ${BUILD_VERSION}"
  

Never put secrets in Dockerfiles. Pass at runtime:

  docker run -e DATABASE_URL=postgres://... my-api:1.0
  

Use Docker secrets or environment files in Compose for production.

Health Checks

  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
  

Or in docker-compose.yml:

  healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s
  timeout: 3s
  retries: 3
  

Tagging and Pushing Images

  # Build with tag
docker build -t myregistry.io/my-api:1.0.0 .
docker build -t myregistry.io/my-api:latest .

# Push to registry
docker login myregistry.io
docker push myregistry.io/my-api:1.0.0
docker push myregistry.io/my-api:latest
  

Use semantic versioning tags — never rely on latest alone in production.

Dockerfile Best Practices

  1. Use specific base image tagsnode:20-alpine not node:latest
  2. Prefer Alpine or slim variants — smaller attack surface
  3. Run as non-root userUSER node or create dedicated user
  4. One process per container — don’t run cron + app in one container
  5. Use multi-stage builds — separate build and runtime environments
  6. Minimize layers — combine RUN commands with &&
  7. Don’t install unnecessary packages — no curl/wget in production unless needed

Debugging Build Failures

  # Verbose build output
docker build --progress=plain --no-cache -t debug .

# Inspect intermediate layers
docker history my-api:1.0

# Shell into failed build stage
docker run -it <layer-id> sh
  

What Comes Next

Combine multiple containers with Docker Compose, then learn production hardening in Production Docker.