Docker 基础体系 · 第 28/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker Compose 开发工作流:Bind、Watch、Override、Profile 和调试
Docker Compose 的开发工作流,核心不是“把代码挂进容器”这一条命令,而是协调四类状态:
- 源代码状态:代码位于宿主机,还是被复制进镜像;
- 容器文件系统状态:容器内的路径由镜像层、Bind mount 或 Named volume 提供;
- Compose 模型状态:哪些服务、网络、卷和配置被当前命令纳入项目;
- 进程运行状态:容器是否运行、主进程是否存活、应用是否已经重新加载。
Bind mount 解决的是宿主机与容器之间的文件映射;Watch 解决的是文件变化后的同步、重建或重启;Override 解决的是多套 Compose 配置的组合;Profile 解决的是可选服务的启停;调试命令则用于观察这些状态是否按照预期变化。
下文以现代 Docker Engine、BuildKit 和 Compose Specification 为基础,并假定应用运行在 Linux 容器 中。Docker Desktop 在 macOS 或 Windows 上通常通过 Linux VM 提供容器运行环境,因此路径共享、文件事件和权限表现还会受到宿主机与 VM 边界影响。
一、先建立 Compose 的运行模型
1. Compose 文件描述的是模型,不是已经运行的容器
一个 Compose 项目通常由以下对象组成:
- service:容器的声明;
- image 或 build:容器根文件系统的来源;
- network:服务之间的通信域;
- volume:持久化或共享数据;
- config、secret:配置和敏感数据;
- profiles:服务是否进入当前模型;
- develop.watch:开发时如何响应文件变化。
执行:
docker compose up
时,Compose 会经历近似如下过程:
flowchart TD
A[读取 Compose 文件] --> B[合并多个配置]
B --> C[解析变量与路径]
C --> D[应用 Profile 选择]
D --> E[校验服务依赖与配置]
E --> F[构建或拉取镜像]
F --> G[创建网络、卷和容器]
G --> H[启动容器进程]
H --> I[观察日志、健康状态和退出状态]
这里有一个容易忽略的因果关系:
- 文件还没有合并完成时,不能讨论最终的
volumes; - Profile 尚未筛选时,不能判断某个服务是否存在于本次运行;
- 容器尚未创建时,
docker compose exec没有目标; - 容器已经创建但进程退出时,
exec同样不能使用。
因此调试 Compose 时,应先确认 最终模型,再确认 容器状态,最后确认 进程和网络状态。
2. 用 config 查看 Compose 的最终模型
假设目录如下:
demo/
├── compose.yaml
├── compose.override.yaml
├── Dockerfile
└── app.py
使用:
docker compose config
它会输出变量插值、多个文件合并、默认值展开后的配置。这个命令不会启动容器,适合验证:
- 文件是否被正确读取;
ports、volumes和environment的最终值;- Profile 是否使服务进入当前模型;
- 相对路径最终指向哪里;
- YAML 缩进和字段类型是否正确。
只想验证配置而不访问镜像或运行环境时,可以使用:
docker compose config --quiet
成功时通常没有输出,失败时返回非零退出码。
二、Bind mount:把宿主机路径映射到容器
1. Bind mount 的定义
Bind mount 是把宿主机上的一个具体文件或目录,挂载到容器文件系统中的某个目标路径:
services:
api:
volumes:
- type: bind
source: .
target: /app
也可以使用短语法:
services:
api:
volumes:
- .:/app
其中:
source是宿主机路径;target是容器内路径;ro或read_only: true可以把挂载设置为只读;bind的数据不由 Docker 创建和管理,而是直接依赖宿主机路径。
典型开发场景是:
宿主机 ./app.py
│
│ Bind mount
▼
容器 /app/app.py
│
▼
应用进程读取 /app/app.py
容器内读取到的不是镜像构建时复制进去的那份文件,而是当前宿主机路径上的文件。
2. Bind mount 会遮挡镜像原有内容
这是开发环境中最重要的文件系统规则之一。
假设 Dockerfile 为:
FROM python:3.12-slim
WORKDIR /app
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
构建镜像后,镜像中已经存在:
/app/app.py
如果运行时使用:
services:
api:
volumes:
- .:/app
那么容器启动后,/app 会被宿主机当前目录遮挡。镜像中的 /app/app.py 并没有被删除,但在这个容器挂载视图中不可见。
因此,如果宿主机目录中没有 app.py,容器内的 /app/app.py 也不能通过普通路径访问。
这也解释了下面的常见故障:
容器镜像构建成功
容器启动失败
错误:/app/app.py 不存在
实际原因可能不是 Dockerfile 的 COPY 失败,而是启动时的 Bind mount 覆盖了镜像中的 /app。
3. Bind mount 与 Named volume 的职责不同
下面两个挂载并不等价:
services:
api:
volumes:
- .:/app
- pycache:/app/__pycache__
volumes:
pycache:
第一项是 Bind mount,数据来源是宿主机当前目录;第二项是 Named volume,数据由 Docker 管理。
当多个挂载覆盖同一路径的不同层级时,较深路径的挂载可以独立提供内容。因此:
/app ← 宿主机目录
/app/__pycache__ ← Named volume
这可以避免 Python 缓存文件写回宿主机。
不过 Named volume 不会自动解决所有依赖问题。例如 Node.js 项目常见:
volumes:
- .:/app
- node_modules:/app/node_modules
原因是宿主机的 node_modules 可能:
- 尚未安装;
- 是 macOS 或 Windows 上生成的依赖;
- 包含与 Linux 容器不兼容的原生扩展。
把 /app/node_modules 单独放到容器侧 Named volume,可以保留代码的 Bind mount,同时让依赖在 Linux 容器中安装。
4. ro 不能阻止应用访问宿主机敏感内容
下面的配置使容器不能写宿主机目录:
services:
api:
volumes:
- type: bind
source: .
target: /app
read_only: true
但只读不等于安全隔离。容器仍然可以读取目录中的源代码、配置文件和其他可读内容。因此不能把宿主机的整个家目录随意挂载进容器,即使使用了 ro。
更安全的范围通常是:
services:
api:
volumes:
- type: bind
source: ./src
target: /app/src
read_only: true
5. Linux 下的权限、用户和文件事件
Bind mount 不会把宿主机文件的所有权“转换”为容器用户的所有权。Linux 上,文件的 UID 和 GID 数值直接参与权限判断。
例如容器进程以 UID 1000 运行:
FROM python:3.12-slim
RUN useradd --uid 1000 --create-home appuser
WORKDIR /app
USER appuser
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
如果宿主机的 ./data 属于一个容器用户没有写权限的 UID,下面的程序可能失败:
services:
api:
volumes:
- ./data:/app/data
错误可能表现为:
PermissionError: [Errno 13] Permission denied
诊断时分别查看两侧:
ls -ln data
docker compose exec api id
docker compose exec api ls -ln /app/data
ls -n 显示数字 UID/GID,避免容器内外用户名不同造成误判。
Linux 上还需要注意:
- 编辑器写文件时可能采用“写临时文件再 rename”的方式;
- 应用监听的是 inode 或文件事件,监听行为可能因此变化;
- 容器内的 inotify 监听限制可能影响大型项目;
- SELinux 启用时,Bind mount 可能还需要
:z或:Z标签选项,具体取决于共享范围和发行版策略。
z 和 Z 属于宿主机安全标签语义,不应在不了解 SELinux 策略时盲目复制到所有环境。
三、Watch:在文件变化后同步、重启或重建
1. Watch 与 Bind mount 不是同一层机制
Bind mount 让容器直接看到宿主机目录:
宿主机文件改变
│
▼
容器挂载视图立即反映变化
Compose Watch 则是一个由 Compose 观察宿主机文件,并执行动作的机制:
宿主机文件改变
│
▼
Compose Watch 规则匹配
│
├── sync:复制文件到运行中的容器
├── sync+restart:复制后重启容器
├── rebuild:重新构建镜像并更新容器
└── restart:重启容器
所以:
- Bind mount 改变的是容器文件系统的挂载关系;
- Watch 改变的是开发工作流中的更新动作;
- Watch 可以在不把整个源码目录暴露为挂载点的情况下,把指定文件同步到容器。
Compose Watch 使用 develop.watch 配置,现代 Compose 版本支持以下常用动作:
sync:把变化同步到容器目标路径;restart:重启服务容器;sync+restart:先同步,再重启;rebuild:触发镜像重建并更新服务。
具体动作和可用字段受 Compose 版本影响,应使用本机版本文档和:
docker compose version
进行确认。
2. 一个可运行的 Watch 示例
目录:
watch-demo/
├── compose.yaml
├── Dockerfile
└── app.py
app.py:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"hello from compose watch\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
print(format % args)
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
compose.yaml:
services:
api:
build:
context: .
ports:
- "8000:8000"
develop:
watch:
- action: sync+restart
path: ./app.py
target: /app/app.py
- action: rebuild
path: ./Dockerfile
启动 Watch:
docker compose up --watch
或者使用:
docker compose watch
不同 Compose 版本对这两个命令的交互方式可能略有差异,但它们都围绕 develop.watch 规则工作。首次执行时会先构建镜像并启动服务。
验证:
curl http://localhost:8000
预期输出:
hello from compose watch
然后修改 app.py 中的响应内容。Compose 会执行:
- 检测到
./app.py改变; - 将该文件同步到容器的
/app/app.py; - 重启
api容器; - 新进程读取修改后的文件;
- 端口映射继续由更新后的容器提供服务。
修改 Dockerfile 时,匹配的是 rebuild 规则:
- 重新执行镜像构建;
- 创建基于新镜像的容器;
- 替换旧容器;
- 重新建立端口、网络和其他 Compose 声明的资源关系。
3. sync 不一定会使程序重新加载代码
如果改为:
- action: sync
path: ./app.py
target: /app/app.py
文件会更新,但正在运行的 Python 进程已经加载了旧代码。除非应用本身具备热重载机制,否则 HTTP 响应可能仍然来自旧版本。
因此动作的选择必须与进程生命周期匹配:
| 文件类型 | 常见动作 | 原因 |
|---|---|---|
| 模板、静态文件 | sync |
应用可能每次请求重新读取 |
| 支持热重载的源码 | sync |
由应用开发服务器负责重载 |
| 不支持热重载的源码 | sync+restart |
同步后需要重新启动进程 |
| 依赖清单 | rebuild |
依赖通常在镜像构建阶段安装 |
| Dockerfile、系统包配置 | rebuild |
必须生成新镜像层 |
例如:
develop:
watch:
- action: sync
path: ./templates
target: /app/templates
- action: sync+restart
path: ./src
target: /app/src
- action: rebuild
path: requirements.txt
rebuild 规则不需要像 sync 那样提供有效的容器目标路径,因为其目的不是把文件复制到正在运行的容器,而是以变化后的构建上下文重新构建镜像。
4. Watch 的路径边界
Watch 中的 path 通常是相对于 Compose 项目目录的路径,目标路径必须是容器内路径。一个常见错误是把宿主机绝对路径写成目标:
# 错误思路
target: /home/user/project/src
target 应该是容器内应用实际使用的路径,例如:
target: /app/src
还可以排除不应同步的目录:
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- __pycache__/
- "*.pyc"
排除目录很重要,因为构建产物、依赖目录和缓存目录可能引发大量无意义事件,甚至把容器生成的文件反向污染开发源目录。
5. Watch 的边界与故障表现
Watch 不是通用的文件系统同步系统。以下情况要单独处理:
- 容器必须处于可管理的运行状态,服务已经退出时无法正常同步;
- 目标路径必须存在或能被 Compose/容器正确创建;
- 应用若在启动时只读取一次配置,单纯
sync不会刷新内存状态; - 依赖的原生模块、系统包和编译工具通常必须通过
rebuild更新; - 大量文件变化可能触发连续重建或重启;
- Docker Desktop 下文件事件经过 VM 边界,性能和事件语义可能与原生 Linux 不同。
Watch 与 Bind mount 也不应同时管理同一个目标路径。例如:
services:
api:
volumes:
- .:/app
develop:
watch:
- action: sync
path: ./src
target: /app/src
此时 /app/src 同时处于 Bind mount 的可见范围和 Watch 的同步目标内。即使配置可以被解析,实际更新路径也会变得难以推断,通常应选择一种机制,而不是叠加两种机制。
四、Override:合并基础配置与开发配置
1. Override 的作用
Compose 支持通过多个文件描述同一个项目。默认情况下,Compose 会读取:
compose.yaml
compose.override.yaml
并把后者作为覆盖文件合并到前者。
常见分层方式是:
compose.yaml # 基础配置
compose.override.yaml # 本地开发默认配置
compose.prod.yaml # 生产或预发布差异
compose.debug.yaml # 调试配置
显式指定文件:
docker compose \
-f compose.yaml \
-f compose.debug.yaml \
up
文件顺序有因果意义:后面的文件覆盖或补充前面的文件。
也可以通过环境变量指定:
export COMPOSE_FILE=compose.yaml:compose.debug.yaml
docker compose config
Linux 使用冒号分隔路径;Windows 的路径分隔规则不同,应由 Compose 环境处理或使用对应平台格式。
2. 一个基础文件和开发 Override
基础文件 compose.yaml:
services:
api:
build:
context: .
command: ["python", "/app/app.py"]
ports:
- "8000:8000"
environment:
APP_ENV: production
开发覆盖文件 compose.override.yaml:
services:
api:
environment:
APP_ENV: development
volumes:
- type: bind
source: .
target: /app
- type: volume
source: pycache
target: /app/__pycache__
volumes:
pycache:
执行:
docker compose config
最终的 api 同时具有:
- 基础文件中的
build、command和ports; - Override 中的开发环境变量;
- Override 中的 Bind mount 和 Named volume。
执行:
docker compose up
会自动读取 compose.override.yaml。如果只想使用基础文件:
docker compose -f compose.yaml up
这两个命令的 Compose 模型不同,后者不会自动加入开发覆盖文件。
3. 合并不是简单的文本覆盖
Compose 合并发生在解析后的 YAML 结构上,而不是把第二个文件的文本粘贴到第一个文件末尾。
大体上:
- 标量字段通常由后一个文件替换,例如
command、image; - 映射字段通常按键合并,例如
environment、labels; - 某些序列字段按 Compose 规则合并或去重;
volumes和devices这类字段按容器目标路径具有特殊合并语义;ports具有端口元组语义,不应简单按普通字符串列表理解。
例如:
# compose.yaml
services:
api:
environment:
LOG_LEVEL: info
APP_ENV: production
command: ["python", "app.py"]
# compose.override.yaml
services:
api:
environment:
LOG_LEVEL: debug
command: ["python", "-u", "app.py"]
结果通常是:
environment:
LOG_LEVEL: debug
APP_ENV: production
command:
- python
- -u
- app.py
LOG_LEVEL 被替换,APP_ENV 保留,而 command 整体被替换。
4. 路径相对于第一个 Compose 文件解析
使用多个文件时,路径解析容易出错:
docker compose \
-f deploy/compose.yaml \
-f dev/compose.override.yaml \
config
相对路径通常以第一个 Compose 文件所在的项目目录为基准,而不是分别以每个覆盖文件所在目录为基准。这意味着:
volumes:
- ./src:/app/src
很可能指向:
deploy/../src
而不是 dev/src。
正确做法是先使用 docker compose config 检查最终路径,并尽量把相关 Compose 文件放在同一个项目根目录附近,减少跨目录合并。
5. 删除基础配置中的字段
普通覆盖适合“替换或追加”,不适合表达“删除基础文件中的值”。现代 Compose 支持用于控制合并行为的特殊 YAML 标签,例如:
services:
api:
ports: !reset []
这表示清空基础文件中的端口列表。还存在更强制的覆盖语义:
services:
api: !override
image: my-debug-image
command: ["python", "-m", "pdb", "app.py"]
这些标签属于 Compose 的扩展合并语义,要求使用支持它们的 Compose 版本。若团队环境版本不一致,先执行:
docker compose version
并在 CI 中固定或验证 Compose 版本,否则同一组文件可能在不同机器上表现不同。
6. Override 不等于继承 Dockerfile
Compose Override 只合并 Compose 模型,不会自动继承 Dockerfile 中的开发命令、依赖或工具。
例如,下面的 Override:
services:
api:
command: ["bash"]
只改变容器启动命令。如果镜像中没有 Bash,容器仍会启动失败:
exec: "bash": executable file not found
开发配置需要同时考虑:
- 镜像是否包含调试器和 Shell;
command的可执行文件是否存在;- 挂载是否遮挡了构建阶段复制的文件;
- 非 root 用户是否有权限访问挂载目录。
五、Profile:让可选服务进入或离开当前模型
1. Profile 的定义
Profile 是 Compose 服务级别的可选激活条件:
services:
api:
image: example/api
adminer:
image: adminer
profiles:
- tools
没有指定 Profile 时:
docker compose up
通常只启动没有 profiles 字段的 api,adminer 不会进入当前默认服务集合。
启用 Profile:
docker compose --profile tools up
也可以使用环境变量:
COMPOSE_PROFILES=tools docker compose up
一次启用多个 Profile:
docker compose --profile tools --profile debug up
或者:
COMPOSE_PROFILES=tools,debug docker compose up
没有 profiles 字段的服务属于始终启用的服务;设置了 profiles 的服务只有在对应 Profile 激活时才可被默认启动。
2. Profile 是模型选择,不只是“容器暂停”
下面的服务:
services:
mailhog:
image: mailhog/mailhog
profiles: ["debug"]
在 debug 未激活时,不只是容器没有运行,而是它通常不参与这次 Compose 操作的服务集合。因此其他服务不能把它当作始终存在的依赖端点。
例如:
services:
api:
image: example/api
depends_on:
- mailhog
mailhog:
image: mailhog/mailhog
profiles: ["debug"]
如果 api 默认启动而 mailhog 未激活,这个依赖关系就可能造成模型无效或启动失败。正确的组织方式通常是让依赖方也属于同一个 Profile:
services:
api:
image: example/api
profiles: ["debug"]
depends_on:
- mailhog
mailhog:
image: mailhog/mailhog
profiles: ["debug"]
或者让 api 在所有环境都存在,但通过环境变量决定是否连接调试服务,而不是声明一个在默认模型中不存在的硬依赖。
3. 直接指定服务可能临时激活其 Profile
执行:
docker compose run --rm adminer
或:
docker compose up adminer
是在直接请求一个具体服务,而不是请求默认服务集合。Compose 对显式指定服务有特殊处理:目标服务的 Profile 可以被视为被直接选择,但它的依赖服务仍必须满足依赖关系和 Profile 约束。
因此不能把“直接指定服务”理解成“自动让所有相关 Profile 都永久启用”。要观察最终行为,使用:
docker compose --profile debug config --services
docker compose --profile debug ps
前者显示启用后的服务集合,后者显示已创建或运行的容器。
4. Profile 的典型用途
调试依赖服务:
services:
api:
build: .
ports:
- "8000:8000"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
adminer:
image: adminer
profiles: ["tools"]
ports:
- "8080:8080"
mailhog:
image: mailhog/mailhog
profiles: ["debug"]
ports:
- "8025:8025"
启动 API 和数据库,但不启动工具:
docker compose up
启动数据库、API 和 Adminer:
docker compose --profile tools up
启动所有已定义 Profile:
docker compose --profile "*" up
"*" 的含义是启用所有 Profile,而不是匹配服务名。
六、一个组合示例:基础配置、开发覆盖、Watch 和调试 Profile
下面的示例将几种机制组合起来。
1. 基础 Compose 文件
services:
api:
build:
context: .
command: ["python", "/app/app.py"]
ports:
- "8000:8000"
environment:
APP_ENV: production
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000')"]
interval: 5s
timeout: 2s
retries: 5
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
adminer:
image: adminer
profiles: ["tools"]
ports:
- "8080:8080"
volumes:
db-data:
这里:
api和db默认属于项目;adminer只有在toolsProfile 激活时才加入;db-data保存 PostgreSQL 数据;healthcheck检查的是容器内的 HTTP 服务,而不是宿主机端口。
2. 开发 Override
services:
api:
environment:
APP_ENV: development
develop:
watch:
- action: sync+restart
path: ./app.py
target: /app/app.py
- action: rebuild
path: Dockerfile
db:
environment:
POSTGRES_PASSWORD: dev-password
默认执行:
docker compose up --watch
会读取:
compose.yaml
compose.override.yaml
并让 api 使用开发环境变量和 Watch 规则。
启用数据库管理工具:
docker compose --profile tools up --watch
如果显式使用 -f,仍需把两个文件都列出:
docker compose \
-f compose.yaml \
-f compose.override.yaml \
--profile tools \
up --watch
3. 配置验证顺序
先验证默认模型:
docker compose config --services
预期至少包含:
api
db
再验证工具 Profile:
docker compose --profile tools config --services
预期增加:
adminer
随后检查具体服务:
docker compose ps
若容器尚未创建,可能没有输出运行中的服务;这不表示配置为空,只表示尚未执行 up 或服务已经被删除。
七、调试 Compose:从模型到进程的逐层定位
1. 第一层:确认命令实际使用了哪些文件
docker compose config
重点检查:
- 是否自动读取了
compose.override.yaml; - 是否错误地使用了
-f导致默认 Override 被绕过; volumes的宿主机路径是否正确;command是否被覆盖;- 环境变量是否完成插值;
- Profile 服务是否出现在模型中。
例如:
docker compose --profile tools config --services
如果 adminer 不在输出中,问题发生在 Profile 选择阶段,还没有必要去看容器日志。
2. 第二层:确认容器状态和退出原因
docker compose ps
查看所有状态:
docker compose ps -a
关注:
Up:容器存在且主进程仍在运行;Exited (0):进程正常退出,但服务可能不是长驻进程;Exited (1)或其他非零状态:应用启动或运行失败;Restarting:重启策略可能形成循环;health: starting或unhealthy:容器进程仍在,但健康检查未通过。
日志:
docker compose logs api
docker compose logs -f api
docker compose logs --tail=100 api
logs 读取的是容器标准输出和标准错误。如果应用把错误写入容器内文件,Compose 日志不会自动显示该文件内容。
3. 第三层:进入正在运行的容器
docker compose exec api sh
exec 的前提是目标容器正在运行,并且容器内存在 sh。如果镜像是极简镜像,可能没有 Shell:
exec: "sh": executable file not found
此时可以执行已有程序:
docker compose exec api python -c "import os; print(os.getcwd())"
查看用户和文件:
docker compose exec api id
docker compose exec api pwd
docker compose exec api ls -la /app
docker compose exec api env | sort
如果容器没有运行,使用:
docker compose run --rm api python -c "import sys; print(sys.version)"
run 会为服务创建一个一次性容器,其行为不同于 exec:
exec进入现有容器,使用现有进程空间和挂载;run创建新的服务容器,适合执行迁移、诊断脚本或一次性命令;run默认不会发布服务的端口,若需要端口映射通常要加--service-ports;run可能使用不同的容器名称和生命周期,不能把它的结果完全等同于常驻服务。
4. 第四层:检查挂载是否遮挡了文件
docker inspect "$(docker compose ps -q api)" \
--format '{{json .Mounts}}'
输出中应能看到类似:
[
{
"Type": "bind",
"Source": "/path/to/project",
"Destination": "/app"
}
]
若容器中的文件与镜像预期不一致,应优先判断:
/app是否被 Bind mount 覆盖;- 宿主机源目录中是否真的有该文件;
- 是否由子路径 Named volume 覆盖;
- 运行的是不是旧容器;
- Compose 文件是否使用了错误的 Override。
必要时重新创建:
docker compose up -d --force-recreate api
如果问题来自旧镜像:
docker compose up -d --build --force-recreate api
5. 第五层:检查网络和端口
查看端口映射:
docker compose port api 8000
查看网络:
docker network ls
docker compose ps
docker network inspect <project>_default
容器之间访问服务时,不应使用宿主机发布端口或 localhost:
api 容器访问 db:
错误:localhost:5432
正确:db:5432
原因是容器内的 localhost 指向当前容器自身。Compose 默认网络会为服务提供基于服务名的 DNS,因此 db 解析到数据库容器。
从 api 容器测试数据库名称解析:
docker compose exec api getent hosts db
如果镜像中没有 getent,可以使用应用语言或数据库客户端验证。网络连通不等于应用可用:端口可达、认证成功、数据库已初始化是三个不同条件。
6. 第六层:区分容器存活和应用健康
健康检查示例:
healthcheck:
test:
[
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000')"
]
interval: 5s
timeout: 2s
retries: 5
这里的检查在容器内部执行:
容器内 Python
│
└── 请求 127.0.0.1:8000
它不验证宿主机是否能通过 localhost:8000 访问,也不验证外部负载均衡器是否能访问。
查看健康状态:
docker inspect "$(docker compose ps -q api)" \
--format '{{json .State.Health}}'
健康检查失败但容器仍然 Up,说明:
- 主进程没有退出;
- 应用没有满足健康条件。
这与容器直接 Exited 是两条不同的故障路径。
八、常见失败模式及其恢复方法
1. 修改文件后容器没有变化
按以下顺序检查:
docker compose config
docker compose ps
docker compose logs api
然后判断使用的是哪种机制:
- Bind mount:检查宿主机路径是否挂载到正确目标;
- Watch:检查是否通过
up --watch或watch运行; sync:检查应用是否具备热重载;sync+restart:检查容器是否发生重启;rebuild:检查构建上下文和 Dockerfile 是否真的包含变化文件。
如果使用 Bind mount,却把文件复制到了镜像中后又用挂载遮挡,容器看到的将是宿主机版本,不是镜像版本。
2. 修改依赖文件后依赖仍然是旧版本
例如:
develop:
watch:
- action: sync
path: requirements.txt
target: /app/requirements.txt
这只会更新文件,不会执行 pip install。更合适的是:
develop:
watch:
- action: rebuild
path: requirements.txt
并在 Dockerfile 中安装依赖:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
推导过程是:
- 依赖安装发生在构建阶段;
sync只改变运行中容器的文件;- 已安装的软件包不会因为清单文件改变而自动变化;
- 因此依赖清单应触发
rebuild,或由应用启动脚本显式安装。
3. docker compose exec 失败
错误:
service "api" is not running
说明容器不存在或主进程已退出。执行:
docker compose ps -a
docker compose logs api
错误:
No such container
可能是:
- 当前目录不是原来的 Compose 项目目录;
COMPOSE_PROJECT_NAME改变;- 使用了不同的
-f文件组合; - 容器已经被
down删除。
错误:
executable file not found
说明容器内不存在所请求的程序。不要假设所有镜像都带有 bash、curl 或 ps。
4. 端口绑定失败
错误:
Bind for 0.0.0.0:8000 failed: port is already allocated
含义是宿主机端口 8000 已被占用,而不是容器内应用一定有问题。检查:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Linux 上也可以检查:
ss -ltnp | grep ':8000'
开发 Override 中可以换宿主机端口:
ports:
- "18000:8000"
此时:
- 宿主机访问
localhost:18000; - 容器内服务仍监听
8000; - Compose 服务间通信仍使用容器端口和服务名。
5. 数据库看似“丢失”
执行:
docker compose down
默认会删除由该 Compose 项目创建的容器和网络,但通常不会删除 Named volume。若执行:
docker compose down -v
则会删除声明关联的 Named volume,数据库数据可能随之丢失。
恢复策略取决于数据来源:
- Bind mount:检查宿主机目录是否仍在;
- Named volume:确认是否执行过
down -v; - 临时容器可写层:容器删除后数据通常消失。
查看卷:
docker volume ls
docker volume inspect <project>_db-data
开发环境清理前,应明确区分“删除容器”与“删除数据卷”。
九、Bind 与 Watch 的选择逻辑
可以用下面的条件判断:
选择 Bind mount,当:
- 应用需要直接读取大量源码;
- IDE、调试器或语言服务器需要看到容器内状态;
- 你接受宿主机和容器共享文件系统视图;
- 需要频繁访问生成文件或交互式修改文件。
代价是:
- 容器内外权限耦合;
- 宿主机生成的依赖可能与 Linux 容器不兼容;
- 大型目录的文件事件和 I/O 可能较慢;
- 挂载会遮挡镜像内容。
选择 Watch,当:
- 希望只同步源码或特定目录;
- 希望依赖和系统工具留在镜像中;
- 希望代码变化触发可控的同步、重启或重建;
- 不希望整个项目目录成为容器挂载点。
代价是:
- 需要较新的 Compose 版本;
- 必须为不同文件类型选择正确动作;
- 应用是否热重载仍由应用进程决定;
- 复杂项目需要认真设计忽略规则和重建边界。
二者的关键差异可以形式化为:
Bind:读取路径 = 宿主机挂载视图
Watch:读取路径 = 镜像文件系统 + Compose 复制结果
如果目标是“容器始终直接看到宿主机目录”,Bind 更直接;如果目标是“宿主机变化触发一套明确的更新动作”,Watch 更容易表达生命周期。
十、开发配置与生产边界
开发配置往往包含:
volumes:
- .:/app
或:
develop:
watch:
- action: sync+restart
这些配置适合快速迭代,但不应默认带入生产,原因包括:
- 生产容器的代码应来自已审计、可追踪的镜像;
- Bind mount 会让容器内容依赖宿主机目录;
- Watch 依赖开发机文件事件,不是生产部署控制器;
- 开发 Override 可能开放调试端口或挂载敏感源码;
profiles只是 Compose 模型选择,不是生产权限边界;- Compose 的重建和替换不等价于完整的发布、回滚和流量治理。
可以把生产命令明确写成:
docker compose -f compose.yaml -f compose.prod.yaml up -d
而不是依赖当前目录下是否存在自动读取的 compose.override.yaml。这样可以减少开发配置被意外合并的可能性。
同时,生产配置应显式验证:
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
config --quiet
再执行:
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
up -d
开发工作流追求短反馈周期;生产工作流追求可复现、可审计和可恢复。Bind、Watch、Override 和 Profile 都是 Compose 的配置与开发能力,不能单独替代镜像发布、密钥管理、备份、滚动更新或故障回滚机制。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Dev Containers 开发环境:配置、Feature、缓存、Secret 和复现
- 下一篇:Docker 容器化测试:一次性依赖、健康等待、Fixture 和资源清理
- 延伸:Docker Compose 完整指南:服务、网络、卷、依赖、Profile 和生产边界
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论