Skip to content

Dockerfiles

Dockerfiles are text files containing instructions to build Docker images. This chapter covers writing effective Dockerfiles.

A Dockerfile is a script with instructions to build an image:

# Comment
FROM ubuntu:20.04
LABEL maintainer="you@example.com"
RUN apt-get update && apt-get install -y nginx
COPY index.html /var/www/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Sets the base image:

# Use official base image
FROM node:18-alpine
FROM python:3.11-slim
# Use specific version
FROM nginx:1.25
# Multi-stage build - first stage
FROM node:18 AS builder

Add metadata to the image:

LABEL version="1.0"
LABEL description="My web application"
LABEL maintainer="you@example.com"
LABEL "org.opencontainers.image.authors"="you@example.com"

Execute commands during build:

# Shell form
RUN apt-get update && apt-get install -y nginx
# Exec form (preferred)
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "nginx"]

Copy files from build context to image:

# Copy single file
COPY index.html /var/www/html/
# Copy directory
COPY src/ /var/www/html/src/
# Copy with ownership
COPY --chown=www-data:www-data . /var/www/html/

Similar to COPY but with additional features:

# Copy and extract tar files
ADD website.tar.gz /var/www/html/
# Copy from URL
ADD https://example.com/file.tar.gz /tmp/

Best Practice: Use COPY unless you need ADD’s features.

Set the working directory:

WORKDIR /app
WORKDIR /var/log
WORKDIR /workspace

Set environment variables:

ENV NODE_ENV=production
ENV PORT=8080
ENV DATABASE_URL=postgres://user:pass@db:5432/mydb

Document which ports the container listens on:

EXPOSE 80
EXPOSE 443
EXPOSE 8080 8081 8082

Set the user for subsequent commands:

# Create user
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
# Switch to user
USER appuser

Define build-time variables:

ARG VERSION=latest
ARG BUILD_DATE
# Use ARG
RUN echo "Building version $VERSION"

Configure the command that always executes:

# Exec form
ENTRYPOINT ["python", "app.py"]
# Shell form
ENTRYPOINT python app.py

Define the default command:

# Exec form (preferred)
CMD ["nginx", "-g", "daemon off;"]
# Shell form
CMD nginx -g "daemon off;"
# Default arguments for ENTRYPOINT
CMD ["--port", "8080"]
# Bad
FROM ubuntu
# Good
FROM ubuntu:20.04
# Better - Alpine based
FROM node:18-alpine
.dockerignore
.git
node_modules
npm-debug.log
.env
*.md
__pycache__
*.pyc
.vscode
.idea
# Bad - invalidates cache for every code change
COPY . .
RUN npm install
FROM node:18
# Good - cache npm install unless package.json changes
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# Bad - multiple RUN layers
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Good - single RUN layer
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -s /bin/sh -D appuser
# Set ownership
RUN chown -R appuser:appgroup /app
USER appuser
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
LABEL maintainer="you@example.com"
LABEL version="1.0.0"
LABEL description="My application"
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1000 -S nodejs && \
adduser -u 1000 -S nodejs -G nodejs
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["python", "main.py"]
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Production stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Terminal window
# Build with default tag
docker build .
# Build with custom tag
docker build -t myapp:latest .
# Build with build args
docker build --build-arg VERSION=1.0 -t myapp:1.0 .
# Build with no cache
docker build --no-cache -t myapp:latest .
# Build with target stage (multi-stage)
docker build --target builder -t myapp:builder .
# Build and pass secrets (for RUN --mount=type=cache)
docker build --secret id=npm,src=.npmrc -t myapp .
Terminal window
# Lint Dockerfile
docker build --check .
# Hadolint (external tool)
hadolint Dockerfile

Most editors support Dockerfile syntax highlighting:

  • VS Code: Docker extension
  • IntelliJ: Docker plugin
  • Sublime: Dockerfile syntax

In this chapter, you learned:

  • Dockerfile instructions (FROM, RUN, COPY, etc.)
  • Best practices for writing efficient Dockerfiles
  • Multi-stage builds
  • Security best practices
  • Building images from Dockerfiles