编写精简的 Dockerfile 与 Shell 入口点
编写多阶段构建脚本和健壮的入口点适配脚本,处理信号并生成配置模板。
编写精简的 Dockerfile 与 Shell 入口点 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
为什么精简的 Dockerfile 对 DevOps 很重要
在生产环境中,Docker 镜像中的每一兆字节都有成本:拉取速度更慢、攻击面更大,还会浪费镜像仓库的存储空间。精简的 Dockerfile 与健壮的 Shell 入口脚本相结合,是成熟 DevOps 实践的标志。
- 多阶段构建将构建时工具与最终运行时镜像分离,从而大幅减小镜像体积。
- 入口脚本适配层是用于引导容器运行的小型 Shell 脚本:它们生成配置文件模板、验证环境变量、处理信号,最后执行主进程。
- 二者共同构成了 Kubernetes、ECS 和裸机环境中可靠且可移植的容器工作负载的基础。
本课将端到端地讲解这两项实践,并介绍可直接应用到您流水线中的生产级模式。
多阶段 Dockerfile 的组成
多阶段 Dockerfile 使用多个 FROM 指令。每个阶段都是一组彼此隔离的层;您只需将所需的构建产物复制到下一阶段。
- 阶段 0(构建器):安装编译器、测试运行器和构建依赖。
- 阶段 1(运行时):从精简的基础镜像(例如
alpine、distroless)开始,只复制编译后的二进制文件或应用程序包。 - 最终镜像不会包含
gcc、make或源代码,除非您明确复制它们。
在 COPY 中使用 --from=<stage>,即可跨阶段提取文件。使用 AS <name> 为阶段命名,便于阅读,也可以配合 docker build --target 有选择地构建目标阶段。
# ---- Stage 0: builder ----
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /app/server ./cmd/server
# ---- Stage 1: runtime ----
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]减少层数与缓存失效
每条 RUN、COPY 和 ADD 指令都会创建一个新层。指令顺序不当会让构建缓存不必要地失效,导致 CI 流水线变慢。
- 在复制源代码之前先复制依赖清单(
package.json、go.mod、requirements.txt),这样依赖安装就能独立使用缓存。 - 使用
&&将相关命令串联起来,并在同一个RUN层中完成清理,避免软件包缓存残留在中间层中。 - 在软件包管理器中使用
--no-cache,并在安装后删除列表文件。
FROM python:3.12-slim AS builder
WORKDIR /app
# 1. Install deps first (cached until requirements change)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 2. Copy source (cache busted only when code changes)
COPY src/ ./src/
FROM python:3.12-slim
COPY --from=builder /install /usr/local
COPY --from=builder /app/src /app/src
WORKDIR /app
CMD ["python", "-m", "src.main"]BuildKit 密钥与 SSH 转发
私有镜像仓库、SSH 密钥和 API 令牌绝不能出现在镜像层中。Docker BuildKit 提供了两种安全机制:
--secret:在单个RUN步骤中挂载密钥文件,而不会将其写入镜像层。通过/run/secrets/<id>访问。--ssh:将主机的 SSH 代理套接字转发到构建过程中,使git clone能够完成身份验证,而无需嵌入私钥。
使用 DOCKER_BUILDKIT=1 或 docker buildx build 启用 BuildKit。# syntax=docker/dockerfile:1 指令会启用这些功能。
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Mount NPM token as a secret — never stored in the image
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci --prefer-offline
# Usage at build time:
# DOCKER_BUILDKIT=1 docker build \
# --secret id=npm_token,src=~/.npmrc-token \
# -t myapp:latest .编写健壮的入口脚本适配层
入口脚本适配层是一个在 Dockerfile 中通过 ENTRYPOINT 设置的 Shell 脚本。它的任务是在将控制权交给主进程之前准备好运行环境。
结构良好的适配层应遵循以下顺序:
- 步骤 1:设置
set -euo pipefail,使任何失败都能立即中止。 - 步骤 2:验证必需的环境变量,并通过有帮助的消息快速报告失败。
- 步骤 3:根据环境变量生成配置文件模板。
- 步骤 4:注册信号处理器,以实现平滑关闭。
- 步骤 5:
exec "$@"— 用主进程替换 Shell,使 PID 1 成为应用程序,而不是适配层。
最后的 exec 至关重要:没有它,Kubernetes 或 Docker 运行时发送的信号就不会转发给子进程。
#!/usr/bin/env bash
set -euo pipefail
# Step 2: validate required env vars
REQUIRED_VARS=(DATABASE_URL APP_SECRET PORT)
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "[entrypoint] ERROR: required env var '$var' is not set" >&2
exit 1
fi
done
# Step 3: template config (see next scene)
# Step 4: signal handling (see scene after that)
# Step 5: hand off to CMD
exec "$@"使用 envsubst 生成配置模板
envsubst(来自 GNU gettext 软件包,在 Alpine 中以 gettext 的形式提供)会将模板文件中的 ${VAR} 占位符替换为当前环境变量的值。
- 将
*.tmpl配置模板随镜像一起发布;入口脚本会在启动时渲染它。 - 向
envsubst显式传入变量列表,避免它意外展开无关的美元符号(例如 nginx 正则表达式中的美元符号)。 - 将渲染后的文件写入
/tmp或专用配置卷等可写路径。
#!/usr/bin/env bash
# Template: /etc/nginx/conf.d/app.conf.tmpl contains:
# server { listen ${NGINX_PORT}; server_name ${SERVER_NAME}; ... }
export NGINX_PORT=${NGINX_PORT:-8080}
export SERVER_NAME=${SERVER_NAME:-localhost}
envsubst '${NGINX_PORT} ${SERVER_NAME}' \
< /etc/nginx/conf.d/app.conf.tmpl \
> /etc/nginx/conf.d/app.conf
echo "[entrypoint] nginx config rendered:"
grep -E 'listen|server_name' /etc/nginx/conf.d/app.conf
exec "$@"信号处理与平滑关闭
当 Kubernetes、ECS 或 docker stop 停止容器时,容器会收到 SIGTERM。如果您的入口脚本适配层是 PID 1 且不转发信号,那么宽限期结束后,主进程会被 SIGKILL 强制终止,从而导致请求丢失或数据损坏。
- 使用
trap在适配层中捕获SIGTERM和SIGINT。 - 使用
kill -TERM "$child"将信号转发给子进程 PID。 - 使用
wait "$child"阻塞等待子进程退出,然后传递其退出代码。 - 或者,使用
exec完全替换 Shell。这样操作系统会直接向子进程传递信号,无需使用 trap。这是简单场景下的首选模式。
#!/usr/bin/env bash
set -euo pipefail
# Start main process in background
"$@" &
child=$!
# Forward SIGTERM and SIGINT to the child
trap 'echo "[entrypoint] caught SIGTERM, forwarding..."; kill -TERM "$child"' TERM
trap 'echo "[entrypoint] caught SIGINT, forwarding..."; kill -INT "$child"' INT
# Wait for child to exit and capture its exit code
wait "$child"
exit $?使用 tini 作为精简的初始化进程
当您的容器会生成子进程(例如派生工作进程的 Shell)时,您需要一个真正的初始化进程来回收僵尸进程。tini 是专为容器设计的微型初始化二进制程序。
- 将
tini添加到镜像中,并将其设置为入口包装器。 - 它会回收僵尸子进程、正确转发信号,并以子进程的状态代码退出。
- Docker 通过
docker run --init提供内置的 tini,但将其嵌入镜像可以确保它在不同运行时(Kubernetes、ECS 等)中的行为一致。
FROM node:20-alpine
# Install tini for proper signal handling and zombie reaping
RUN apk add --no-cache tini
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
# tini wraps CMD; forwards SIGTERM and reaps zombies
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]以非根用户运行
以 root(UID 0)运行的容器会带来重大安全风险:一旦容器逃逸,攻击者就能获得主机的完整访问权限。请始终在执行主进程前切换到非特权用户。
- 在 Dockerfile 中使用
addgroup/adduser(Alpine)或groupadd/useradd(Debian)创建专用系统用户和用户组。 - 使用
COPY --chown=appuser:appgroup更改应用文件的所有权,这比单独创建一个RUN chown层更高效。 - 使用
USER指令切换用户。入口脚本和 CMD 会继承该用户。 - Kubernetes 的
securityContext.runAsNonRoot: true会拒绝启动仍以 root 身份运行的镜像。
FROM python:3.12-slim
# Create non-root user
RUN groupadd --gid 1001 appgroup && \
useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app files with correct ownership in a single layer
COPY --chown=appuser:appgroup src/ ./src/
COPY --chown=appuser:appgroup entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER appuser
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "-m", "src.main"]镜像中的健康检查与就绪探针
Kubernetes 的存活探针和就绪探针在清单中定义,但您也可以在 Dockerfile 中加入 HEALTHCHECK,用于独立的 docker run 和 Docker Compose 环境。
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 CMD ...- 对于 HTTP 服务,使用
curl --fail或wget -qO-;对于非 HTTP 守护进程,可通过/dev/tcp/localhost/PORT检查套接字。 - 只安装所需内容:在 distroless 镜像中,不要仅为了健康检查而添加
curl,请使用专用探测二进制程序或应用程序自身的健康检查二进制程序。
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY dist/ /usr/share/nginx/html/
# Lightweight health check using bash TCP pseudo-device
# (no curl needed — works on any image with bash)
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD bash -c 'exec 3<>/dev/tcp/localhost/80 && echo -e "GET /health HTTP/1.0\r\n" >&3 && cat <&3 | grep -q "200 OK"' || exit 1
EXPOSE 80整合实践:生产级入口脚本
下面是一个完整的生产级入口脚本适配层,综合了本课中的所有模式:环境变量验证、配置模板生成、信号转发和 exec 交接。这个模式实际用于部署在 Kubernetes 上的 Node.js、Python 和 Go 微服务。
- 每个部分都包含注释,可作为自说明模板。
- 适配层控制在 50 行以内——入口脚本应当简单且易于审查。
- 请注意最后的
exec "$@":所有准备工作完成后,Shell 会被应用程序进程替换,因此应用程序会成为 PID 1,并直接接收所有 OS 信号。
#!/usr/bin/env bash
# entrypoint.sh — production-grade container entrypoint shim
set -euo pipefail
# ── 1. Validate required environment variables ──────────────────
REQUIRED=(DATABASE_URL APP_SECRET PORT LOG_LEVEL)
for var in "${REQUIRED[@]}"; do
[[ -n "${!var:-}" ]] || { echo "[entrypoint] FATAL: $var is not set" >&2; exit 1; }
done
# ── 2. Set safe defaults for optional variables ─────────────────
export HOST=${HOST:-0.0.0.0}
export WORKERS=${WORKERS:-2}
# ── 3. Render config template ───────────────────────────────────
if [[ -f /etc/app/app.conf.tmpl ]]; then
envsubst '${DATABASE_URL} ${PORT} ${LOG_LEVEL} ${HOST}' \
< /etc/app/app.conf.tmpl \
> /etc/app/app.conf
echo "[entrypoint] config rendered at /etc/app/app.conf"
fi
# ── 4. Wait for dependent services (optional, fast) ─────────────
if [[ -n "${WAIT_FOR_HOST:-}" ]]; then
echo "[entrypoint] waiting for ${WAIT_FOR_HOST}:${WAIT_FOR_PORT:-5432}..."
until bash -c "exec 3<>/dev/tcp/${WAIT_FOR_HOST}/${WAIT_FOR_PORT:-5432}" 2>/dev/null; do
sleep 1
done
echo "[entrypoint] dependency ready"
fi
# ── 5. Hand off to CMD (PID 1 becomes the application) ──────────
echo "[entrypoint] starting: $*"
exec "$@"知识检查:入口脚本中的信号处理
测试您对容器入口脚本中信号处理的理解。
回顾:精简的 Dockerfile 与 Shell 入口脚本
您已经学习了生产环境容器编写的完整技术栈:
- 多阶段构建使用多个
FROM指令,将编译器和构建工具排除在最终镜像之外,从而生成精简的最小运行时层。 - 层的顺序——在源代码之前复制依赖清单——可以最大限度地命中缓存,加快 CI 流水线。
- BuildKit 密钥和 SSH 挂载可以在不破坏经过身份验证的构建的情况下,将凭据排除在镜像历史记录之外。
- 入口脚本适配层会验证环境变量,使用
envsubst生成配置文件模板,并通过exec "$@"将控制权交给应用程序。 - 信号处理要求使用
exec(使应用程序成为 PID 1),或者在使用后台任务时,明确采用trap+kill+wait模式。 - tini会在容器生成多个进程时增加僵尸进程回收功能,并正确转发信号。
- 非根用户和
HEALTHCHECK指令共同完成安全且可观测的镜像,使其能够用于 Kubernetes 生产工作负载。
持续一致地组合这些模式,您的镜像就会更小、部署更快,并且在生产环境下更加稳健。
常见问题解答
「编写精简的 Dockerfile 与 Shell 入口点」课时是免费的吗?
是的 — 「编写精简的 Dockerfile 与 Shell 入口点」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「编写精简的 Dockerfile 与 Shell 入口点」这节课中我会学到什么?
编写多阶段构建脚本和健壮的入口点适配脚本,处理信号并生成配置模板。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「编写精简的 Dockerfile 与 Shell 入口点」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 编写精简的 Dockerfile 与 Shell 入口点
- 使用 envsubst 和 heredoc 创建配置模板
- 通过 CLI 和 jq 编写云资源脚本
- 健康探针、就绪门禁与等待循环