dockerfile entrypoint cmd vs k8s command args

一、ENTRYPOINT vs CMD 核心语义

容器的本质是进程,既然是进程就要有主程序和可选参数

1.1 ENTRYPOINT

  • 定义容器启动时固定执行的命令,主程序

  • 不容易被覆盖(除非显式使用 --entrypoint

  • 类比:不可变的 executable

1.2 CMD 的两种完全不同的语义

情况 A:没有 ENTRYPOINT

# CMD = 完整启动命令
CMD ["nginx", "-g", "daemon off;"]

# 不规范的情况
CMD ["nginx"]

情况 B:有 ENTRYPOINT

# ENTRYPOINT = 可执行文件
# CMD        = 默认参数
# 最终执行:nginx -g "daemon off;"

ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

1.3 完整对应关系

DockerKubernetes覆盖方式说明
ENTRYPOINT command --entrypoint / command: 覆盖后镜像 CMD 仍拼接
CMD args docker run <img> <args> / args: 只替换 CMD 部分

重要:K8s 中单独覆盖 command 时,镜像里的 CMD 会被完全丢弃,必须同时提供 args


二、exec 格式 vs shell 格式

这是生产环境最常见的坑,必须理解。

2.1 格式对比

# shell 格式 —— 进程是 /bin/sh -c 的子进程,PID != 1
CMD ./captain
ENTRYPOINT "./captain --config /etc/app/config.yaml"
​
# exec 格式 —— 进程直接是 PID 1,能正确接收信号
CMD ["./captain"]
ENTRYPOINT ["./captain"]

2.2 为什么 exec 格式是强制要求

K8s 优雅停机依赖 SIGTERM 能送达 PID 1:

shell 格式启动链:
  /bin/sh -c "./captain"  (PID 1)
      └── ./captain       (PID 2) ← SIGTERM 送不到这里
​
exec 格式启动链:
  ./captain               (PID 1) ← SIGTERM 直接送达

使用 shell 格式会导致:

  • terminationGracePeriodSeconds 失效

  • 容器被强制 SIGKILL,无法优雅关闭连接

  • 数据可能丢失

2.3 shell 格式 ENTRYPOINT 会吞掉 CMD

# 错误:shell 格式的 ENTRYPOINT 会忽略 CMD 和 K8s args
ENTRYPOINT "./captain --config /etc/app/config.yaml"
CMD ["--log-level", "debug"]   # 这行完全无效
​
# 正确:exec 格式才能正确拼接
ENTRYPOINT ["./captain"]
CMD ["--config", "/etc/app/config.yaml"]

三、为什么 Dockerfile 结尾总是写 CMD

3.1 提供"默认运行行为"(最重要)

ENTRYPOINT ["myapp"]
# 不写 CMD:docker run myimage 可能什么都不发生
​
ENTRYPOINT ["myapp"]
CMD ["--config", "/etc/app/config.yaml"]
# 写了 CMD:容器开箱即用

3.2 支持运行时覆盖(灵活性)

# 覆盖 CMD,不影响 ENTRYPOINT
docker run myimage --config /tmp/test.yaml
# 等价于:myapp --config /tmp/test.yaml

3.3 避免硬编码(提高复用性)

# 不推荐:参数写死在 ENTRYPOINT,用户无法修改
ENTRYPOINT ["myapp", "--config", "/etc/app/config.yaml"]
​
# 推荐:符合 12-factor app 设计
ENTRYPOINT ["myapp"]
CMD ["--config", "/etc/app/config.yaml"]

3.4 构建层语义 + 覆盖规则

CMD ["a"]
CMD ["b"]   # 实际生效的是这一行,后者覆盖前者

写在最后避免被后续指令覆盖,也符合"最终状态"的可读性习惯。

3.5 典型 Dockerfile 结构

FROM   ...
RUN    ...   # 安装依赖
COPY   ...   # 复制文件
ENV    ...   # 环境变量
EXPOSE ...   # 声明端口
ENTRYPOINT ["./app"]          # 固定可执行文件
CMD    ["--config", "..."]    # 默认参数,可覆盖

四、何时可以不写 CMD

场景示例说明
ENTRYPOINT 已完整 ENTRYPOINT ["nginx", "-g", "daemon off;"] 不需要 CMD
工具型镜像 FROM alpine 用户自己指定命令

五、K8s command/args 覆盖规则详解

5.1 四种组合

镜像 ENTRYPOINT镜像 CMDK8s commandK8s args最终执行
[ep] [cmd] 未设置 未设置 ep cmd
[ep] [cmd] 未设置 [args] ep args
[ep] [cmd] [command] 未设置 command
[ep] [cmd] [command] [args] command args

5.2 常见踩坑场景

# 踩坑:只写了 command,CMD 被丢弃,配置文件参数也丢失
command: ["./captain"]
# args 没写 → 等价于 ./captain(没有 --config 参数)
​
# 正确:command + args 同时提供
command: ["./captain"]
args: ["--config", "/www/configs/config.yml"]

5.3 推荐的 K8s values 用法

# 场景1:使用镜像默认配置,不写任何 command/args
# CMD 自动生效,最简洁
​
# 场景2:只覆盖参数(最常用)
args:
  - "--config"
  - "/www/configs/config.yml"
​
# 场景3:完全自定义
command: ["./captain"]
args:
  - "--config"
  - "/www/configs/config.yml"
  - "--log-level"
  - "debug"

六、Go 服务 Dockerfile 标准模板

以 xxx 服务为例,结合多阶段构建和交叉编译最佳实践:

# ── 全局 ARG(多阶段共享)──────────────────────────────────────
ARG WORKDIR=/usr/local/xxx
​
# ── build stage ───────────────────────────────────────────────
# 固定跑在原生构建主机上做交叉编译,避免 QEMU 模拟
FROM --platform=$BUILDPLATFORM golang:1.25.5 AS builder
​
ARG WORKDIR
ARG TARGETOS
ARG TARGETARCH
ARG GH_ACCESS_TOKEN
​
# 安装依赖,--no-install-recommends 减少镜像层体积
RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    unzip \
    && rm -rf /var/lib/apt/lists/*
​
# 配置私有仓库访问
RUN git config --global \
    url."https://${GH_ACCESS_TOKEN}@github.com".insteadOf \
    "https://github.com"
​
ENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct
ENV GOPRIVATE="github.com/timekettle/proto,github.com/timekettle/common"
​
WORKDIR ${WORKDIR}
​
# 先拷贝 go.mod/go.sum,利用 Docker 层缓存
# 只要依赖不变,go mod download 层就不会重新执行
COPY go.mod go.sum ./
RUN go mod download
​
COPY . .
​
# 交叉编译,静态二进制
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
    go build -trimpath -ldflags="-s -w" -o bin/captain main.go
​
# ── runner stage ──────────────────────────────────────────────
# 使用 debian slim 而非完整 golang 镜像
# golang:1.25.5 ≈ 800MB,debian:bookworm-slim ≈ 30MB
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS runner
​
ARG WORKDIR
​
# 安装运行时必要包
RUN apt-get update && apt-get install -y --no-install-recommends \
    tzdata \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*
​
ENV TZ=UTC
​
WORKDIR /www
​
# 只复制构建产物,不复制源码
COPY --from=builder ${WORKDIR}/bin/captain .
COPY --from=builder ${WORKDIR}/configs/*.yml ./configs/
​
# 非 root 运行,提升安全性
RUN useradd -u 1001 -r appuser && chown -R appuser /www
USER appuser
​
EXPOSE 9171
​
# ENTRYPOINT 固定可执行文件(exec 格式,保证 PID=1 能收到 SIGTERM)
# CMD 提供默认参数,K8s 可通过 args 覆盖
ENTRYPOINT ["./captain"]
CMD ["--config", "./configs/config.yml"]

七、当前 xxx Dockerfile 存在的问题

问题1:runner 使用完整 golang 镜像

# 现状:~800MB
FROM --platform=$TARGETPLATFORM golang:1.25.5 AS runner
​
# 建议:~30MB,Go 静态二进制不需要 Go 运行时
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS runner
# 或者更极简(无 shell):
FROM --platform=$TARGETPLATFORM gcr.io/distroless/static AS runner

问题2:没有 ENTRYPOINT,CMD 承担全部职责

# 现状:CMD 承担全部职责
CMD ["./captain"]
​
# 风险:K8s values 里一旦有人加了 command 字段
# CMD 会被完全丢弃,--config 参数也跟着消失
# 服务可能因找不到配置文件而启动失败

问题3:以 root 运行

# 现状:没有指定用户,默认 root 运行
# 建议:
RUN useradd -u 1001 -r appuser && chown -R appuser /www
USER appuser

问题4:apt 缓存未清理

# 现状
RUN apt-get update && apt-get install -y tzdata
​
# 建议:清理缓存,减少镜像层体积
RUN apt-get update && apt-get install -y --no-install-recommends tzdata \
    && rm -rf /var/lib/apt/lists/*

八、ConfigMap 挂载与配置文件读取原理

8.1 挂载时序

K8s 调度 Pod
    ↓
kubelet 在节点上准备容器
    ↓
① 挂载所有 Volume(ConfigMap、Secret、PVC 等)← 进程启动前完成
    ↓
② 容器进程启动(执行 ENTRYPOINT + CMD)
    ↓
③ captain 进程读取 /www/configs/config.yml   ← 文件已经存在

8.2 ConfigMap 覆盖镜像内置配置

镜像内置的 /www/configs/config.yml   (构建时 COPY 进去)
        ↓ 被覆盖
K8s 挂载的 ConfigMap config.yml      (运行时挂载,优先级更高)
挂载点会遮盖镜像里同路径的文件,进程实际读到的是 ConfigMap 里的配置。

8.3 应用如何找到配置文件(不需要显式传参)

WORKDIR /www          ← Dockerfile 设置工作目录
CMD ["./captain"]     ← 在 /www 下执行
​
captain 进程的 CWD = /www
代码内默认路径:configs/config.yml
绝对路径等价:/www/configs/config.yml  ← 自动对上

Go 代码中常见实现:

// 1、硬编码默认路径
viper.SetConfigFile("configs/config.yml")
​
// 2、或支持命令行参数但有默认值
flag.StringVar(&cfgFile, "config", "configs/config.yml", "config file path")
​

具体案例:

workPath, err := os.Getwd()          // 取当前工作目录
configPath = filepath.Join(workPath, "configs")  // 拼上 "configs" 子目录
viper.AddConfigPath(configPath)
viper.SetConfigName("config")        // 文件名固定为 config.yml
​
也就是说,配置文件路径 = 当前工作目录 +
config.yml
,没有任何命令行参数或环境变量可以覆盖这个路径。
​
Dockerfile 里 WORKDIR /www,同时复制了:
COPY --from=builder ${WORKDIR}/configs/*.yml /www/configs/
​
所以容器启动时 os.Getwd() 返回 /www,配置文件就是
config.yml,路径是对的,但完全依赖工作目录,不够灵活。
​

如果想支持外部挂载配置(K8s ConfigMap 等场景),建议在 InitConfig() 里加一个环境变量覆盖

方式一:

k8s 层加env
env:
 - name: CONFIG_PATH
   value: /etc/captain/configs
​
应用代码:
if envPath := os.Getenv("CONFIG_PATH"); envPath != "" {
   configPath = envPath
} else {
   configPath = filepath.Join(workPath, "configs")
}

​方式二:

支持 --config flag(需改 main.go)
// main.go
import "flag"
​
func main() {
   configPath := flag.String("config", "", "config file directory path")
   flag.Parse()
   if *configPath != "" {
       os.Setenv("CONFIG_PATH", *configPath) // 传给 InitConfig
   }
   // ...
}

但注意:bootstrap.go 里 InitConfig() 是在 init() 里调用的,init() 比 main() 先执行,所以 flag 解析必须在 init() 之前完成,这个方案需要重构初始化顺序,改动较大。


九、一句话总结

概念总结
CMD 写在结尾 给 ENTRYPOINT 提供"默认参数 + 可覆盖能力",让镜像既能直接运行又保持灵活性
exec 格式 保证进程是 PID 1,能正确接收 SIGTERM,优雅停机的前提
K8s command 覆盖 会完全丢弃镜像 CMD,必须同时提供 args
CM 挂载时序 kubelet 在进程启动前完成挂载,进程启动时文件已存在
runner 镜像选择 Go 静态二进制用 debian-slim 或 distroless,不需要完整 golang 镜像
Auto Copied
Auto Copied
posted @ 2026-05-03 10:58  凡人半睁眼  阅读(26)  评论(0)    收藏  举报