Building Custom Docker Images

4 Nov 2026, 11:2715 min readby ITYantra Admin

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 binary

Prerequisites

  • 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.0

Phase 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
*.md

Phase 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 1

Phase 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/
#best-practices#layer-caching#image-optimization#multi-stage-build#image-build#dockerfile#Docker