Docker Compose 中 build 和 image 在 VPS 上的区别
了解 image 如何拉取已发布标签、build 如何在 VPS 本地构建,以及为什么修改 Dockerfile 后执行 docker compose up 仍使用旧镜像。
Docker Compose 中 build 与 image 的区别:简要说明
在 Docker Compose 文件中,image: 指定要从镜像仓库拉取的镜像,build: 告诉 Compose 在本机根据 Dockerfile 构建镜像。只设置 image: 时,Compose 会拉取该标签对应的镜像并运行。只设置 build: 时,Compose 会在本机构建镜像,并使用项目名和服务名生成镜像名称。两者同时设置时,Compose 会在本机构建镜像,然后使用 image: 中的名称为结果添加标签。这种方式可用于构建镜像,并以自定义名称推送。
这就是两者的全部区别。以下内容说明它们在服务器上的实际行为。本文假设 Docker Engine 和 Compose 插件已经安装;在 VPS 上运行 Docker介绍了相关安装过程。
三种完整形式
拉取已发布的标签并运行。整个过程中不会使用 Dockerfile。
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"使用当前目录中的 Dockerfile 构建。除 FROM 中指定的基础镜像外,不会拉取其他内容。
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"在本地构建并为结果设置标签。随后,docker compose push 可以将这个确切的标签发送到镜像仓库。
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context 是发送给构建器的目录。dockerfile 会相对于该上下文解析,因此 context: . 配合 dockerfile: docker/prod.Dockerfile 使用是正常且正确的。运行 docker compose images,查看每个服务容器背后的镜像名称和镜像 ID。这是确认你实际写的是上述三种形式中的哪一种的最快方法。
为什么修改 Dockerfile 后运行 docker compose up 不会重新构建?
因为 up 只检查镜像是否存在,不检查镜像是否为最新版本。
当 Compose 启动包含 build: 部分的服务时,它会在本地镜像存储中查找该镜像。如果本地已经存在同名镜像,Compose 就会使用它。它不会读取 Dockerfile,不会比较源文件,也不会检查任何时间戳。Compose 规范将此规则定义为 pull_policy 属性;默认行为是仅在镜像不存在时构建镜像。只要镜像存在,Compose 就认为条件已满足。
因此,您修改 app.py,运行 docker compose up -d,看到 Compose 报告容器正在运行,但实际提供的仍是旧代码。整个过程没有失败,因此也不会显示警告。这是 Compose 中最常见的“修改未生效”问题。判断依据是 Compose 在容器名称旁显示的状态词:如果 Compose 重新创建了容器,状态会显示为 recreated 或 started;如果 Compose 决定不处理容器,状态会显示为 running。
执行两项检查即可确认原因。docker compose images 会输出每个容器使用的镜像 ID,因此应在部署前记录该 ID,并在部署后进行比较。docker image ls 包含 CREATED 列。如果镜像的创建时间早于最近一次提交,那么无论部署脚本输出了什么,该镜像都已过时。
哪些选项会强制重新构建
docker compose up -d --build会先构建镜像,然后重新创建镜像发生变化的容器。这是大多数人需要的选项。docker compose build web只构建一个服务,不启动任何服务。随后执行docker compose up --no-deps -d web,即可只替换该容器,同时让堆栈中的其他容器继续运行。docker compose build --no-cache web会丢弃所有缓存层,并从第一条指令开始重新构建。docker compose build --pull会尝试拉取FROM中基础镜像的较新版本。因此,像node:22这样的可变标签会使用当前内容,而不是您在 March 下载的副本。docker compose up -d --force-recreate会根据容器当前使用的镜像重新创建容器。它不会执行构建。在本应使用--build时误用此选项,是常见的排查死路。
您也可以将这一决定写入文件。按照 Compose 规范的定义,pull_policy: build 表示由 Compose 构建镜像;如果镜像已经存在,Compose 还会重新构建它。这样,每次执行 up 都会进行构建。此行为适合笔记本电脑,但在服务器上通常并不需要。
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: build还需要了解一个交互行为。docker compose pull 也会尝试拉取包含 build 部分的服务所使用的镜像。如果拉取失败,它会提示必须改为构建镜像。传入 --ignore-buildable 可静默跳过这些服务。
构建缓存如何决定部署耗时
Dockerfile 中的每条指令都会生成一个层。当指令及其输入未发生变化时,构建器会复用缓存层。对于 COPY,输入是要复制的文件内容。一旦某个层未命中缓存,后续所有层都会重新构建,因为每个层都基于前一层生成的文件系统构建。
这一条规则决定了部署耗时是几秒还是几分钟。应按变更频率从低到高排列 Dockerfile:先放很少变化的内容,再放每次提交都会变化的内容。
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci 位于 COPY . . 之上,因此编辑源文件后,安装层仍可使用缓存,构建会从复制步骤继续。交换这两行后,即使只改动一个字符,也会重新安装所有依赖,因为 COPY . . 会使 npm ci 所依赖的层失效。同样的结构也适用于 pip install -r requirements.txt 和 go mod download。
当您怀疑旧缓存层掩盖了修复结果时,--no-cache 是正确的工具。但不应默认使用它,因为它会丢弃通过合理排列 Dockerfile 所获得的缓存复用。
有一项设置可以由镜像和 Compose 覆盖:Dockerfile 中的 CMD 指定镜像默认运行的内容,而服务中的 command: 键会替换它。命令与 entrypoint 的交互方式 在这里很重要,因为 Compose 覆盖设置可能导致刚构建的镜像表现得与旧镜像完全相同。
构建上下文和 .dockerignore
context: .表示 Compose 会在执行第一条指令前,将该目录打包并发送给构建器。目录中的所有内容都会被发送,包括 .git,以及您恰好放在源代码旁边的任何数据目录。如果项目没有其他变化,但构建在传输上下文这一步停顿,说明构建上下文过大。
在上下文根目录中创建 .dockerignore 文件,可以将路径排除在传输范围之外。其语法接近 .gitignore。
.git
node_modules
*.log
data/
.env这样做有两个好处。传输内容变少,因此每次构建都会更快。此外,COPY . . 无法再将 .env 复制到镜像中,从而避免任何拉取该镜像的人读取其中内容。
会随着时间推移变慢的典型情况是绑定挂载。命名卷位于项目目录之外,但像 ./data:/var/lib/postgresql/data 这样的绑定挂载位于构建上下文内。因此,数据库不断增长时,构建也会逐周变慢。在 .dockerignore 中添加一行即可解决。绑定挂载与命名卷的比较介绍了更广泛的权衡。
构建参数也存在类似但规模更小的风险。通过 args: 传入的值会显示在镜像历史中,任何持有该镜像的人都能读取。因此,应在其中放置版本号,绝不要放置令牌。Compose 中的环境文件和密钥介绍了凭据应放置的位置。
应在 VPS 上构建,还是在其他位置构建后拉取?
在承载网络流量的服务器上构建是默认做法,因为路径最短:git pull,然后 docker compose up -d --build。对于尚未被其他人依赖的小型服务器,这通常没有问题。但有两个可以测量的原因,以及一个只会在糟糕情况下暴露的问题,会让这种做法不再合适。
内存。 构建会在运行中的应用旁边启动编译器和打包器,而它们通常是整个技术栈中最耗内存的部分。在 1 GB VPS 上,JavaScript 打包器或 Rust 编译过程通常会成为服务器上占用内存最大的进程。当内核内存耗尽时,它会终止占用内存最多的进程:构建可能因 Killed 停止并返回退出状态 137,也可能是数据库被终止,导致网站在部署过程中中断。dmesg -T | grep -i oom 会输出包含进程名称的终止记录,因此您可以确认到底是哪一个进程被终止,而不必猜测。
磁盘。 每次构建都会留下镜像层,构建器还会将自己的缓存与镜像分开保存。docker system df 会显示这两部分,其中构建缓存一栏只会不断增长。使用 docker image prune 回收悬空镜像,使用 docker builder prune 回收缓存层。磁盘空间耗尽造成的影响不止是构建失败。数据库也会停止写入,而这种故障的代价远高于部署变慢。
可复现性。 在服务器上构建的镜像只存在于该服务器上。回滚时,您必须检出旧提交并重新构建,而且无法保证这次构建会生成之前的结果,因为基础镜像标签和软件包镜像都可能已经变化。在其他位置构建并推送标签后,回滚只需修改一处配置:将 image: 指向之前的标签,然后运行 docker compose up -d。
可靠的方案很简单。持续集成系统负责构建并推送 registry.example.com/acme/web:<git-sha>,VPS 上的 Compose 文件只保留 image:,完全不包含 build: 键。这样部署只需执行两个命令,几乎不占用内存。
docker compose pull
docker compose up -d在服务器上运行一次 docker login registry.example.com,之后 Compose 就可以拉取私有标签。
开发环境仍应保留构建部分,不要删除。将它放在一个由您自行命名的文件中。
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --build将该文件命名为 compose.dev.yaml,不要命名为 compose.override.yaml。只要覆盖文件存在,Compose 就会自动加载它,因此如果服务器上残留了覆盖文件,就会悄悄重新在服务器上构建。分层使用多个 Compose 文件介绍了合并过程如何解析各个键。
在其他环境构建时容易忽略的架构问题
镜像包含其构建时使用的 CPU 架构。在 Apple Silicon 笔记本上构建并推送镜像,然后将该标签拉取到 x86_64 VPS,Docker 会警告请求的镜像平台与检测到的主机平台不匹配。随后进程会因 exec format error 退出。这个错误看起来像二进制文件损坏,但实际并不是。请明确指定目标架构进行构建:
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .如果您的笔记本使用 x86,而您运行的是ARM VPS,而不是 x86 VPS,也会发生相反的架构不匹配。让 CI 在您部署所用的架构上构建镜像,即可避免这个问题。
部署后需要检查的内容
docker compose images会打印每个运行中容器所使用的镜像及标签。镜像 ID 发生变化,说明新构建已投入运行。docker compose config会在变量替换后打印合并后的文件。这样可以在运行任何命令前确认 Compose 将使用的最终镜像名称。- 在切换后的最初 30 秒内执行
docker compose logs -f web。启动后立即退出的容器会反复重启,而不是真正保持运行;如果不主动查看,这种循环通常不会显示出来。 docker image ls会显示 CREATED 列。如果镜像早于最近一次提交,则说明它从未重新构建。
如果您仍在组装这些检查所使用的文件,VPS 上 Compose 文件的基础知识介绍了相关配置键,Compose 命令速查表列出了其余子命令。
FAQ
我可以在同一个服务中同时使用 build 和 image 吗?
可以。这是自行构建项目的常见配置。Compose 根据 build: 部分构建镜像,并使用 image: 的值为结果添加标签。docker compose push 将镜像推送到镜像仓库时使用该标签,其他机器也会根据该标签拉取镜像。不设置 image: 键时,Compose 仍会构建镜像,但会根据项目名和服务名命名镜像,并警告由于缺少该属性,无法推送镜像。
为什么 Dockerfile 修改后,docker compose up 没有应用更改?
因为 up 只检查是否存在具有该名称的镜像。如果镜像已存在,Compose 就会直接启动它,不会将其与 Dockerfile 或源文件进行比较。请运行 docker compose up -d --build;如果只需替换一个服务,也可以依次运行 docker compose build web 和 docker compose up --no-deps -d web。在服务上设置 pull_policy: build 后,每次 up 都会重新构建,适合开发环境。
--build 和 --force-recreate 有什么区别?
--build 会重新构建镜像,然后重新创建镜像已发生变化的容器。--force-recreate 会使用容器已有的镜像重新创建容器,因此无法应用代码更改。如果更改位于源文件或 Dockerfile 中,应使用 --build 标志。--force-recreate 用于重置容器本身,例如在保留相同镜像的同时清除容器的可写层。
应该在 VPS 上构建 Docker 镜像,还是在其他位置构建?
应在其他位置构建,然后在服务器同时承载网络流量后拉取标签对应的镜像。构建过程会与应用争用内存。对于小型 VPS,内存不足时,内核可能会终止占用内存最多的进程,这个进程可能是构建任务,也可能是数据库。构建还会在磁盘上留下缓存,而这些缓存不会自动清理。
对于没有用户的小型项目,直接在服务器上构建没有问题。如果将 build: 部分保留在仅用于开发的 Compose 文件中,之后迁移的成本也很低。
如何防止 Docker 构建缓存占满磁盘?
运行 docker system df,查看镜像和构建缓存分别占用多少空间。docker builder prune 会删除缓存层,docker image prune 会删除之前构建遗留的悬空镜像。在任一命令中添加 -a 会执行更激进的清理,并强制下一次构建从零开始。不要在服务器上计划执行 docker system prune -af --volumes,因为 --volumes 会删除当前没有任何容器使用的卷。停止进行维护的堆栈会将数据库保存在这类卷中。