news 2026/10/22 3:27:05

Dockerfile核心指令详解与生产环境最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dockerfile核心指令详解与生产环境最佳实践

第一次真正“用明白”Dockerfile,是在一个项目交付前的那个深夜。在那之前,我的日常操作是:起一个容器,手动装环境、拷贝代码、跑通服务,然后docker commit把容器状态存成镜像,再导出给同事。表面上看一切正常,直到需要升级某个依赖时,我在那个“黑箱”镜像里翻来覆去找不到当初的配置,只能重头再折腾一遍。从那一刻起,我意识到:Dockerfile 才是 Docker 体系里最值得花时间研究的东西。

这篇文章是《Docker入门》系列的第四篇,专门讲 Dockerfile。如果你已经会docker run、docker ps这些基础操作,但对“镜像到底是怎么生产出来的”还一知半解,这篇文章就是为你准备的。我会从 Dockerfile 的定位讲起,把核心指令逐个拆开讲清楚,再给出一套完整的实战案例,最后分享我在生产环境里踩过的几个坑。全程都是能直接用、能省时间的经验,不是教科书式的罗列。

1. 为什么需要 Dockerfile:从手工容器到可复现镜像

先回忆一下很多人刚接触 Docker 时的路径:docker run启动一个容器,在容器里安装各种软件,把代码传进去,最后docker commit把当前状态保存成镜像。这套流程很直观,一开始甚至会觉得“这不挺好吗”?但问题很快会暴露出来。

1.1 手工构建镜像的三个痛点

第一个痛点是“说不清”。三个月后回看一个旧镜像,你只能大概记得“当时装了些东西”,但具体装了哪些包、改过什么配置、执行过什么命令,完全没有记录。如果这个镜像要交接给同事,你只能靠口述,对方只能靠猜。

第二个痛点是“不可复现”。换一台机器、换一个人,走同一套手工流程,几乎不可能得到一模一样的镜像。版本会有差异,源会有差异,甚至一条命令失败的时机都不同。

第三个痛点是“难演进”。依赖要升级时,你得重新进容器,手动改、手动测,改完后提交一个新层。日积月累,镜像里堆满了历史遗留的临时文件、旧版本的缓存,体积越来越大,构建一个人肉流水线,效率极低。

1.2 Dockerfile 的本质:把构建过程变成文本

Dockerfile 用一系列指令描述“从哪个基础镜像开始、执行什么命令、复制什么文件、暴露什么端口、启动时运行什么程序”。它把手工构建的整个过程文本化,可以放进 Git 里做版本管理,可以参与代码评审,可以让 CI 系统自动执行。

一个最小的 Dockerfile 长这样:

FROM alpine:3.20 RUN echo "hello dockerfile" CMD ["echo", "hello dockerfile"]

你可以用docker build -t hello-test .构建它,然后docker run hello-test看到输出。虽然这个例子没有任何实际业务价值,但它清晰展示了 Dockerfile 的基本形态:每一条指令都对应镜像构建过程中的一步。

我常用一个类比来解释 Dockerfile 的价值:手工做菜 vs 写菜谱。手工做菜好吃,但每次味道都不一样,别人也没法照着一比一复刻;写菜谱看起来多花了点时间,但换任何一个人、换一口锅,按步骤执行就能得到基本一致的成品。Dockerfile 就是菜谱,镜像是做出来的菜。对一个要长期维护、多人协作、自动化部署的项目来说,菜谱的价值远大于一次性做出来的菜。

这一篇建议的阅读门槛是:已经会用docker run跑过容器,知道镜像和容器的区别。如果完全没接触过 Docker,建议先补一下前几篇的基础概念。下面我默认你已经能起容器了,接下来直接进入 Dockerfile 的核心指令。

2. 核心指令逐个拆解:每一行都有讲究

Dockerfile 的指令不算多,但每个都有它存在的理由和容易踩的坑。这一节我会按实际使用频率来拆解,重点讲“为什么这么做”,而不是只罗列语法。

2.1 FROM:一切构建的起点

FROM指定基础镜像,必须是 Dockerfile 里第一个非注释指令。基础镜像的选择决定了你整个镜像的“底层生态”,选错会让你后面多折腾不少时间。

基础镜像大致分四类:

类型典型代表优势劣势
完整发行版ubuntu、debian软件包齐全,调试方便体积大
精简版alpine体积小,安全面小musl 替代 glibc,部分二进制不兼容
运行时镜像node、python、openjdk语言环境开箱即用基础系统可能不够灵活
空镜像scratch体积最小几乎什么都没有,只能跑静态编译产物

很多人一上来就选 alpine,图它体积小,结果到生产发现缺证书、缺时区数据、缺系统依赖,反而折腾半天。我的习惯是:如果跑的是静态编译的 Go 程序,用scratch或alpine;如果是 Node、Python 这类需要系统依赖的项目,优先选官方镜像的-slim变体,比如node:20-bookworm-slim,体积不大,缺系统库时用包管理器装也方便;只有确认依赖非常简单时,再考虑 alpine。

这里还要强调一个标签问题:不要用latest。latest不是一个固定的版本,它会随着发布更新而变化。今天构建得到一个系统,三个月后构建可能就是一个完全不同的底层系统,这相当于给“在我机器上能跑”这个老问题换了一种形式继续存在。生产环境请固定具体版本标签,比如node:20.17.0-bookworm-slim。

2.2 RUN:指令越少越精,但别忘了清理

RUN在构建阶段执行命令,主要用于安装软件、准备环境。它的机制很容易被误解:每一条 RUN 都会生成一层镜像,所以 RUN 条数越多,镜像层数越多,构建越慢,体积越大。

理解这一点后,就明白为什么要“合并命令”了:

# 反例 RUN apt-get update RUN apt-get install -y curl # 正例 RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*

反例看起来只是多了一行,但问题有三个:第一,apt-get update产生的列表缓存会留在这个镜像层里,白白占用体积;第二,update 和 install 分开写时,Docker 缓存可能命中 update 层,之后 install 拿到的却是过期列表;第三,违反了“尽量少建层”的原则。所以把更新、安装、清理连在一起,才是正确的姿势。

我还想补充一点:RUN内如果涉及连续多个命令,建议用&&连接,并且命令过长时用\换行,保证 Dockerfile 的可读性。这个习惯在代码评审时非常加分。

2.3 COPY vs ADD:绝大多数时候只需要 COPY

COPY把构建上下文里的文件或目录复制到镜像中。ADD除了复制,还支持从远程 URL 下载,以及自动解压本地 tar 包。听起来 ADD 更强大,但正是这些额外功能带来了坑:

  • ADD 的远程 URL 下载发生在构建时,意味着每次构建都可能拉取外部内容,镜像的构建结果确定性被打折扣;
  • ADD 复制 tar 包会自动解压,这一点经常让人措手不及;
  • ADD 下载远程文件后不会自动清理临时文件,镜像容易变大。

所以最佳实践非常明确:复制本地文件一律用 COPY,只有需要自动解压本地 tar 包时才考虑 ADD;远程 URL 下载用 RUN 配合 curl 或 wget 更可控,还能顺手清理。

COPY 有个非常实用的细节:--chown参数,可以在复制时就指定文件所有者:

COPY --chown=node:node package.json /app/package.json

这对那些容器内以非 root 用户运行的应用来说非常重要,能避免后续运行时报权限错误。

2.4 CMD 与 ENTRYPOINT:命令入口的正确打开方式

CMD指定容器启动时执行的命令。它有几个特点:Dockerfile 里多个CMD只有最后一个生效;docker run后面跟的参数会覆盖CMD。

ENTRYPOINT定义的是容器启动时“必须运行”的命令,docker run的参数会追加到它后面,而不是替换它。把两者组合起来,是 Docker 社区最经典的入口模式:

ENTRYPOINT ["docker-entrypoint.sh"] CMD ["--help"]

容器启动时实际执行的是docker-entrypoint.sh --help。这是一个很聪明的设计:入口脚本负责初始化逻辑(等待数据库就绪、生成配置文件、检查环境变量),CMD 提供默认参数,而用户可以用docker run myimage --version传入自定义参数来覆盖默认值。

这里必须强调一个高频坑:指令要写成数组形式(exec form),而不是字符串形式(shell form)。

# 推荐 CMD ["node", "src/server.js"] # 不推荐 CMD node src/server.js

数组形式会直接以 exec 系统调用执行目标进程,信号能正常传递给它,退出码也能正确返回。字符串形式会被/bin/sh -c包一层,中间多出一个 shell 进程,Docker 发给主进程的 SIGTERM 可能被 shell 吞掉,导致应用来不及平滑退出。生产环境必须用数组形式,除非你确实需要 shell 展开,比如CMD ["sh", "-c", "echo $HOME"]这种显式写法。

2.5 ENV、ARG、WORKDIR:容易被忽视却天天在用的指令

ENV声明环境变量,既影响构建期(可用于 RUN 里的命令),也会成为运行期容器里的环境变量:

ENV NODE_ENV=production ENV APP_PORT=3000

ARG与ENV不同,它只在构建期有效。一个常见用途是配合--build-arg传入构建参数,比如临时替换镜像源:

ARG APT_MIRROR=mirrors.example.com RUN sed -i "s|deb.debian.org|${APT_MIRROR}|g" /etc/apt/sources.list

构建时用docker build --build-arg APT_MIRROR=xxx就能覆盖默认值。注意:如果 ARG 里放了敏感信息(比如密码),它会在镜像历史里留下痕迹,需要谨慎处理,不要存 token 和密钥。

WORKDIR设置后续指令的工作目录,目录不存在时会自动创建。它比RUN cd /app更可靠,因为后者只影响当前这一条 RUN,不会改变后续指令的目录。统一写WORKDIR /app,整份 Dockerfile 的上下文就清晰多了。

2.6 EXPOSE、USER、HEALTHCHECK:生产环境离不开的细节

EXPOSE只是声明“容器内的服务监听这个端口”,它并不会真的打开端口。真正映射端口的是docker run -p或 docker-compose。声明的主要作用是文档化和方便自动化工具识别。

USER用来切换运行用户。默认情况下容器以 root 运行,这是一个安全隐患。生产环境应该创建非 root 用户并切换:

RUN useradd -m -s /bin/bash appuser USER appuser

如果基础镜像自带运行用户(比如官方 node 镜像的node用户),直接USER node就行。

HEALTHCHECK是很多人没注意到的指令,但它对生产环境很有价值。它告诉 Docker 如何检查容器是否存活:

HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -q -O - http://localhost:3000/ || exit 1

有了它,编排系统才能准确判断服务是“活着”还是“能对外提供服务”,避免出现“进程在但服务已经不可用”的假活状态。

到这里,我把最常用的指令讲完了。你可以先照着写一版,跑通之后再往下看缓存和多阶段构建,这两块才是 Dockerfile 性能优化的真正分水岭。

指令作用高频坑
FROM指定基础镜像别用 latest
RUN构建期执行命令分层多、临时文件残留
COPY复制文件进镜像顺序影响缓存命中
ADD复制+解压+远程下载自动解压和远程不可控
CMD默认启动命令要写成数组形式
ENTRYPOINT入口命令和 CMD 配合时机
ENV环境变量构建期运行期都生效
ARG构建参数注意敏感信息
WORKDIR工作目录尽量不用 cd
EXPOSE声明端口不是真的开端口
USER切换用户注意目录属主
HEALTHCHECK健康检查生产必配

3. 层与缓存:Dockerfile 性能优化的底层逻辑

很多人写 Dockerfile 能从三分写到十分,差别就在懂不懂“层”和“缓存”。这一节讲清楚这两件事,你的构建速度和镜像体积都会有质的提升。

3.1 镜像分层:像 Git 提交一样理解

Docker 镜像由一系列只读层叠加而成,每一层对应 Dockerfile 里的一条指令。构建时,如果某条指令之前的内容没有变化,Docker 会直接复用已有的缓存层;只有发生变化的那一层以及它后面的层,才会重新执行。

我常用 Git 提交来类比:每一层就像一个 commit,构建新镜像就像在旧 commit 上追加新提交。如果你改了中间某一层,中间这个 commit 变了,它之后所有的层都会因为“父层”变化而重新生成,即使那些层本身的指令没变。

所以你会得到一个重要推论:Dockerfile 里指令的顺序,直接影响构建缓存命中率。

3.2 缓存命中机制:怎么判断层是否需要重建

Docker 判断缓存是否可用,主要看指令本身是否有变化,以及指令涉及的文件是否变化。以 COPY 为例,如果构建上下文里被复制的文件内容一致,这层就会命中缓存。

你可以在构建输出里看到明确标志:

$ docker build -t myapp:v1 . Sending build context to Docker daemon 1.024MB Step 1/7 : FROM node:20.17.0-bookworm-slim ---> a1b2c3d4e5f6 Step 2/7 : WORKDIR /app ---> Using cache ---> b2c3d4e5f6a7

当出现Using cache,说明该层直接复用,没有执行任何操作。优化就是让尽可能多的“稳定层”命中缓存,把变化的“不稳定层”尽量往后放。

3.3 缓存优化的核心策略:依赖声明先复制,业务代码后复制

最典型的例子是 Node 项目的依赖安装:

# 推荐:先复制依赖声明文件,再安装依赖 COPY package.json package-lock.json ./ RUN npm install COPY src ./src

因为src业务代码变化频繁,放在后面;package.json和package-lock.json变化较少,先复制。这样大部分情况下,npm install 这层能稳定命中缓存,改一行业务代码不需要重装依赖。

反过来,如果你把所有文件一次性复制进去再npm install,那么只要改一行代码,整个依赖安装层就全部失效,每次构建都要重新下载安装。Python 项目同理,先COPY requirements.txt,再复制业务代码;Go 项目先复制go.mod、go.sum,再复制源码。这个思路在所有语言生态里都适用。

3.4 .dockerignore:被严重低估的加速器

构建上下文的大小直接影响构建速度。如果项目根目录有node_modules、.git、dist,COPY . .时会把这些目录原封不动地打包传输给 Docker 守护进程,几十上百 MB 的体积就这样白白浪费在传输上。

项目根目录放一个.dockerignore非常有必要:

node_modules .git .gitignore *.log Dockerfile docker-compose.yml

这个文件不是可选项。检查上下文大小也简单,构建时看开头的Sending build context to Docker daemon这一行输出,如果明显比预期大,基本就是漏了.dockerignore。

3.5 多阶段构建:一套 Dockerfile 同时拿到完整构建环境和瘦小运行镜像

多阶段构建是我最常用的优化手段之一。它在单个 Dockerfile 里写多个FROM,每个FROM开启一个构建阶段。只有最后一个阶段会成为最终镜像,前面的阶段只是“临时脚手架”。

典型场景是编译型项目:构建阶段需要完整的编译器、依赖库和构建工具,运行阶段只需要最终产物。举例说明:

# 构建阶段 FROM node:20-bookworm-slim AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm install COPY src ./src RUN npm run build # 运行阶段 FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

这个例子里,构建阶段用完整的 Node 环境编译前端代码,运行阶段只把构建产物复制进 nginx 镜像。最终镜像不包含任何 npm 包、编译器、构建日志,体积能减少一个数量级。

多阶段构建最妙的地方在于,它可以灵活组合:构建阶段用大而全的基础镜像,运行阶段用 slim 或 alpine,想再压体积时还可以在最终阶段只复制二进制文件。这个思路值得你在自己的项目里马上试一遍。

4. 实战:从零写出一个可生产的应用镜像

理论讲了一堆,现在真正动手写一个能上生产环境的镜像。我用一个最常见的场景:Node.js 后端服务,使用 Express 框架,监听 3000 端口。目标是构建一个体积合理、可维护、适合自动化部署的镜像。

4.1 项目结构与目标

假设项目目录结构如下:

myapp/ ├── .dockerignore ├── Dockerfile ├── package.json ├── package-lock.json └── src/ └── server.js

server.js内容很简单:

const express = require('express'); const app = express(); app.get('/', (req, res) => res.send('Hello Dockerfile')); app.listen(3000, () => console.log('app listening on 3000'));

4.2 第一版:能跑,但别高兴太早

第一版很多人会这样写:

FROM node:20 WORKDIR /app COPY . . RUN npm install EXPOSE 3000 CMD ["node", "src/server.js"]

构建和运行确实没问题,但它的问题是一堆:

  • 没有.dockerignore,构建上下文会把本地的node_modules一起传给 Docker 守护进程;
  • COPY . .不分青红皂白复制所有文件,改一行源码就导致后续层全部缓存失效;
  • 没有切换非 root 用户;
  • 基础镜像标签用node:20,存在漂移风险。

每一项单独看不致命,合在一起,这套镜像在长期迭代里会越来越难维护。所以要写就写第二版。

4.3 第二版:规范化的标准做法

先补上.dockerignore:

node_modules .git .gitignore *.log Dockerfile docker-compose.yml

然后重写 Dockerfile:

FROM node:20.17.0-bookworm-slim WORKDIR /app # 先装依赖,这一步充分利用缓存 COPY package.json package-lock.json ./ RUN npm ci --only=production # 再复制业务代码,业务代码变化不会影响依赖层 COPY src ./src # 创建并切换到非 root 用户 USER node EXPOSE 3000 CMD ["node", "src/server.js"]

这里有几个细节值得说明:

  • 用npm ci而不是npm install。npm ci严格按照package-lock.json安装,结果可复现,速度也更快;
  • --only=production跳过开发依赖,不会把测试框架打包进镜像;
  • USER node避免容器内以 root 运行;
  • COPY 只复制需要的目录,而不是整个上下文。

构建并运行:

docker build -t myapp:v2 . docker run -p 3000:3000 myapp:v2

浏览器访问http://localhost:3000,能看到响应就是构建成功。测试多次,流程完全可复现,不会出现在你机器上能跑、换台机器就挂的情况。

4.4 第三版:多阶段构建与发布流程

第二版已经不错了。但如果你想进一步瘦身,同时保留本地开发、测试、构建全流程的便利,可以引入多阶段构建。对纯 Node 项目,运行阶段本身不需要编译,多阶段构建的价值更多体现在“依赖隔离”上:

# ---------- 构建阶段 ---------- FROM node:20.17.0-bookworm-slim AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY src ./src # 如果有编译步骤(TypeScript、打包等),在这里执行 # RUN npm run build # ---------- 运行阶段 ---------- FROM node:20.17.0-bookworm-slim ENV NODE_ENV=production WORKDIR /app COPY --from=builder /app/package.json ./ COPY --from=builder /app/package-lock.json ./ RUN npm ci --only=production && npm cache clean --force COPY --from=builder /app/src ./src USER node EXPOSE 3000 CMD ["node", "src/server.js"]

这种写法看起来绕了一层,但好处是逻辑非常清晰:builder 阶段的npm ci装的是全量依赖(包括开发依赖),供测试和构建使用;运行阶段只复制 package.json 和 lock 文件过来,重新安装生产依赖,确保最终镜像不含任何 devDependencies。

构建、打标签、推送一气呵成:

docker build -t myapp:v3 . docker tag myapp:v3 registry.example.com/myapp:v3 docker push registry.example.com/myapp:v3

到这一步,这个镜像已经可以发布到自己的镜像仓库,后续接入 CI/CD 也顺理成章。

5. 我在生产环境踩过的坑与排查方法

理论会了,样例也跑了,但真正上手时还是会遇到各种奇怪的问题。这一节我把自己的踩坑经历整理成可直接对照的经验,每一条都是查了很多资料、试了很多方案才总结出来的。

5.1 时区问题:容器里的时间和宿主机对不上

很多应用要读当前时间,而容器的默认时区是 UTC。如果你的业务需要的是北京时间,直接跑起来后打印的日志时间会差 8 小时。更麻烦的是,大多数基础镜像里没有安装时区数据,即使设置了 TZ 环境变量,容器内也可能没有数据可读。

我的解法是在基础镜像阶段就装好时区数据并设置默认时区:

ENV TZ=Asia/Shanghai RUN apt-get update && apt-get install -y tzdata \ && rm -rf /var/lib/apt/lists/*

把 update、install、cleanup 连在一个 RUN 里,既避免体积膨胀,又保持层数精炼。时区问题很容易被忽略,等你在排查日志时间对不上时,才会意识到它有多坑。

5.2 依赖缓存:镜像体积的隐形杀手

很多基础镜像安装软件包时不会自动清理缓存。如果不显式删除/var/lib/apt/lists/*,这些列表文件会一层层累积在镜像里,表面上看每一个都不大,长期迭代后体积会缓慢膨胀。

除了 apt 缓存,还要小心那些在 RUN 过程中从公网下载的临时文件。原则是:所有临时下载、临时安装产生的文件,尽量在同一个 RUN 指令里完成清理。我见过一个镜像因为某层下载了编译工具忘了清理,最终体积比预期大了将近一倍。

如果团队使用内部镜像源,还可以把“换源”这个操作放在 Dockerfile 的第一层,这样后续构建时这个配置能被所有层复用,团队内的基础镜像也能保持统一。

5.3 启动前初始化:用 ENTRYPOINT 接住复杂启动逻辑

生产环境经常遇到“等数据库就绪再启动”“根据环境变量生成配置文件”这类需求。有些人会把这些逻辑写进应用代码里,这当然可以;但如果你不想改代码,只想在容器启动时先跑一段初始化脚本,更通用的做法是使用 ENTRYPOINT。

我的做法是写一个entrypoint.sh,复制进容器,然后用 ENTRYPOINT 执行它,CMD 提供默认参数:

COPY entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] CMD ["start"]

entrypoint.sh内部可以检查环境变量、生成配置、等待依赖端口打开。注意最后用exec执行最终命令,这样信号才能直接传给主进程:

#!/bin/sh echo "waiting for database..." # 等待逻辑 exec node src/server.js

如果不加exec,主进程会变成 shell 的子进程,信号传递会出问题,这一点和第二节课强调的数组形式是同一条底层原则。

5.4 用户权限:非 root 运行后的一连串问题

“切换非 root 用户”这句话说起来简单,实际操作中很容易踩坑。核心问题有两个:目录属主不对,和数据卷挂载后权限不匹配。

第一个问题,切换USER appuser后,如果应用要往工作目录写文件,而工作目录的属主是 root,运行时会直接报permission denied。解决办法是显式创建目录并修改属主:

RUN mkdir -p /app/data && chown -R appuser:appuser /app/data

第二个问题更隐蔽。如果运行时用docker run -v mydata:/app/data挂载外部数据卷,宿主机上这个目录的属主往往不是容器内的用户 id,容器里的进程依然可能没有写权限。这种情况一般在部署时才暴露,调试起来很费劲,所以最好在写 Dockerfile 时就把所有需要写文件的目录规划好,并配合文档写明需要映射的用户 id。

5.5 信号与平滑退出:容器“假停”的罪魁祸首

最后这个坑,是很多应用上容器之后才发现的:docker stop会向主进程发送 SIGTERM,给应用一个平滑退出的机会。但如果你的 Dockerfile 里写成字符串形式(CMD node src/server.js),Docker 实际启动的是/bin/sh -c "node src/server.js",主进程是 shell 而不是 node。SIGTERM 发给 shell 后,shell 可能直接退出,node 却还在后台运行,容器状态和进程状态不一致,应用来不及做清理工作,甚至可能丢数据。

正确做法就是全文反复强调的:CMD和ENTRYPOINT都使用数组形式,让主进程直接以 exec 方式运行。如果实在需要 shell 的变量展开,就在数组形式里显式写sh -c,并且脚本内部用exec拿到最终进程。

5.6 常见问题速查表

现象大概率原因解决方向
镜像太大依赖缓存未清理、COPY 了多余文件合并 RUN 并清理缓存,补.dockerignore
构建很慢COPY 顺序不对,业务代码变化导致依赖层缓存失效先复制依赖声明文件,再复制业务代码
容器启动后立即退出CMD 写错、应用启动失败先docker logs看日志,核对 CMD 数组形式
权限报错非 root 用户对目录无写权限创建目录并 chown,规划数据卷属主
日志时间差 8 小时容器时区是 UTC安装 tzdata,设置 TZ 环境变量
端口无法访问EXPOSE 只是声明,未做端口映射用docker run -p映射端口

一些关于持续演进的建议

Dockerfile 写得好这件事,其实没有终点。我自己的标准模板每隔半年就会重构一遍:镜像源变了、运行环境依赖变了、安全扫描策略变了,Dockerfile 就得跟着调整。这篇文章里的示例,是我目前觉得对大多数后端 Web 服务最稳妥的写法,但它绝不是唯一解。

如果你读完只想记三件事,我个人的排序是这样的:第一,把 COPY 命令按“依赖声明文件先复制、业务代码后复制”的顺序写,这比任何花哨技巧都更能节省构建时间;第二,生产环境一定用固定标签的基础镜像,不要用 latest;第三,CMD 和 ENTRYPOINT 默认写成数组形式,并尽量以非 root 用户运行容器。这三条做到位,你的 Dockerfile 已经超过大部分项目了。

剩下的细节,就交给实践和时间去慢慢磨吧。愿你的镜像瘦一点,构建快一点,半夜被线上问题叫醒的次数少一点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:26:10

V4L2与DMA-BUF联动解析:相机内核内存映射与泄漏排查实战

如果你是在应用层或者 HAL 层调相机,遇到“申请内存失败”、“预览莫名其妙花屏”、“相机开一段时间就再也打不开”这类问题,通常第一反应是去翻上层逻辑。但根据我维护这套 QCOM_CAMERA 内核驱动的经验,九个问题里有八个最后都戳到内核内存…

作者头像 李华
网站建设 2026/10/22 3:24:45

Docker入门指南:从环境一致性到容器编排实战

1. 先搞清楚 Docker 到底解决了什么问题1.1 从“环境不一致”这个老大难说起你肯定遇到过这种情况:代码在自己电脑上跑得好好的,一到同事的机器或者测试服务器上就报错。排查半天,发现是环境变量没配上、依赖库版本对不上,甚至操作…

作者头像 李华
网站建设 2026/10/22 3:23:00

网络参考模型详解:从OSI到TCP/IP,掌握数据封装与排障思路

2. 网络参考模型想搞懂华为数通方向的HCIA,网络参考模型是绝对绕不开的第一道门槛。不管你以后是去调路由器、配交换机,还是跟人家排查一个“网络不通”的问题,最后都会回到这张图上:数据到底是怎么从一台设备跑到另一台设备&…

作者头像 李华
网站建设 2026/10/22 3:21:43

Redis主从复制“三次握手”源码拆解:状态机与同步机制

用INFO replication观察过一次主从同步的人&#xff0c;应该都有过类似的经历&#xff1a;从库刚配好replicaof&#xff0c;日志里刷出一堆“MASTER <-> REPLICA sync started”&#xff0c;主库那边同步进度也跳了好几个状态&#xff0c;最后两边都指向online&#xff0…

作者头像 李华
网站建设 2026/10/22 3:20:55

MATLAB实战:深度置信网络(DBN)在股票时间序列预测中的应用

最近在折腾时间序列预测的时候&#xff0c;发现深度置信网络&#xff08;DBN&#xff09;这个老牌选手居然在股票数据上表现挺有意思。今天就拿MATLAB实操一把&#xff0c;咱们边写代码边聊门道。先说清楚我为什么回头看DBN。现在一提深度学习做预测&#xff0c;大家条件反射就…

作者头像 李华
网站建设 2026/10/22 3:20:51

Visual Studio 2017 Installer 闪退打不开?三步排查修复指南

各位搞 Visual Studio 2017 的朋友&#xff0c;不知道你们有没有遇到过这种情况&#xff1a;某天打开电脑&#xff0c;想继续写点代码&#xff0c;双击 VS2017 的快捷方式&#xff0c;图标转了两圈&#xff0c;然后就没有然后了。要么直接闪退&#xff0c;要么弹个提示说找不到…

作者头像 李华