Skip to main content

Base vs Parent vs Custom Image

Understanding the difference between base image, parent image, and custom image is essential before diving into best practices.
scratch → Base Image (ubuntu:22.04) → Parent Image (myparent) → Custom Image
Base image — Built from scratch (the empty Docker image):
# Dockerfile for ubuntu:22.04 (base image)
FROM scratch
ADD 433cf0b8353e08be3a6582ad5947c57a66bdbb842ed3095246a1ff6876d157f1 /
CMD ["bash"]
Parent image — Built from a base image:
# Dockerfile for mypythonparent (parent image)
FROM ubuntu:22.04
WORKDIR /app
RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip && \
    pip3 install -r /app/requirements.txt && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
CMD ["bash"]
Custom image — Built from a parent image:
# Dockerfile for mycustomimage (custom image)
FROM mypythonparent

COPY . /app

ENV APP_ENV=production \
    APP_PORT=8080

EXPOSE 8080

CMD ["python3", "/app/main.py"]

Best Practices

Each image should be focused on a single application or service. For example, create separate images for a web server and a database.This enables better scalability, maintainability, and reusability, while also reducing image size by including only the necessary dependencies.
Containers are ephemeral — they can be created and destroyed at any time. Data stored inside a container is lost when the container is removed.Use Docker volumes or external storage solutions (e.g., Redis, PostgreSQL) to persist data outside of the container.
Regularly update your images and dependencies to include the latest security patches. Stale base images are a common source of vulnerabilities.
Regularly scan images using docker scan or third-party tools like Trivy to detect known vulnerabilities before deployment.
Only install the dependencies required for your application. Remove build tools and temporary files after installation to keep the image lean.
RUN apt-get update && apt-get install -y \
  curl && \
  apt-get clean && rm -rf /var/lib/apt/lists/*
Exclude unnecessary files and directories from the build context to reduce image size and speed up builds.
node_modules
.git
*.log
Use multi-stage builds to separate the build environment from the runtime environment. This ensures only necessary files end up in the final image, improving both size and security.
# Stage 1: Build
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .

# Stage 2: Runtime
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
CMD ["./main"]
Combine RUN, COPY, and ADD commands where possible to minimize the number of layers and reduce image size — but keep readability in mind.
RUN apt-get update && apt-get install -y curl && \
  apt-get clean && rm -rf /var/lib/apt/lists/*
Always use specific version tags (e.g., ubuntu:22.04) instead of latest to ensure consistent builds and avoid unexpected breaking changes.
Avoid running your application as the root user. Create a dedicated non-root user to reduce the attack surface.
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
Use LABEL to record metadata such as maintainer, version, and description. This helps CI/CD pipelines, compliance tracking, and discoverability in registries.
LABEL maintainer="karchunt"
LABEL version="1.0.0"
LABEL description="A custom Docker image for my application"
Use official images or verified publisher tags from Docker Hub or trusted registries.Enable Docker Content Trust (DCT) to ensure images are signed and verified:
export DOCKER_CONTENT_TRUST=1
Start with a minimal base image like alpine or debian-slim to reduce the image size and attack surface. Only install what is strictly necessary.
Distroless images contain only the application and its runtime dependencies — no package manager, shell, network tools, or text editors. This minimizes the attack surface significantly.Examples:
  • gcr.io/distroless/python3-debian12
  • gcr.io/distroless/base-debian12
See the full list at GoogleContainerTools/distroless.

Build docs developers (and LLMs) love