Dockerfile
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
- Use specific base image tags —
node:20-alpinenotnode:latest - Prefer Alpine or slim variants — smaller attack surface
- Run as non-root user —
USER nodeor create dedicated user - One process per container — don’t run cron + app in one container
- Use multi-stage builds — separate build and runtime environments
- Minimize layers — combine RUN commands with
&& - 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.