Dockerfile CMD 和 ENTRYPOINT 有什么区别?
Dockerfile 里的 CMD 和 ENTRYPOINT 都和容器启动命令有关,但职责不一样:ENTRYPOINT 更像“固定要运行的程序”,CMD 更像“默认参数”或“默认命令”。
如果只记一句话:想让镜像像一个可执行程序一样运行,用 ENTRYPOINT;想给容器提供一个可以轻松替换的默认启动命令,用 CMD。两者一起用时,通常是 ENTRYPOINT 写可执行文件,CMD 写默认参数。
CMD 是什么?
CMD 用来指定容器启动时的默认命令。它最大的特点是:容易被 docker run 后面的参数覆盖。
常见写法有三种:
dockerfileCMD ["node", "server.js"] CMD node server.js CMD ["--help"]
前两种是完整命令,第三种通常配合 ENTRYPOINT 使用,表示给 ENTRYPOINT 传默认参数。
比如:
dockerfileFROM node:20-alpine WORKDIR /app COPY . . CMD ["node", "server.js"]
直接运行:
bashdocker run my-node-app
实际执行的是:
bashnode server.js
但如果这样运行:
bashdocker run my-node-app node worker.js
CMD ["node", "server.js"] 会被 node worker.js 覆盖。所以 CMD 适合放“可以被用户改掉的默认行为”。
ENTRYPOINT 是什么?
ENTRYPOINT 用来指定容器启动时固定执行的程序。普通的 docker run 参数不会直接覆盖它,而是会追加到 ENTRYPOINT 后面,作为参数传入。
dockerfileFROM alpine ENTRYPOINT ["ping"] CMD ["localhost"]
运行:
bashdocker run ping-image
等价于:
bashping localhost
运行:
bashdocker run ping-image 8.8.8.8
等价于:
bashping 8.8.8.8
这里 ping 是固定程序,localhost 只是默认参数。用户传了 8.8.8.8 后,覆盖的是 CMD,不是 ENTRYPOINT。
exec 形式和 shell 形式有什么区别?
CMD 和 ENTRYPOINT 都有 exec 形式和 shell 形式。
exec 形式写成 JSON 数组:
dockerfileENTRYPOINT ["node", "server.js"] CMD ["--port", "3000"]
shell 形式写成普通字符串:
dockerfileENTRYPOINT node server.js CMD node server.js
推荐优先用 exec 形式,原因有两个。
第一,exec 形式不会额外套一层 shell,参数传递更准确。第二,exec 形式对信号处理更友好。容器里的主进程通常是 PID 1,Docker 停止容器时会先发送 SIGTERM。如果用 shell 形式,真正的业务进程可能变成 shell 的子进程,信号不一定能正确传过去,容易出现容器停止慢、进程残留、优雅退出失效等问题。
dockerfileENTRYPOINT node server.js
实际可能是:
bash/bin/sh -c "node server.js"
更推荐:
dockerfileENTRYPOINT ["node", "server.js"]
如果确实需要 shell 能力,比如环境变量展开、管道、&&,可以使用 shell 形式,但要知道它带来的信号处理问题。生产镜像里更稳妥的方式通常是把复杂逻辑放到脚本里,并在脚本最后使用 exec:
sh#!/bin/sh set -e exec node server.js
再配合:
dockerfileENTRYPOINT ["./entrypoint.sh"]
docker run 怎么覆盖它们?
覆盖 CMD 很简单,直接在镜像名后面追加命令或参数。
bashdocker run my-image bash
如果镜像只有:
dockerfileCMD ["node", "server.js"]
那么 bash 会覆盖原来的 CMD。
如果镜像是:
dockerfileENTRYPOINT ["node", "server.js"] CMD ["--port", "3000"]
执行:
bashdocker run my-image --port 8080
最终命令是:
bashnode server.js --port 8080
如果连 ENTRYPOINT 也想覆盖,需要显式使用 --entrypoint:
bashdocker run --entrypoint sh my-image
注意,--entrypoint 只替换入口程序,镜像原来的 CMD 仍可能作为参数拼到后面。排查问题时可以看镜像的 Config.Entrypoint 和 Config.Cmd:
bashdocker inspect my-image
什么时候只用 CMD?
如果镜像只是给一个默认启动命令,用户经常需要替换整个命令,用 CMD 就够了。
dockerfileFROM node:20-alpine WORKDIR /app COPY . . CMD ["npm", "run", "dev"]
默认启动开发服务,但用户也可以方便地执行:
bashdocker run my-app npm test
这种场景下,CMD 的可覆盖特性反而是优点。
什么时候用 ENTRYPOINT + CMD?
如果希望镜像表现得像一个固定工具,推荐 ENTRYPOINT + CMD。
dockerfileFROM alpine RUN apk add --no-cache curl ENTRYPOINT ["curl"] CMD ["--help"]
默认运行会输出 curl 帮助;传入参数时:
bashdocker run curl-image -I https://example.com
实际执行:
bashcurl -I https://example.com
服务型镜像也可以这样写:
dockerfileENTRYPOINT ["node", "server.js"] CMD ["--port", "3000"]
这样镜像默认监听 3000,用户需要改端口时只覆盖参数即可。
docker compose 里的 command 和 entrypoint 对应什么?
在 Docker Compose 里,command 对应 Dockerfile 里的 CMD,entrypoint 对应 Dockerfile 里的 ENTRYPOINT。
yamlservices: app: image: my-app command: ["--port", "8080"]
如果镜像里有:
dockerfileENTRYPOINT ["node", "server.js"] CMD ["--port", "3000"]
Compose 的 command 会把默认参数改成:
bashnode server.js --port 8080
如果同时覆盖入口和参数:
yamlservices: app: image: my-app entrypoint: ["sh"] command: ["-c", "env && sleep 3600"]
这常用于临时调试。
常见写法怎么选?
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 默认启动一个服务,用户可能替换整条命令 | CMD | 覆盖方便 |
| 镜像就是一个 CLI 工具 | ENTRYPOINT + CMD | 固定工具名,参数可变 |
| 服务程序固定,只想允许改参数 | ENTRYPOINT + CMD | 程序稳定,参数灵活 |
| 需要临时进入容器排查 | --entrypoint sh | 直接绕过原入口 |
| 需要信号正确传给主进程 | exec 形式 | 避免 shell 吞信号 |
实际项目里可以这样记:CMD 管默认值,ENTRYPOINT 管主程序;exec 形式优先,shell 形式慎用;Compose 里的 command 改 CMD,entrypoint 改 ENTRYPOINT。