Dockerfile 的最佳实践 ongoing
文档用途
说明 Dockerfile 的最佳实践.
ARG 声明变量的有效范围
ARG 命令所声明的变量, 其有效范围仅限当前的 构建阶段 (stage).
# 首次声明名为 GREET 的 ARG 的值
ARG GREET="hello"
# 进入新的 stage 后, 之前的 ARG 都失效
FROM alpine
# 重新声明名为 GREET 的 ARG, 会自动按照之前的值来赋值
ARG GREET
RUN echo "$GREET amiao!"
# 再次进入新的 stage, 之前的 ARG 都失效
FROM alpine
# 重新声明名为 GREET 的 ARG, 会自动按照之前的值来赋值
ARG GREET
RUN echo "$GREET yahaha!"充分利用工程文件的缓存
使用 COPY 命令时, 应该默认使用 --link 选项.
这样, 即使在 COPY 命令之前的 layer 发生了变化, 只要 COPY 命令所涉及的源文件不变,
这一层缓存就依然有效. 因此能高效的利用工程文件的缓存.
# 使用 --link 选项
COPY --link . .另外我们也可以直接避免使用 COPY 命令.
通过 RUN 命令的 --mount=type=bind 选项来替代前面的 COPY.
# 假设构建所需的所有源文件都位于 src 文件夹.
RUN --mount=type=bind,source=src,target=src \
dotnet publish -c Release我们通用这个方法来利用更多来源的缓存, 例如第三方库和编译的中间结果.
例如在 rust 工程中, 可以将第三方库缓存起来, 通过 --mount=type=cache:
RUN --mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
cargo fetch然后在后续的构建过程中使用第三方库的缓存, 并且将编译的中间结果缓存起来:
RUN --mount=type=cache,target=./target/,id=${APP_NAME}-${TARGETPLATFORM} \
--mount=type=cache,target=/usr/local/cargo/registry,readonly \
--mount=type=cache,target=/usr/local/cargo/git/db,readonly \
cargo build --release多平台镜像的交叉编译问题
有些编程语言或目标平台会对多平台镜像的构建带来挑战. 这通常是因为:
我们希望通过当前设备原生的架构 (例如 x86_64 或 aarch64) 的基础镜像来编译,
并编译出适合于其他架构的目标平台的可执行文件, 也就是交叉编译.
这么做的好处是: 效率高, 即更高的编译效率, 和更高的构建缓存利用效率.
而难点在于: 需要恰当安装交叉编译所需的工具链, 并恰当配置编译命令的参数.
为了解决这个难题, 对于 rust 工程, 笔者建议借助 tonistiigi/xx.
rust 工程 Dockerfile 示例
# ARGs only last for the build phase of a single image.
# For the multistage, renew the ARG by simply stating: ARG XXX
ARG APP_NAME=my_app
################################################################################
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
################################################################################
# Create a stage for building the application.
FROM --platform=$BUILDPLATFORM rust:alpine AS build
ARG APP_NAME
WORKDIR /code
# Copy helper scripts from tonistiigi/xx
COPY --from=xx / /
# Install host build dependencies.
RUN apk add --no-cache musl-dev clang
# Fetch crates before building stage for better caching.
RUN --mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=cache,target=/usr/local/cargo/registry/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
cargo fetch
# This is the architecture you're building for, which is passed in by the builder.
# Placing it here allows the previous steps to be cached across architectures.
# https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope
ARG TARGETPLATFORM
RUN mkdir -p /app/${TARGETPLATFORM}
# Copy all project files while respecting .dockerignore
COPY . .
# Build the application.
# Leverage a cache mount to /usr/local/cargo/registry/ for downloaded dependencies,
# a cache mount to /usr/local/cargo/git/db for git repository dependencies, and a cache
# mount to ./target/ for compiled dependencies which will speed up subsequent builds.
RUN --mount=type=cache,target=./target/,id=rust-cache-${APP_NAME}-${TARGETPLATFORM} \
--mount=type=cache,target=/usr/local/cargo/registry/,readonly \
--mount=type=cache,target=/usr/local/cargo/git/db,readonly \
xx-cargo build --release --offline --target-dir ./target && \
xx-verify --static ./target/$(xx-cargo --print-target-triple)/release/${APP_NAME} && \
cp ./target/$(xx-cargo --print-target-triple)/release/${APP_NAME} /app/${TARGETPLATFORM}
################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base image
# from the build stage where the necessary files are copied from the build stage.
FROM alpine AS final
ARG APP_NAME
WORKDIR /app
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser
# Expose the port that the application listens on.
EXPOSE 1234
# Placing it here allows the previous steps to be cached across architectures.
ARG TARGETPLATFORM
# Copy the executable from the "build" stage.
COPY --from=build /app/${TARGETPLATFORM}/${APP_NAME} .
# What the container should run when it is started.
CMD ["/app/my_app"].NET 工程 Dockerfile 示例
################################################################################
# Create a stage for building the application.
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
COPY . /code
WORKDIR /code
# This is the architecture you’re building for, which is passed in by the builder.
# Placing it here allows the previous steps to be cached across architectures.
ARG TARGETARCH
RUN mkdir -p /app/${TARGETARCH}
# Build the application.
# Leverage a cache mount to /root/.nuget/packages so that subsequent builds don't have to re-download packages.
# If TARGETARCH is "amd64", replace it with "x64" - "x64" is .NET's canonical name for this and "amd64" doesn't work in .NET 6.0.
RUN --mount=type=cache,id=nuget-${TARGETARCH},target=/root/.nuget/packages \
dotnet publish -c Release --self-contained \
--arch ${TARGETARCH/amd64/x64} \
--output /app/${TARGETARCH}
################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base image
# from the build stage where the necessary files are copied from the build stage.
FROM alpine AS final
WORKDIR /app
# Switch to a non-privileged user (defined in the base image) that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
# and https://github.com/dotnet/dotnet-docker/discussions/4764
USER $APP_UID
# Expose the port that the application listens on.
EXPOSE 1234
# This is the architecture you’re building for, which is passed in by the builder.
# Placing it here allows the previous steps to be cached across architectures.
ARG TARGETARCH
# Copy everything needed to run the app from the "build" stage.
COPY --from=build /app/${TARGETARCH} .
CMD ["./MyApp"]在非 root 权限下运行程序
在不做特别设置的情况下, 构建出来的容器镜像将会使用 root 用户来运行, 这存在安全隐患.
因为攻击者可能会从容器中逃逸并获取宿主系统的控制权 (这被称之为 privilege escalation).
如果我们构建非 root 权限运行的镜像, 那么攻击者就需要先想办法取得容器内的 root 权限,
再进一步尝试获取宿主系统的控制权限, 这样难度会更大, 对我们来说也就更安全.
通过在 Dockerfile 中写入类似以下的内容, 可以构建非 root 权限的镜像:
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
app
# Switch to the use just created.
USER app
# copy with `--chown` to set group and user
COPY --chown=app:app --from=builder /source /target# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN useradd \
--comment "" \
--home-dir "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
app
# Switch to the use just created.
USER app
# copy with `--chown` to set group and user
COPY --chown=app:app --from=builder /source /targetWARNING
应该直接使用 COPY 命令中的 --chown 来设置 group 和 user.
不要 使用 RUN chown -R group:user target 这种命令.
在调试时, 缺少 root 权限可能不方便, 为此我们借助配置文件来获取 root 权限.
例如在 compose.yaml, 可以通过将 previleged 字段设置为 true 来获取权限.
Kubernetes 的配置文件中也有类似的字段.
services:
app:
image: xxx
privileged: true # 存在安全隐患, 不应用于生产环境 #