Build Workflow
Build container images with Docker/Podman, supporting multi-architecture builds and optimization techniques.
Quick Start
Detect runtime:
# Auto-detect (prefers Podman)
../tools/DetectRuntime.sh
# Specify runtime
../tools/DetectRuntime.sh --runtime docker
../tools/DetectRuntime.sh --runtime podman
Basic build:
docker build -t myapp:latest .
podman build -t myapp:latest .
Multi-architecture:
# Docker buildx
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
# Podman manifest
podman build --platform linux/amd64 -t myapp:amd64 .
podman build --platform linux/arm64 -t myapp:arm64 .
podman manifest create myapp:latest
podman manifest add myapp:latest myapp:amd64
podman manifest add myapp:latest myapp:arm64
Optimize with multi-stage:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine:3.19
COPY --from=builder /app/myapp /
CMD ["/myapp"]
Runtime Detection
Detect Container Runtime
Auto-detect:
../tools/DetectRuntime.sh
# Prefers Podman, falls back to Docker
Specify runtime:
../tools/DetectRuntime.sh --runtime docker
../tools/DetectRuntime.sh --runtime podman
Check available builders:
# Docker buildx
docker buildx ls
# Podman buildah
buildah version
Process Integration
- Parse user prompt for explicit runtime mention
- Run DetectRuntime.sh with appropriate flags
- Identify build tools (buildx, buildah)
- Use detected runtime for all commands
Building Images
Basic Build Process
Docker:
# Basic build
docker build -t myapp:latest .
# With build arguments
docker build --build-arg VERSION=1.0 -t myapp:1.0 .
# With specific target
docker build --target production -t myapp:prod .
# Build without cache
docker build --no-cache -t myapp:latest .
# Build with custom context
docker build -t myapp:latest -f path/to/Dockerfile context/
Podman:
# Basic build
podman build -t myapp:latest .
# With build arguments
podman build --build-arg VERSION=1.0 -t myapp:1.0 .
# With specific target
podman build --target production -t myapp:prod .
# Build without cache
podman build --no-cache -t myapp:latest .
# Build with custom context
podman build -t myapp:latest -f path/to/Dockerfile context/
Build Arguments and Tags
Multiple tags:
docker build -t myapp:latest -t myapp:1.0 -t myregistry/myapp:1.0 .
podman build -t myapp:latest -t myapp:1.0 -t myregistry/myapp:1.0 .
Build arguments:
ARG VERSION=1.0
ARG BUILD_DATE
ENV APP_VERSION=${VERSION}
RUN echo "Building version ${VERSION} on ${BUILD_DATE}"
docker build --build-arg VERSION=2.0 --build-arg BUILD_DATE=$(date -u +%Y-%m-%d) .
podman build --build-arg VERSION=2.0 --build-arg BUILD_DATE=$(date -u +%Y-%m-%d) .
Dockerfile Best Practices
Base image selection:
# Use specific tags, never :latest
FROM alpine:3.19
FROM python:3.11-slim
FROM node:20-alpine
# For static binaries
FROM scratch
FROM gcr.io/distroless/static-debian12
Layer ordering (cache optimization):
# Good: dependencies cached separately
FROM node:20-alpine
WORKDIR /app
# Copy dependency files first (changes less frequently)
COPY package*.json ./
RUN npm ci
# Copy code last (changes most frequently)
COPY . .
RUN npm run build
Non-root user:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
Minimal layers:
# Bad: creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Good: single layer with cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Using .dockerignore
Create .dockerignore to exclude unnecessary files:
# Version control
.git
.gitignore
# Dependencies
node_modules
vendor
# Build artifacts
dist
build
*.o
# Documentation
*.md
docs/
# Tests
*_test.go
test/
__tests__
# CI/CD
.github
.gitlab-ci.yml
# Development
.env
.vscode
*.log
# OS files
.DS_Store
Multi-Architecture Builds
Docker with Buildx
Setup QEMU:
docker run --privileged --rm tonistiigi/binfmt --install all
Create builder:
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
List available platforms:
docker buildx inspect --bootstrap | grep Platforms
Build multi-arch image:
# Build and push
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t myregistry/myapp:latest \
--push \
.
# Build single platform locally
docker buildx build \
--platform linux/arm64 \
-t myapp:arm64 \
--load \
.
Build with cache:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=myregistry/myapp:buildcache \
--cache-to type=registry,ref=myregistry/myapp:buildcache,mode=max \
-t myregistry/myapp:latest \
--push \
.
Manage builders:
# List builders
docker buildx ls
# Remove builder
docker buildx rm multiarch
# Use default builder
docker buildx use default
Podman with Buildah
Install QEMU:
# On host system
sudo dnf install qemu-user-static # Fedora/RHEL
sudo apt install qemu-user-static # Debian/Ubuntu
# Or with Podman
podman run --rm --privileged multiarch/qemu-user-static --reset -p yes
Build for specific platform:
podman build --platform linux/arm64 -t myapp:arm64 .
Build multi-arch with manifest:
# Build for each platform
podman build --platform linux/amd64 -t myapp:amd64 .
podman build --platform linux/arm64 -t myapp:arm64 .
podman build --platform linux/arm/v7 -t myapp:armv7 .
# Create manifest
podman manifest create myapp:latest
# Add images to manifest
podman manifest add myapp:latest myapp:amd64
podman manifest add myapp:latest myapp:arm64
podman manifest add myapp:latest myapp:armv7
# Push manifest
podman manifest push myapp:latest docker://myregistry/myapp:latest
Using buildah directly:
buildah bud --arch arm64 -t myapp:arm64 .
buildah bud --arch amd64 -t myapp:amd64 .
Platform Targeting
Common platforms:
| Platform | Description | Use Case |
|---|---|---|
| linux/amd64 | 64-bit x86 | Standard servers, desktops |
| linux/arm64 | 64-bit ARM | Raspberry Pi 4, Apple Silicon, AWS Graviton |
| linux/arm/v7 | 32-bit ARM v7 | Raspberry Pi 2/3 |
| linux/arm/v6 | 32-bit ARM v6 | Raspberry Pi Zero/1 |
| linux/386 | 32-bit x86 | Legacy systems |
Platform-aware Dockerfile:
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
# Build for target platform
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app .
FROM --platform=$TARGETPLATFORM alpine:latest
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
Conditional platform logic:
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then \
echo "Installing ARM64 packages"; \
elif [ "$TARGETARCH" = "amd64" ]; then \
echo "Installing AMD64 packages"; \
fi
Verify Multi-Arch Images
Inspect manifest (Docker):
docker buildx imagetools inspect myregistry/myapp:latest
docker manifest inspect myregistry/myapp:latest
Inspect manifest (Podman):
podman manifest inspect myapp:latest
skopeo inspect docker://myregistry/myapp:latest
Test platform-specific images:
# Force ARM64 on AMD64 host
docker run --platform linux/arm64 myregistry/myapp:latest
# Verify architecture
docker run --rm myregistry/myapp:latest uname -m
# Output: x86_64, aarch64, armv7l, etc.
Image Optimization
Multi-Stage Builds
Basic pattern:
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o myapp
# Runtime stage
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["/app/myapp"]
# Result: ~20 MB vs ~1 GB
Multiple build stages:
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Runtime
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Base Image Selection
Size comparison:
# Large (1.1 GB)
FROM ubuntu:latest
# Medium (130 MB)
FROM ubuntu:22.04
# Small (77 MB)
FROM debian:bookworm-slim
# Smaller (7 MB)
FROM alpine:3.19
# Smallest (< 2 MB)
FROM scratch
FROM gcr.io/distroless/static-debian12
Language-specific minimal images:
# Python
FROM python:3.11-slim # 50 MB vs 900 MB
FROM python:3.11-alpine # 45 MB
# Node.js
FROM node:20-slim # 240 MB vs 1 GB
FROM node:20-alpine # 180 MB
# Go
FROM golang:1.21-alpine # Build stage
FROM scratch # Runtime (static binary)
Layer Optimization
Combine RUN commands:
# Bad: creates 4 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean
# Good: single layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
rm -rf /var/lib/apt/lists/*
Clean up in same layer:
# Bad: file remains in layer
RUN wget https://example.com/large-file.tar.gz
RUN tar -xzf large-file.tar.gz
RUN rm large-file.tar.gz
# Good: cleanup in same layer
RUN wget https://example.com/large-file.tar.gz && \
tar -xzf large-file.tar.gz && \
rm large-file.tar.gz
Size Reduction Techniques
Remove package manager cache:
# APT (Debian/Ubuntu)
RUN apt-get update && \
apt-get install -y package && \
rm -rf /var/lib/apt/lists/*
# APK (Alpine)
RUN apk add --no-cache package
# DNF (Fedora/RHEL)
RUN dnf install -y package && \
dnf clean all
# npm
RUN npm ci --only=production && \
npm cache clean --force
# pip
RUN pip install --no-cache-dir -r requirements.txt
Minimize installed packages:
# Bad: installs many unnecessary packages
RUN apt-get update && \
apt-get install -y build-essential
# Good: minimal installation
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
make && \
rm -rf /var/lib/apt/lists/*
Remove build dependencies:
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
postgresql-dev && \
pip install psycopg2 && \
apk del .build-deps
Static Binaries
Go static binary:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
# Result: ~10 MB (just the binary)
Rust static binary:
FROM rust:1.75-alpine AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/myapp /myapp
ENTRYPOINT ["/myapp"]
Distroless Images
Language-specific distroless:
# Java
FROM gcr.io/distroless/java17-debian12
COPY target/app.jar /app.jar
CMD ["app.jar"]
# Python
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
WORKDIR /app
CMD ["app.py"]
# Node.js
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app /app
WORKDIR /app
CMD ["index.js"]
# Static binaries
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /
CMD ["/myapp"]
Build Cache Strategies
BuildKit cache mounts:
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine
WORKDIR /app
COPY go.* ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o myapp
Remote cache:
docker buildx build \
--cache-from type=registry,ref=myregistry/myapp:buildcache \
--cache-to type=registry,ref=myregistry/myapp:buildcache,mode=max \
-t myapp:latest .
Optimization Examples
Python Flask app (500 MB → 50 MB):
# Before
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
# After
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Node.js React app (1.2 GB → 100 MB):
# Before
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
# After
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Go microservice (800 MB → 10 MB):
# Before
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["/app/myapp"]
# After
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o myapp
FROM scratch
COPY --from=builder /app/myapp /
CMD ["/myapp"]
Analysis Tools
Check image size:
docker images myapp
podman images myapp
View image history:
docker history myapp:latest
docker history --no-trunc myapp:latest # Full commands
podman history myapp:latest
Analyze with dive:
# Install dive
brew install dive
# Analyze image
dive myapp:latest
Find large layers:
docker history myapp:latest --format "{{.Size}}\t{{.CreatedBy}}" | sort -h
Best Practices
Dockerfile Quality Checklist
- Use specific base image tags (not
latest) - Minimize number of layers
- Use multi-stage builds where appropriate
- Run as non-root user
- Have .dockerignore file
- No secrets embedded
- Proper layer ordering (least to most frequently changed)
- Install only required packages
- Clean up in same layer
- Use minimal base images
Build Optimization Checklist
- Leverage build cache effectively
- Combine RUN commands where appropriate
- Remove package manager cache
- Use
--no-install-recommends(apt) - Use
--no-cache(apk) - Remove build dependencies after use
- Exclude development files via .dockerignore
- Order layers by change frequency
Security Best Practices
- Use specific version tags
- Update packages during build
- Run as non-root user
- Don’t include secrets in image
- Scan for vulnerabilities
- Use minimal base images (smaller attack surface)
- Consider distroless for production
Multi-Arch Best Practices
- Use multi-stage builds for smaller final images
- Leverage build cache with remote cache
- Test on actual hardware when possible
- Use QEMU for cross-platform testing
- Build platform-specific optimizations with TARGETARCH
- Verify critical functionality on each platform
CI/CD Integration
GitHub Actions:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm64
push: true
tags: myregistry/myapp:latest
GitLab CI:
build-multiarch:
image: docker:latest
services:
- docker:dind
before_script:
- docker buildx create --use
- docker buildx install
script:
- docker buildx build
--platform linux/amd64,linux/arm64
-t myregistry/myapp:latest
--push .
Troubleshooting
Build Failures
Issue: Build fails with permission denied
Docker:
- User not in docker group:
sudo usermod -aG docker $USER - Daemon not running:
sudo systemctl start docker
Podman:
- Check rootless permissions:
podman system migrate - Check user namespace:
cat /etc/subuid /etc/subgid
Issue: Build cache not working
- Check layer ordering (copy dependencies before code)
- Verify .dockerignore excludes frequently changing files
- Use
--no-cacheto force rebuild if needed
Issue: Large image size
- Review Dockerfile layer ordering
- Implement multi-stage builds
- Use smaller base images
- Clean up in same layer
Multi-Arch Issues
Issue: Build very slow for non-native platforms
- Cause: QEMU emulation overhead
- Solution: Use native builders or accept slower builds
Issue: Build fails with “exec format error”
- Cause: QEMU not configured
- Solution:
docker run --privileged --rm tonistiigi/binfmt --install all
Issue: Cannot load multi-platform image locally
- Cause: Local load only supports single platform
- Solution: Use
--platformto specify one, or push to registry
Issue: Different behavior on different platforms
- Cause: Platform-specific dependencies
- Solution: Review Dockerfile for cross-platform compatibility
Issue: Manifest push fails
- Cause: Authentication or registry doesn’t support manifests
- Solution: Verify registry supports manifest lists, check credentials
Optimization Issues
Issue: Removing files doesn’t reduce image size
- Cause: Files removed in different layer
- Solution: Clean up in same RUN command
Issue: Dependencies installed but not needed
- Cause: Installing dev dependencies in production
- Solution: Use
npm ci --only=production,pip install --no-cache-dir
Issue: .dockerignore not working
- Cause: File in wrong location or syntax error
- Solution: Place in build context root, verify syntax
Output Examples
After successful build:
✓ Image built successfully
Runtime: docker
Image: myapp:latest
Size: 125 MB
Build time: 45s
Next steps:
- Run the image: docker run myapp:latest
- Scan for vulnerabilities: [trigger SecurityScan]
- Push to registry: [trigger RegistryManage]
After multi-arch build:
✓ Multi-architecture image built successfully
Runtime: docker buildx
Image: myregistry/myapp:latest
Platforms built:
- linux/amd64 (152 MB)
- linux/arm64 (148 MB)
- linux/arm/v7 (145 MB)
Total build time: 8m 32s
Verify:
docker buildx imagetools inspect myregistry/myapp:latest
Test platforms:
docker run --platform linux/arm64 myregistry/myapp:latest
After optimization:
✓ Image optimized successfully
Image: myapp:latest
Size reduction:
Before: 850 MB
After: 45 MB
Savings: 805 MB (94.7%)
Layers:
Before: 15 layers
After: 5 layers
Optimizations applied:
- Changed base image: python:3.11 → python:3.11-slim
- Added multi-stage build
- Combined RUN commands (4 → 1)
- Removed package cache
- Added .dockerignore (excluded 250 MB)
Build time:
Before: 3m 45s
After: 1m 20s (faster caching)
Quick Reference
Basic builds:
docker build -t myapp:latest .
podman build -t myapp:latest .
Multi-architecture:
# Docker
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
# Podman
podman build --platform linux/amd64 -t myapp:amd64 .
podman manifest create myapp:latest
podman manifest add myapp:latest myapp:amd64
Analyze image:
docker history myapp:latest
dive myapp:latest
Multi-stage pattern:
FROM golang:1.21-alpine AS builder
# ... build ...
FROM alpine:3.19
COPY --from=builder /app/binary /
Optimize layers:
RUN apk add --no-cache package && \
# work && \
cleanup