Building Custom Images
This lesson covers the complete process of building Docker images, from writing a Dockerfile to optimizing the build process.
The docker build Command
Basic build syntax:
docker build [OPTIONS] PATH|URL|-
# Build from current directory
docker build .
# Build with a tag
docker build -t myapp:1.0 .
# Build with multiple tags
docker build -t myapp:1.0 -t myapp:latest .
# Build from a different Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
Build Options
| Option | Description |
|---|---|
| -t, --tag | Name and optional tag (name:tag) |
| -f, --file | Specify Dockerfile path |
| --build-arg | Set build-time variables |
| --no-cache | Don't use cache |
| --pull | Always pull base image |
| --target | Build specific stage |
| --platform | Set target platform |
# Common build scenarios
docker build -t myapp:1.0 --no-cache .
docker build -t myapp:1.0 --build-arg VERSION=1.0 .
docker build -t myapp:1.0 --platform linux/amd64 .
docker build -t myapp:1.0 --target production .
Build Context
The build context is the set of files Docker can access during build:
# Current directory as context
docker build -t myapp .
# Specific directory
docker build -t myapp ./app
# Git repository
docker build -t myapp https://github.com/user/repo.git
# Git with branch
docker build -t myapp https://github.com/user/repo.git#branch
# Tar archive
docker build -t myapp - < app.tar.gz
Optimizing Build Context
# Large context slows builds
Sending build context to Docker daemon 500MB
# Use .dockerignore to reduce context
Sending build context to Docker daemon 2.5MB
Example .dockerignore:
# .dockerignore
node_modules
.git
*.md
.env*
Dockerfile*
docker-compose*
.dockerignore
logs/
coverage/
.nyc_output/
Layer Caching
Docker caches each layer. When a layer changes, all subsequent layers rebuild:
# Layer 1: Base image (cached until base changes)
FROM node:18-alpine
# Layer 2: Working directory (almost always cached)
WORKDIR /app
# Layer 3: Package files (cached until package.json changes)
COPY package*.json ./
# Layer 4: Dependencies (cached if package.json unchanged)
RUN npm install
# Layer 5: Source code (changes frequently)
COPY . .
# Layer 6: Build command
RUN npm run build
Cache Optimization Tips
Order by change frequency:
# Bad - npm install runs on every code change
COPY . .
RUN npm install
# Good - npm install only runs if package.json changes
COPY package*.json ./
RUN npm install
COPY . .
Separate build dependencies:
# Install build tools separately
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Then install application dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
Build Arguments
Pass variables at build time:
# Define argument
ARG NODE_ENV=production
ARG APP_VERSION
# Use in build
ENV NODE_ENV=${NODE_ENV}
RUN echo "Building version ${APP_VERSION}"
# Pass arguments during build
docker build \
--build-arg NODE_ENV=development \
--build-arg APP_VERSION=1.0.0 \
-t myapp:1.0 .
Default Values
# With default
ARG DEBUG=false
# Without default (must be provided)
ARG SECRET_KEY
Complete Build Example
Let's build a production-ready Node.js image:
Project Structure
myapp/
├── Dockerfile
├── .dockerignore
├── package.json
├── package-lock.json
└── src/
└── index.js
Dockerfile
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install all dependencies (including devDependencies)
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
# Security: run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production && npm cache clean --force
# Copy built application from builder stage
COPY --from=builder /app/dist ./dist
# Change ownership
RUN chown -R appuser:appgroup /app
# Switch to non-root user
USER appuser
# Document port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Start application
CMD ["node", "dist/index.js"]
.dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env*
Dockerfile*
docker-compose*
.dockerignore
coverage
.nyc_output
*.md
Build Commands
# Development build
docker build --target builder -t myapp:dev .
# Production build
docker build --target production -t myapp:prod .
# Build with version
docker build \
-t myapp:1.0.0 \
-t myapp:latest \
--build-arg APP_VERSION=1.0.0 \
.
Debugging Builds
Build Output
# Show all build output
docker build --progress=plain -t myapp .
# Build specific stage and stop
docker build --target builder -t myapp:debug .
# Run intermediate container
docker run -it myapp:debug sh
Layer Inspection
# View image layers
docker history myapp
# Detailed layer info
docker history --no-trunc myapp
BuildKit
Docker BuildKit provides better caching and output:
# Enable BuildKit
export DOCKER_BUILDKIT=1
# Build with BuildKit features
docker build -t myapp .
# Inline cache
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t myapp .
Build Cache Management
# Build without cache
docker build --no-cache -t myapp .
# Pull latest base image
docker build --pull -t myapp .
# Clear build cache
docker builder prune
# Clear all build cache
docker builder prune -a
Cross-Platform Builds
Build for different architectures:
# Build for specific platform
docker build --platform linux/amd64 -t myapp:amd64 .
docker build --platform linux/arm64 -t myapp:arm64 .
# Multi-platform with buildx
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:multi .
Build Verification
After building, verify your image:
# Check image was created
docker images myapp
# Inspect the image
docker inspect myapp
# Test run the container
docker run --rm myapp
# Check container logs
docker run -d --name test myapp
docker logs test
docker rm -f test
Key Takeaways
- Use
-tto tag images with meaningful names and versions - Order Dockerfile instructions by change frequency for optimal caching
- Use
.dockerignoreto reduce build context size - Pass build-time configuration with
--build-arg - Test images after building to verify correctness
- Use BuildKit for faster, more efficient builds
- Enable health checks for production images
- Build for multiple platforms when needed

