Docker 常用命令的 5 个坑,第四个我踩了两次

上周 code review,看到同事的 Dockerfile 里写着 ADD requirements.txt,我愣了一下------这哥们把 ADD 当 COPY 用了。再往下看,docker-compose.yml 里的 depends_on 写得信心满满,但服务启动顺序根本不对。

这些问题我当年全踩过,有的还踩了不止一次。今天把最常见的 5 个坑整理出来,希望你少走点弯路。

COPY 和 ADD,真不是一回事

很多人觉得 ADD 是 COPY 的"增强版",能复制文件还能解压 tar、下载远程 URL。听着挺方便,实际上是个坑。

dockerfile 复制代码
# 错误写法:ADD 会隐式解压 tar 文件,行为不可控
ADD app.tar.gz /app/
# 你以为复制了个压缩包,其实它被解压了

# 正确写法:明确意图,用 COPY 就完事
COPY app.tar.gz /app/

ADD 的隐藏行为太多了。你传了个 tar 文件进去,它自动解压;你写了个 URL,它自动下载。这些"贴心"操作在 CI 环境里经常翻车。

dockerfile 复制代码
# 错误写法:用 ADD 下载远程文件,多了一层不可控的网络依赖
ADD https://example.com/config.json /app/config.json

# 正确写法:用 RUN + curl,行为透明,还能加校验
RUN curl -fSL https://example.com/config.json -o /app/config.json

Docker 官方文档的建议很直接:首选 COPY。只有在你明确需要自动解压 tar 的时候才用 ADD。记住这条就够了。

构建缓存的顺序,别写反了

Docker 构建是一层一层来的,每一层都有缓存。只要上面某一层变了,后面的层全部重新构建。这个机制很多人知道,但写 Dockerfile 的时候经常忽略。

dockerfile 复制代码
# 踩坑写法:每次改一行代码,npm install 都要重跑
FROM node:18-alpine
WORKDIR /app
COPY . .              # 任何文件变动都导致这一层缓存失效
RUN npm install        # 于是每次构建都要重新装依赖,慢到怀疑人生

正确的做法是把不常变的依赖文件先复制进去,让缓存尽量命中。

dockerfile 复制代码
# 优化写法:先复制 package.json,利用缓存
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # 只有依赖文件变了才重新安装
RUN npm install                           # 这层缓存命中率极高
COPY . .                                  # 代码改动不影响上面的缓存

还有一个容易忽略的点:.dockerignore 没配。node_modules.gitdist 这些目录全被 COPY 进去了,不仅镜像臃肿,缓存也容易被误触发。

text 复制代码
# .dockerignore 示例,建议每个项目都配一个
node_modules
.git
dist
*.log

CMD 和 ENTRYPOINT,我踩了两次

这个坑我必须说说,因为它太隐蔽了。

第一次踩坑是写了个 CMD 的 shell 格式,结果容器启动后信号传递出了问题,docker stop 要等 10 秒才强杀。

dockerfile 复制代码
# 踩坑写法:shell 格式,CMD 会被 /bin/sh -c 包一层
CMD node server.js
# 问题:进程 PID 不是 1,信号传不到 node 进程,优雅停机直接废了

# 正确写法:exec 格式,进程直接是 PID 1
CMD ["node", "server.js"]

第二次踩坑更离谱。我在基础镜像里写了 ENTRYPOINT,然后在业务镜像里用 CMD 传参数,结果参数怎么都传不过去。

dockerfile 复制代码
# 基础镜像 Dockerfile.base
ENTRYPOINT ["python", "app.py"]
# CMD 被 ENTRYPOINT 的 exec 格式"接管"了,下面的 CMD 只是默认参数

# 业务镜像 Dockerfile
FROM mybase:latest
CMD ["--port", "8080"]   # 这行其实是给 ENTRYPOINT 传参数,不是独立命令
# 实际执行的是:python app.py --port 8080

搞明白两者的关系很重要:ENTRYPOINT 定义容器的"主命令",CMD 提供默认参数。如果你不需要自定义容器行为,只写 CMD 就行。两个一起用的时候,一定要用 exec 格式,不然参数传递会一脸懵。

dockerfile 复制代码
# 推荐写法:两个都用 exec 格式,行为清晰
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# docker run 时传的参数会覆盖 CMD,但不会覆盖 ENTRYPOINT

docker-compose 的 depends_on,不等服务就绪

这个坑我见身边至少三个人踩过。depends_on 只保证容器启动顺序,不等服务真正 ready。

yaml 复制代码
# 踩坑写法:以为 depends_on 会等 MySQL 就绪
services:
  web:
    build: .
    depends_on:
      - db              # 只保证 db 容器先启动,不等 MySQL 准备好接受连接
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret

web 容器启动的时候,MySQL 可能还在初始化。你的应用一上来就连数据库,直接报 Connection refused

yaml 复制代码
# 方案一:加健康检查,让 depends_on 等 condition: service_healthy
services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy   # 等 db 健康检查通过才启动
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]  # MySQL 自带的探活命令
      interval: 5s          # 每 5 秒检查一次
      timeout: 3s           # 超时 3 秒算失败
      retries: 10           # 连续失败 10 次才判定不健康

如果不想配健康检查,还有个土办法------在应用启动脚本里加重试逻辑。虽然不够优雅,但胜在简单粗暴。

yaml 复制代码
# 方案二:应用侧重试(适合小项目快速搞定)
services:
  web:
    build: .
    command: >
      sh -c "
        for i in $$(seq 1 30); do
          nc -z db 3306 && break;   # 探测 MySQL 端口是否通
          echo 'Waiting for db...';
          sleep 2;
        done;
        node server.js              # 数据库就绪后再启动应用
      "
    depends_on:
      - db

最后

五个坑简单总结一下:

  • COPY 和 ADD 别混用,绝大多数场景用 COPY 就够
  • Dockerfile 里把不常变的层放前面,充分利用构建缓存
  • 别忘了配 .dockerignore,不然缓存和镜像体积都受影响
  • CMD 和 ENTRYPOINT 优先用 exec 格式,别用 shell 格式
  • docker-compose 的 depends_on 不等服务就绪,要加健康检查或重试逻辑

这些都是实战里踩过才记住的,希望对你有用。你踩过哪些 Docker 的坑?评论区聊聊。

相关推荐
芝士土豆薯片3 小时前
CSS Grid 布局实战指南,看这篇就够了
程序员
嘟嘟嘟95276 小时前
Spotify Honk 的启示:AI 编程日常
人工智能·程序员·ai编程
程序员cxuan6 小时前
从 0 到 1 速通 WorkBuddy
人工智能·后端·程序员
野生的码农19 小时前
5 年级的儿子居然学会了破解。。。
程序员
这个DBA有点耶1 天前
数据库教程:从零基础到实战的完整学习路径(2026版)
数据库·程序员·代码规范
kyriewen1 天前
GPT-6 拿下模型众测第一:我拆完 30 个主题的实时榜单,「最强 AI」得看你问哪个场景
人工智能·程序员·ai编程
Sam_Deep_Thinking1 天前
单一职责原则:JAVA LocalDate的设计取舍
java·后端·程序员·单一职责原则
codigger1 天前
程序员别再踩这 3 个坑——做了五年开发,我把能踩的坑全踩了一遍
后端·ai·程序员·架构·程序员职场