0Pricing
DevOps Bootcamp · 课时

编写精简的 Dockerfile 与 Shell 入口点

编写多阶段构建脚本和健壮的入口点适配脚本,处理信号并生成配置模板。

编写精简的 Dockerfile 与 Shell 入口点 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 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 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。

「编写精简的 Dockerfile 与 Shell 入口点」这节课中我会学到什么?

编写多阶段构建脚本和健壮的入口点适配脚本,处理信号并生成配置模板。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DevOps Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「编写精简的 Dockerfile 与 Shell 入口点」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DevOps Bootcamp 课中编写并运行代码吗?

能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 编写精简的 Dockerfile 与 Shell 入口点
  2. 使用 envsubst 和 heredoc 创建配置模板
  3. 通过 CLI 和 jq 编写云资源脚本
  4. 健康探针、就绪门禁与等待循环
← 返回 DevOps Bootcamp