Cloud Native 15 min read

How to Build Lean, Reusable Docker Images: 5 Steps to Standardized Container Management

This article shares five practical steps and three core principles for creating minimal, reproducible, and secure Docker images, covering multi‑stage builds, parameterized builds, automated security scanning, version tagging, CI/CD integration, and advanced optimizations that can shrink image size by up to 70% and speed up builds fivefold.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
How to Build Lean, Reusable Docker Images: 5 Steps to Standardized Container Management

Introduction: Have you ever struggled with bloated Docker images?

In the middle of the night a production alert revealed a newly deployed container failed to start because the production image ballooned to 3.2 GB, packed with compilers, test data, and even an SSH private key – a classic case of "image obesity".

1. Three Core Principles of Image Building

1. Minimize: only include what is required

Many Dockerfiles copy everything in, resulting in images that contain full build toolchains, Python, vim, and more. The correct approach is to separate build‑time dependencies from runtime dependencies.

# ❌ Bad example: single‑stage build, everything packed in
FROM node:16
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm","start"]
# Final size: 1.2GB
# ✅ Good example: multi‑stage build, only runtime essentials remain
# Build stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Runtime stage
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node","index.js"]
# Final size: 180MB

Key command: docker history <image> to view layer sizes and spot "fat" layers.

2. Reproducibility: lock dependency versions

Six months after an image was built, a rebuild failed because the Dockerfile used apt-get install nginx without specifying a version, and the upstream package changed its configuration.

# ❌ Dangerous way
RUN apt-get update && apt-get install -y nginx
RUN pip install flask

# ✅ Safe way
RUN apt-get update && apt-get install -y \
    nginx=1.18.0-6ubuntu14.4 \
    && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# requirements.txt pins versions, e.g., flask==2.3.2

3. Security: avoid turning images into vulnerability hotspots

A 2023 security audit found that 30 % of production images contained high‑severity vulnerabilities because they were built from ubuntu:latest and never updated.

Use specific base image versions, e.g., FROM alpine:3.18.4 instead of FROM alpine:latest Create a non‑root user to run the application

Delete build caches and package manager caches

Regularly scan images for vulnerabilities

# Secure image template
FROM python:3.11-slim-bullseye
# Create non‑root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install deps and clean cache
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
    && rm -rf /root/.cache/pip
# Switch to non‑root user
USER appuser
COPY --chown=appuser:appuser . /app
WORKDIR /app
CMD ["python","app.py"]

2. Practical 5‑Step Production‑Grade Image Build System

Step 1: Write an efficient Dockerfile (multi‑stage example)

# Production‑grade Dockerfile for a Java Spring Boot app
# Stage 1: download dependencies (cached)
FROM maven:3.8.6-openjdk-11-slim AS deps
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Stage 2: build the application
FROM maven:3.8.6-openjdk-11-slim AS builder
WORKDIR /app
COPY --from=deps /root/.m2 /root/.m2
COPY . .
RUN mvn clean package -DskipTests

# Stage 3: runtime image
FROM openjdk:11-jre-slim
RUN groupadd -r spring && useradd -r -g spring spring
# Optional monitoring tools
RUN apt-get update && apt-get install -y \
    curl=7.74.0-1.3+deb11u7 && \
    rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/*.jar app.jar
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1
USER spring
EXPOSE 8080
ENTRYPOINT ["java","-Xmx512m","-jar","/app.jar"]

Step 2: Parameterize builds with ARG

# Use ARG for build‑time parameters
ARG APP_ENV=production
ARG NODE_VERSION=16-alpine
FROM node:${NODE_VERSION} AS builder
ARG APP_ENV
RUN if [ "$APP_ENV" = "development" ]; then \
        npm install; \
    else \
        npm ci --only=production; \
    fi

Build commands:

# Development build
docker build --build-arg APP_ENV=development -t myapp:dev .

# Production build
docker build --build-arg APP_ENV=production -t myapp:prod .

Step 3: Automated image security scanning

#!/bin/bash
# scan_image.sh – image security scan script
IMAGE_NAME=$1
REPORT_FILE="scan_report_$(date +%Y%m%d_%H%M%S).json"

echo "🔍 Scanning image: $IMAGE_NAME"
# Use Trivy (must be installed)
trivy image --severity HIGH,CRITICAL \
    --format json \
    --output $REPORT_FILE \
    $IMAGE_NAME
CRITICAL_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' $REPORT_FILE)
HIGH_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity=="HIGH")] | length' $REPORT_FILE)

echo "📊 Scan results:"
echo "  - Critical: $CRITICAL_COUNT"
echo "  - High: $HIGH_COUNT"
if [ $CRITICAL_COUNT -gt 0 ]; then
    echo "❌ Critical vulnerabilities found – aborting release!"
    exit 1
fi
echo "✅ Security check passed"

Step 4: Image version management (standard tag format)

# Tag format: <major>.<minor>.<patch>-<build>-<git>
# Example: v1.2.3-20231125-7a3b5c9
#!/bin/bash
VERSION=$(cat VERSION)   # read from VERSION file
BUILD_DATE=$(date +%Y%m%d)
GIT_COMMIT=$(git rev-parse --short HEAD)
TAG="v${VERSION}-${BUILD_DATE}-${GIT_COMMIT}"
# Build and tag
docker build -t myapp:${TAG} .
docker tag myapp:${TAG} myapp:latest
# Push
docker push myapp:${TAG}
docker push myapp:latest

echo "✅ Image published: myapp:${TAG}"

Step 5: CI/CD integration (GitLab example)

# .gitlab-ci.yml
stages:
  - build
  - scan
  - push
variables:
  DOCKER_REGISTRY: "registry.company.com"
  IMAGE_NAME: "$DOCKER_REGISTRY/myapp"

build:
  stage: build
  script:
    - docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
    - docker save $IMAGE_NAME:$CI_COMMIT_SHA > image.tar
  artifacts:
    paths:
      - image.tar
    expire_in: 1 hour

security_scan:
  stage: scan
  script:
    - docker load < image.tar
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME:$CI_COMMIT_SHA
  dependencies:
    - build

push_image:
  stage: push
  script:
    - docker load < image.tar
    - docker tag $IMAGE_NAME:$CI_COMMIT_SHA $IMAGE_NAME:latest
    - docker push $IMAGE_NAME:$CI_COMMIT_SHA
    - docker push $IMAGE_NAME:latest
  only:
    - main
  dependencies:
    - build

3. Advanced Optimizations

Use BuildKit for faster builds

# Enable BuildKit (30‑50% faster)
export DOCKER_BUILDKIT=1

docker build --build-arg BUILDKIT_INLINE_CACHE=1 \
    --cache-from registry.company.com/myapp:latest \
    -t myapp:new .

Cache optimization strategy

#!/bin/bash
# optimize_cache.sh – smart cache management
# Remove dangling images
docker image prune -f
# Remove images unused for >7 days
docker image prune -a --filter "until=168h" -f
# Keep the latest 5 versions of a specific image
IMAGE_NAME="myapp"
docker images --format "{{.Repository}}:{{.Tag}}" | \
    grep "^${IMAGE_NAME}:" | \
    sort -V | \
    head -n -5 | \
    xargs -r docker rmi

echo "✅ Cache optimization completed"

Extreme image size reduction (scratch Go binary)

# Build a minimal Go binary and copy into a scratch image
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o app

FROM scratch
COPY --from=builder /app/app /
ENTRYPOINT ["/app"]
# Final size: <10MB

4. Common Pitfalls

Pitfall 1: Storing secrets inside images

In a 2022 audit, images were found to contain database passwords and AWS keys because a .env file was copied during the build.

# Use .dockerignore to exclude sensitive files
*.env
*.pem
.git/
.aws/

Pitfall 2: Running containers as root

Running as root gives attackers host‑level privileges if the container is compromised.

# Never run as root in production
USER 1000:1000  # use UID/GID instead of a username

Pitfall 3: Ignoring timezone settings

Logs were off by eight hours and scheduled jobs ran at wrong times.

# Set timezone inside the image
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

Conclusion

By applying the three principles (minimize, reproducible, secure), following the five‑step workflow, and leveraging advanced tricks such as BuildKit, cache pruning, and scratch‑based images, you can shrink Docker image size by up to 70 %, accelerate builds fivefold, and cut security vulnerabilities by around 90 %.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

image-optimizationDevOpsContainerSecurity
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.