Building Custom Docker Images
Complete guide to building custom Docker images including Dockerfile best practices, multi-stage builds, layer caching, and image optimization.
# Building Custom Docker Images
Introduction
Building custom Docker images is a fundamental skill for containerized applications. This tutorial covers Dockerfile syntax, best practices, multi-stage builds, layer caching, image optimization, and security considerations.
Learning Objectives
By the end of this tutorial, you will be able to:
- Write efficient Dockerfiles
- Use multi-stage builds to reduce image size
- Optimize Docker layer caching
- Build and tag images
- Optimize image size and build time
- Apply security best practices for images
Technical Background
Dockerfile Instructions
| Instruction | Description | |-------------|-------------| | FROM | Base image | | RUN | Execute command | | COPY | Copy files from host | | ADD | Copy files (with tar/URL support) | | ENV | Set environment variable | | EXPOSE | Document port | | CMD | Default command | | ENTRYPOINT | Executable for container | | WORKDIR | Working directory | | USER | User to run as | | ARG | Build-time variable | | LABEL | Metadata | | HEALTHCHECK | Health check command | | ONBUILD | Trigger on child build | | STOPSIGNAL | Signal to stop container |
Multi-Stage Build
Stage 1: Build (golang:1.21)
- Copy source
- Compile binary
Stage 2: Runtime (alpine:latest)
- Copy binary from Stage 1
- Run binaryPrerequisites
- Docker installed and running
- Application code to containerize
Hardware and Software Requirements
| Component | Requirement | |-----------|-------------| | Docker | v24+ |
Step-by-Step Implementation
Phase 1: Basic Dockerfile
# Dockerfile for Node.js application
FROM node:20-alpine
WORKDIR /app
# Copy package files first (layer caching)
COPY package*.json ./
RUN npm ci
# Copy application code
COPY . .
# Build application
RUN npm run build
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
# Expose port
EXPOSE 3000
# Run as non-root
USER node
# Start application
CMD ["node", "server.js"]Phase 2: Build Image
# Build with tag
docker build -t myapp:1.0.0 .
# Build with multiple tags
docker build -t myapp:1.0.0 -t myapp:latest .
# Build from specific Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
# Build with build arguments
docker build --build-arg VERSION=1.0.0 -t myapp:1.0.0 .
# View build history
docker history myapp:1.0.0Phase 3: Multi-Stage Build
#### Java Application
# Build stage
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
# Runtime stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]#### Node.js Application
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]Phase 4: Layer Caching Optimization
#### Optimized Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copy only package files first (cache layer)
COPY package*.json ./
RUN npm ci && npm cache clean --force
# Copy source code (invalidates only this layer on changes)
COPY . .
RUN npm run build
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]#### .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
Dockerfile
.dockerignore
.env
coverage
dist
build
*.mdPhase 5: Image Size Optimization
#### Remove Unnecessary Files
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
curl \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean#### Use Distroless Images
# Google distroless images (minimal, no shell)
FROM gcr.io/distroless/java-debian11:17
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]Phase 6: Image Security
# Run as non-root
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Drop capabilities (at runtime)
# docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Read-only root filesystem (at runtime)
# docker run --read-only --tmpfs /tmp myapp
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1Phase 7: Image Tagging Strategy
# Semantic versioning
docker build -t myapp:1.2.3 .
docker build -t myapp:1.2 .
docker build -t myapp:1 .
# Git commit SHA
docker build -t myapp:abc1234 .
# Environment tags
docker build -t myapp:production .
docker build -t myapp:staging .Expected Output
- Optimized Docker images built
- Multi-stage builds reducing image size
- Layer caching working
- Security best practices applied
Verification Procedures
| Check | Command | Expected Result | |-------|---------|-----------------| | Image built | `docker images` | Image listed with correct tag | | Image size | `docker images` | Reasonable size (<100MB for simple apps) | | Build history | `docker history` | Multiple layers visible | | Container runs | `docker run <image>` | Application starts successfully |
Troubleshooting Scenarios
| Issue | Diagnosis | Resolution | |-------|-----------|------------| | Build fails | Dockerfile syntax error | Check syntax; validate with `docker build` | | Image too large | Unnecessary files or layers | Use multi-stage build; add .dockerignore | | Build cache not working | COPY order wrong | Copy dependency files before source | | Permission denied | Running as root | Add USER directive |
Best Practices
- **Pin base images:** Use specific versions (e.g., `node:20-alpine`)
- **Use .dockerignore:** Exclude unnecessary files
- **Combine RUN commands:** Reduce layers
- **Clean up in same layer:** Remove temp files
- **Multi-stage builds:** Reduce final image size
- **Run as non-root:** Add USER directive
- **Add HEALTHCHECK:** Enable health monitoring
- **Tag properly:** Use semantic versioning or commit SHA
Security Considerations
- **Scan images:** Use Trivy, Docker Scout, or Snyk
- **Run as non-root:** Never run containers as root
- **Drop capabilities:** Use --cap-drop=ALL at runtime
- **Read-only filesystem:** Use --read-only where possible
- **Content trust:** Enable Docker Content Trust
- **Base image vulnerabilities:** Regularly update base images
Conclusion
Building custom Docker images efficiently requires understanding Dockerfile best practices, multi-stage builds, layer caching, and image optimization. By following these practices, you can create small, secure, and fast-to-build images for your containerized applications.
References
- Dockerfile Reference: https://docs.docker.com/engine/reference/builder/
- Multi-Stage Builds: https://docs.docker.com/build/building/multi-stage/
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
- Docker Scout: https://docs.docker.com/scout/