Instruction file imported from jim60105/Containerfile-template (
.github/instructions/containerfile-guidelines.instructions.md). Copyright stays with the author.
Dockerfile/Containerfile Authoring Guide
Overview
This guidelines focuses on building high-quality, secure, and performance-optimized container images, supporting multi-architecture builds and following OpenShift and Kubernetes best practices. The author prefers using Containerfile to show support for open source technologies.
File Structure and Naming Conventions
File Naming
- Prefer
ContainerfileoverDockerfile - Use descriptive names for different base images:
alpine.Dockerfile- Based on Alpine Linuxdistroless.Dockerfile- Based on Google Distrolessubi.Dockerfile- Based on Red Hat UBInuitka.Dockerfile- For special use cases (e.g., Nuitka compilation)
Required Syntax Declaration
# syntax=docker/dockerfile:1
Dockerfile Structure Guidelines
1. ARG Definition Block
Define all ARGs at the top of the file, using standard variables. Only UID, VERSION, and RELEASE go at the top level — architecture ARGs (TARGETARCH, TARGETVARIANT) should be declared inside the stage that needs them, not globally:
# syntax=docker/dockerfile:1
ARG UID=1001
ARG VERSION=EDGE
ARG RELEASE=0
[!NOTE]
TARGETARCHandTARGETVARIANTare automatically set by BuildKit but must be explicitly declared withARGinside each stage that references them (e.g., in cache mount IDs). Declare them at the top of the build stage, not at the file level.
2. Multi-stage Build Structure
Use clear stage naming and comment separation:
########################################
# Build stage
########################################
FROM python:3.13-alpine AS build
########################################
# Final stage
########################################
FROM python:3.13-alpine AS final
[!NOTE]
Always name the last stage asfinal, even if there is only one stage. This ensures consistency and clarity in multi-stage builds and CI usage.
[!IMPORTANT]
Only use a separatebasestage when it requires non-trivial setup (e.g., installing system packages, configuring interpreters on UBI/Debian). For Alpine-based images where both build and final share the same unmodified base image, have each stageFROMthe base image directly — this avoids an unnecessary indirection layer.
3. Stage Comment Guidelines
- Use 40
#characters to separate stages - Stage names in English, concise and clear
- Include a brief description of the stage's purpose
Cache Optimization Strategies
BuildKit Cache Mode
Use a unified cache strategy for different package managers:
# Alpine APK
RUN --mount=type=cache,id=apk-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apk \
apk update && apk add -u package-name
# Debian/Ubuntu APT
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
--mount=type=cache,id=aptlists-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/lib/apt/lists \
apt-get update && apt-get install -y --no-install-recommends package-name
# Python PIP
RUN --mount=type=cache,id=pip-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/pip \
pip install package-name
# Python UV
RUN --mount=type=cache,id=uv-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/uv \
uv pip install package-name
Multi-architecture Support
Always define multi-arch variables:
# RUN mount cache for multi-arch: https://github.com/docker/buildx/issues/549#issuecomment-1788297892
ARG TARGETARCH
ARG TARGETVARIANT
Security and Permission Management
Create Non-root User
# Alpine
ARG UID
RUN adduser -g "" -D $UID -u $UID -G root
# Debian/Ubuntu
ARG UID
RUN groupadd -g $UID $UID && \
useradd -l -u $UID -g $UID -m -s /bin/sh -N $UID
OpenShift Compatibility
Support arbitrary UID permission settings:
# Create directories
RUN install -d -m 775 -o $UID -g 0 /app && \
install -d -m 775 -o $UID -g 0 /licenses
# Copy files
COPY --link --chown=$UID:0 --chmod=775 source dest
License File Management
Always copy license files to the specified location:
# Copy licenses (OpenShift Policy)
COPY --link --chown=$UID:0 --chmod=775 LICENSE /licenses/Dockerfile.LICENSE
COPY --link --chown=$UID:0 --chmod=775 project/LICENSE /licenses/project.LICENSE
Tools and Dependency Management
Use Static Binaries
Prefer pre-built static tools:
# ffmpeg (statically compiled and UPX compressed)
COPY --link --from=ghcr.io/jim60105/static-ffmpeg-upx:7.0-1 /ffmpeg /usr/bin/
COPY --link --from=ghcr.io/jim60105/static-ffmpeg-upx:7.0-1 /ffprobe /usr/bin/
# dumb-init (signal handling)
COPY --link --from=ghcr.io/jim60105/static-ffmpeg-upx:7.0-1 /dumb-init /usr/bin/
# curl (health check)
COPY --link --from=ghcr.io/tarampampam/curl:8.7.1 /bin/curl /usr/local/bin/
Package Installation Best Practices
# APT installation
RUN apt-get update && apt-get install -y --no-install-recommends \
package1 \
package2 \
package3
# Version pinning (Alpine)
RUN apk add -u \
dumb-init=1.2.5-r3 \
git=2.45.2-r0
Language-specific Guidelines
Python Projects
Choose between pip or uv based on your project needs:
Option A: Using UV (Recommended for new projects)
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# UV configuration
ENV UV_PROJECT_ENVIRONMENT=/venv
ENV VIRTUAL_ENV=/venv
ENV UV_LINK_MODE=copy
ENV UV_PYTHON_DOWNLOADS=0
# Step 1: Install dependencies ONLY (cached — deps change less often than source)
RUN --mount=type=cache,id=uv-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
uv sync --frozen --no-dev --no-install-project --no-editable
# Step 2: Copy source, then install the project itself
COPY --link src/ src/
RUN --mount=type=cache,id=uv-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
uv sync --frozen --no-dev --no-editable
[!IMPORTANT]
The two-step uv sync pattern is critical for cache efficiency:
- Step 1 (
--no-install-project): Installs only third-party dependencies. This layer is cached and reused as long aspyproject.tomlanduv.lockdon't change.- Step 2 (after
COPY src/): Installs the project package itself. This layer is rebuilt when source code changes, but the expensive dependency installation is already cached.- Do NOT combine these into a single step — it would invalidate the dependency cache on every source code change.
Option B: Using pip (For compatibility/legacy projects)
# pip optimization
ENV PIP_USER="true"
ARG PIP_NO_WARN_SCRIPT_LOCATION=0
ARG PIP_ROOT_USER_ACTION="ignore"
ARG PIP_NO_COMPILE="true"
ARG PIP_DISABLE_PIP_VERSION_CHECK="true"
# Install dependencies under /root/.local
RUN --mount=type=cache,id=pip-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/pip \
pip install package-name
# Cleanup strategy (for both pip and uv)
RUN find "/root/.local" -name '*.pyc' -print0 | xargs -0 rm -f || true && \
find "/root/.local" -type d -name '__pycache__' -print0 | xargs -0 rm -rf || true
Environment Setup
# For uv projects: Use virtual environment path
ENV PATH="/venv/bin:$PATH"
ENV PYTHONPATH="/venv/lib/python3.11/site-packages"
# For pip projects: Use user installation path
ENV PATH="/root/.local/bin:$PATH"
Rust Projects
# Use cargo-chef for build cache optimization
FROM lukemathwalker/cargo-chef:latest-rust-alpine AS chef
# Set static linking
ENV RUSTFLAGS="-C target-feature=+crt-static"
# Planner stage
FROM chef AS planner
RUN cargo chef prepare --recipe-path recipe.json
# Cook stage (build dependencies)
FROM chef AS cook
RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json
Node.js Projects
# Use Alpine base image
FROM node:lts-alpine AS build
# Clean cache
RUN npm ci && npm cache clean --force
# Runtime stage
FROM node:lts-alpine AS final
.NET Projects
For .NET 8 applications (following Visual Studio patterns):
# Base image with runtime dependencies (self-contained deployments usually use runtime-deps)
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS base
WORKDIR /app
# Debug stage (separate from production)
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine AS debug
# Debug-specific dependencies
ENV PATH="/venv/bin:$PATH"
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
WORKDIR /src
# Copy project file and restore dependencies
COPY ["app.csproj", "."]
RUN dotnet restore -a $TARGETARCH "app.csproj"
# Publish stage
FROM build AS publish
COPY . .
RUN dotnet publish "app.csproj" -a $TARGETARCH -c $BUILD_CONFIGURATION \
-o /app/publish --self-contained true
# Final production image
FROM base AS final
ARG APP_UID=1001
ENV PATH="/app:$PATH"
RUN mkdir -p /app && chown -R $APP_UID:$APP_UID /app
COPY --from=publish --chown=$APP_UID:$APP_UID /app/publish/app /app/app
USER $APP_UID
ENTRYPOINT ["/app/app"]
Key .NET patterns:
- Use
runtime-depsbase image for self-contained deployments - Enable
PublishTrimmed=trueandPublishSingleFile=truein.csproj - Separate debug and production stages
- Use
--self-contained truefor deployment - Include TrimmerRootAssembly directives for reflection-heavy libraries
Golang Projects
# Disable CGO to produce static binaries
ENV CGO_ENABLED=0
# Use UPX for compression
RUN upx --best --lzma /go/bin/binary || true
Runtime Environment Settings
Final Stage Instruction Ordering (CRITICAL)
The order of instructions in the final stage is deliberate and significant for cache efficiency, security, and correctness. Follow this exact order:
- System cleanup — Remove pip/setuptools/wheel (they're not needed at runtime, reduces attack surface)
- Create user — Non-root user via
adduser(must exist beforeCOPY --chown) - Create directories —
install -dwith proper ownership (must happen before COPYing into them) - COPY from build — Copy built artifacts with
--link --chown=$UID:0 --chmod=775(OpenShift compatibility) - ENV — Set PATH and other environment variables (depends on copied artifacts location)
- WORKDIR — Set working directory
- VOLUME — Declare volumes (if applicable)
- EXPOSE — Declare ports
- USER — Switch to non-root (as late as possible, after all root-requiring operations)
- STOPSIGNAL — Signal handling configuration
- ENTRYPOINT / CMD — Runtime command
- ARG VERSION + ARG RELEASE + LABEL — Always last (see below)
[!IMPORTANT]
LABEL MUST be the very last instruction. PlaceARG VERSIONandARG RELEASEimmediately before LABEL. This is critical because:
- ARG values bust the build cache for all subsequent instructions
- VERSION/RELEASE change on every build — placing them early would invalidate all layers after them
- LABEL itself doesn't create a filesystem layer, so putting it last has zero cost
- This pattern ensures maximum cache reuse across builds with different version tags
# ✅ Correct: LABEL at the very end
USER $UID
STOPSIGNAL SIGINT
ENTRYPOINT [ "dumb-init", "--", "command" ]
CMD [ "--help" ]
ARG VERSION
ARG RELEASE
LABEL name="project-name" \
vendor="original-author" \
maintainer="jim60105" \
url="https://github.com/jim60105/project" \
version=${VERSION} \
release=${RELEASE} \
...
# ❌ Wrong: LABEL before USER/CMD — VERSION ARG busts cache for all instructions after it
ARG VERSION
ARG RELEASE
LABEL name="project-name" \
version=${VERSION} \
release=${RELEASE}
USER $UID
CMD [ "command" ] # This layer is rebuilt on every version change!
Required Runtime Settings
# Working directory
WORKDIR /app
# Persistent directories
VOLUME [ "/data", "/tmp" ]
# User switch
USER $UID
# Signal handling
STOPSIGNAL SIGINT
# Use dumb-init as PID 1
ENTRYPOINT [ "dumb-init", "--", "command" ]
CMD [ "--help" ]
Health Check (if applicable)
[!WARNING]
HEALTHCHECK does not function in OCI image builds and podman builds. Do not implement healthcheck in the Containerfile unless the user specifically asks for it.
When implementing health checks, you need to include the curl binary from the static curl image:
# curl for healthcheck
COPY --link --from=ghcr.io/tarampampam/curl:8.7.1 /bin/curl /usr/local/bin/
HEALTHCHECK --interval=30s --timeout=2s --start-period=30s \
CMD [ "curl", "--fail", "http://localhost:8080/" ]
LABEL Standards
Required Labels
[!IMPORTANT]
LABEL MUST be the very last instruction in the Containerfile. PlaceARG VERSIONandARG RELEASEimmediately before it. See "Final Stage Instruction Ordering" above for the rationale.
ARG VERSION
ARG RELEASE
LABEL name="project-name" \
# Authors for the main application
vendor="original-author" \
# Maintainer for this container image
maintainer="jim60105" \
# Containerfile source repository
url="https://github.com/jim60105/project" \
version=${VERSION} \
# This should be a number, incremented with each change
release=${RELEASE} \
io.k8s.display-name="Display Name" \
summary="Brief summary of the application" \
description="Detailed description with website reference: https://example.com"
Commenting and Documentation
Comment Style
- Write comments in English
- Include relevant GitHub issues or documentation links
- Explain the reason for special settings
- Provide useful reference information
# This is needed for OpenShift compatibility
# https://docs.openshift.com/container-platform/4.14/openshift_images/create-images.html
#! UPX will skip small files and large files
# https://github.com/upx/upx/blob/5bef96806860382395d9681f3b0c69e0f7e853cf/src/p_unix.cpp#L80
upx --best --lzma /binary || true
ML/AI Project Special Considerations
CUDA Support
# Partial CUDA toolkit installation instead of full installation
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
# Fix missing TensorRT link
RUN ln -s /usr/lib/x86_64-linux-gnu/libnvinfer.so /usr/lib/x86_64-linux-gnu/libnvinfer.so.7
Build Parameters and Flexible Design
Conditional Build
# Allow skipping certain steps to reduce image size
ARG SKIP_REQUIREMENTS_INSTALL=
FROM prepare_build${SKIP_REQUIREMENTS_INSTALL:+_empty} AS build
Multi-image Support
# Support conditional selection of different base images
FROM prepare_base_$TARGETARCH$TARGETVARIANT AS base
Performance Optimization
COPY Optimization
# Use --link to reuse already built layers in subsequent builds with --cache-from
# This allows layer reuse even if previous layers have changed, especially important
# for multi-stage builds and when rebasing images on updated base images
COPY --link --chown=$UID:0 --chmod=775 source dest
[!TIP]
Use--linkwhen you want to optimize layer cache reuse in multi-stage builds or when frequently rebuilding images with changing base layers. This is particularly beneficial in CI/CD pipelines where base images are regularly updated.
[!IMPORTANT]
Do NOT use--linkwhen your destination path contains symlinks that need to be followed. With--link, COPY/ADD commands cannot read files from the previous state or follow symlinks in the destination directory. The final destination path will always contain only directories, not symlinks.
[!NOTE]
If you don't rely on symlink-following behavior in destination paths, using--linkis always recommended for better cache reuse and equivalent or better performance.
Layer Cache Strategy
# Install large, rarely changed packages first
RUN uv pip install \
torch==2.7.0 \
tensorflow>=2.16.1
# Then install project-specific dependencies
RUN uv pip install -r requirements.txt
Debugging and Testing
Binary Stage (optional)
########################################
# Binary stage
# How to: docker build --output=. --target=binary .
########################################
FROM scratch AS binary
COPY --from=builder /app/binary /
[!IMPORTANT]
The binary stage is only applicable when the output binary file is either fully statically linked or relies on runtime dependencies; otherwise, in many cases, the extracted content cannot run. When implementing this stage, a review must be conducted to ensure it meets the requirements and to warn users.
Test Stage
########################################
# Test stage
########################################
FROM builder AS test
RUN cargo test --release --all-targets --locked
Summary
Remember, the goal of these guidelines is to build secure, efficient, and maintainable container images while following open source best practices and industry standards. Focus on:
- Security: Non-root users, principle of least privilege
- Performance: Multi-stage builds, BuildKit cache, UPX compression
- Maintainability: Clear comments, standardized structure
- Compatibility: OpenShift support, multi-arch builds
- Best Practices: Static tools, minimized image size
When writing new Dockerfile/Containerfile files, refer to the relevant templates in this project and follow these guidelines.