docker compose up 深度解析:从配置对齐到生产级容器编排

docker compose up容器编排配置对齐 本内容遵循CC 4.0 BY-SA版权协议
白追追 白追追 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

日常工作里如果只能留一个和容器相关的命令,我一定会选 up——就是 docker compose up 里的那个 up。它看上去简单得像个开关,按键一下就完事,但在真实项目里每一次执行 up,背后都是一次从编排文件到业务可用的完整调度。很多刚接触容器的朋友对这命令的理解停留在“把容器跑起来”,等到生产环境里容器起不来、配置不生效、卷权限报错,才开始回头研究它到底做了什么。

这篇就把 up 讲透。包括它和 docker run 的差别、执行时的完整流程、常见场景下的正确用法、以及我踩过的一些坑。不管是刚入行的,还是已经在用 Compose 维护服务的人,看下来至少能少走几段弯路。

1. 为什么说 up 是容器编排的“总开关”

1.1 up 不是 start,而是一次“期望状态对齐”

很多人容易把 up 理解成容器的 start,这是最容易出错的地方。start 是对某个已经存在的容器对象做一次启动操作,而 up 是让环境的实际状态和你写的 Compose 配置保持一致。

我用一个生活化的例子解释。你要布置一间临时展厅,手里有一份展品清单。start 相当于“把厅门打开,让里面现有的展品亮了灯”,如果清单上写了 3 件展品,现场却只有 2 件,start 不会管这些。up 会:它先看清单,再数现场,缺了展品就补、多出来的展品如果不在清单里就按规则提示或清理,展品说明改了也要替换成新版本。也就是说,它做的是“对齐”。

对应到容器世界,这段对话是这样的:

  • 解析 docker-compose.yml,确定期望状态;
  • 检查当前项目下面有没有对应的网络、卷、容器;
  • 不存在的就创建,配置有变化的就重建,没变化的继续用;
  • 最后把整个集群拉起来。

所以当你改了 Compose 文件里某个服务的镜像版本,直接执行 docker compose up -d 就够了,它会自动识别差异并重建受影响的服务,不需要先手动 stop 再 remove 再 create。这是它最值钱的设计。

1.2 为什么推荐 compose up 而不是 docker run

单容器快速验证场景下,docker run 确实很顺手:

BASH
docker run -d --name nginx -p 8080:80 nginx:alpine

但这个命令的一切状态都在命令行参数里。项目一复杂,环境变量十几个、端口三四组、依赖关系一堆,命令行就会变成一坨没人愿意维护的“玄学命令”。这时候 Compose 的价值就出来了。

  • 配置可读:写了什么端口、依赖哪个服务、挂载哪些目录,一眼就能在 docker-compose.yml 里看到;
  • 可重复:同一份配置在不同机器上跑出来的结果基本一致;
  • 一致性:up 会把整个服务组当作一个项目统一管理,网络、卷、容器名都有固定命名规则。

另外注意,现在官方推荐的是新版子命令 docker compose,老项目的 docker-compose(带横杠)是旧版 Python 实现,虽然还能用,但功能迭代已经落后。新环境一律用 docker compose 就好。判断方法很简单:看插件的安装路径 /usr/libexec/docker/cli-plugins/docker-compose,或者直接 docker compose version 看输出格式。

还有一点值得强调:Compose 文件本身是会被 Git 纳入版本管理的。我习惯把 docker-compose.yml 当成“基础设施代码”看待,不能随手动修改命令,只能通过改文件再执行 up 来变更状态。这样团队里任何人拿到仓库都能复制出同一套环境,而不是靠某个人脑子里的记忆或聊天记录里的碎片命令。

2. 一次 up 背后到底发生了什么

2.1 读配置、算依赖、定项目名

执行 up 时,Compose 做的第一件事不是拉镜像,而是把配置读进内存。

默认情况下它读取当前目录的 docker-compose.yml。如果存在 docker-compose.override.yml,会把它合并进去,后者的优先级更高。这里有个很难察觉的知识点:Compsoe 支持多个配置文件合并,但数组类型字段(比如 environment、ports)不是简单覆盖,而是合并追加。很多人改了 override 里的端口,发现原来的监听端口还在,多半就是这个合并规则导致的。

同一时间还要确定“项目名”。默认项目名等于当前目录名(不包含路径前缀,如果有 COMPOSE_PROJECT_NAME 环境变量或 name: 字段则优先)。项目名直接参与了后续所有资源的命名:

TEXT
{项目名}_{服务名}_{序号}

比如项目名是 demo,服务名是 web,容器名就是 demo-web-1。网络则叫 demo_default,卷叫 demo_pgdata。这也是为什么两个不同目录下的 Compose 项目不会互相干扰——项目名做了隔离。反过来,如果你在两个目录里用了相同的项目名,就很容易出现网络和容器冲突,端口占用之类的问题就会冒出来。

依赖解析也是在这一步完成的。depends_on 字段会告诉 Compose 哪个服务先启动。旧版本里这个字段只控制启动顺序,不保证依赖服务真正“可用”。新版加上 condition: service_healthy 之后,可以等到依赖服务健康检查通过再启动自身,这在编排数据库、消息队列这类服务时很有用。

2.2 网络、存储卷与容器的编排顺序

配置解析完之后,Compose 开始按顺序创建底层资源:

  1. 网络;
  2. 存储卷;
  3. 镜像(拉取或构建);
  4. 容器;
  5. 启动容器。

为什么要先建网络?因为容器启动时必须挂到指定的网络上,如果网络还不存在,后面的创建步骤直接失败。同理,卷要先准备好,容器创建时才能直接挂载。

这里要补充一个我见过很多次的现象:同一个 Compose 项目里的多个服务天然在同一个自定义网络上,所以它们可以直接用服务名互相访问,不需要记住具体 IP。例如 web 容器里访问 redis,直接写 redis:6379 就行。这是基于 Docker 内置 DNS 的服务发现,不是魔法,更不是靠运气。搞清楚这一点,你排查“服务名解析失败”时就不会一头雾水了。

网络创建后,Compose 会逐个处理镜像策略。默认行为分为几种:

  • 服务里没有 build 字段,直接 pull 配置指定的镜像;
  • 有 build 字段且没有明确 pull 策略,优先尝试拉取同名镜像后再考虑本地构建;一旦执行了 up --build,则会强制走构建流程;
  • 本地已有镜像且配置里 tag 没变化,默认使用本地镜像,不会比对远端。这也是为什么很多人改了代码重新构建镜像时总要先手动打一个 tag,或者用 docker pull 更新基础镜像。

容器创建本身是轻量操作,真正花时间的是镜像拉取和健康检查启动。所以在网络慢的环境里,up 卡在 Pulling image 是正常的,耐心等。

2.3 对存量容器采用增量更新

up 最有价值的地方不在于首次创建,而在于二次、三次执行时的“增量对齐”。

如果你的服务已经跑着,直接再次执行 docker compose up -d:

  • 配置没变的容器保持原样,不打断不重启;
  • 配置有变化的容器被 recreate(重建);
  • 多了几个配置里不存在的容器,会提示 orphan 容器(孤儿容器),并询问是否清理;
  • 如果镜像 tag 变了,会重新拉取并重建。

这意味着日常发布新版本时,不需要手动画线地停服等半天。你只要把镜像 tag 改掉,然后执行一次 up -d 就行。

但要注意,不是所有“变化”都会触发重建。Compose 会做配置哈希比对,某个服务只要网络、端口、卷、环境变量、镜像等关键配置没变化,即使 docker-compose.yml 里改了个注释,也不会重建。如果你确实想强制重建(哪怕配置没变),用 --force-recreate。反过来,如果因为改动太频繁导致服务一直被重建,也可以用 --no-recreate 让 Compose 保留现有容器,只创建缺失的。

3. 实战:五种高频场景下的 up 用法

3.1 第一次启动:docker compose up -d

这是最标准的首次部署命令。我几乎不用不带 -d 的 up,因为前台模式会把所有容器的日志实时打到终端上,一旦关掉窗口或 Ctrl+C,整个服务组就被带停了。只有在调试单个服务、需要实时看输出时才考虑不加 -d。

BASH
docker compose up -d

正常执行完的提示长这样:

TEXT
[+] Running 3/3
✔ Network demo_default Created
✔ Container demo-web-1 Started
✔ Container demo-redis-1 Started

第一次执行会创建网络和容器,第二次执行这段输出基本只会显示 Running 而不是 Created。如果项目很大,可以加 --wait,让 up 命令阻塞到所有容器进入 running 或 healthy 状态后再退出。这个参数在 CI/CD 里特别实用,不然脚本会误以为服务立刻就能访问了。

BASH
docker compose up -d --wait

首次部署后别急着庆祝,至少看一眼 docker compose ps,确认容器状态是 Up 而不是 Restarting 或 Exited。状态列的情况才是最关键的。

3.2 改完配置想刷新:up -d 而不是 restart

新手最常见的操作是改完 docker-compose.yml 或者 .env 后执行 docker compose restart。restart 只是把已有容器停止再启动,它不会重新读取更改后的 Compose 配置,也不会重建容器。这会导致改了端口、环境变量、挂载卷之后,restart 根本不生效,然后在那边怀疑人生。

正确的做法是:

BASH
docker compose up -d

它会根据新配置做增量更新。最好再加一个 --remove-orphans,把配置里已经删掉但旧容器还残留的资源清理掉:

BASH
docker compose up -d --remove-orphans

这里单独提醒一句:.env 文件里的变量加载时机很微妙。Compose 在解析 docker-compose.yml 时就会读取 .env 做变量替换,容器内存的环境变量则取决于服务定义里的 environment: 段或 .env 文件本身。如果你改了 .env 后再执行 up -d,多数情况下会正常生效,但如果容器进程在启动时把环境变量缓存到了应用内部,那你需要的是重建容器,而不是重启应用。up -d 恰好做了这件事。

3.3 代码更新后强制重建:up --build

开发场景中,镜像的更新频率远高于 Compose 配置本身。如果服务定义里带着 build: 字段,修改代码后直接 up -d 大概率还是旧镜像在跑,因为没有触发重新构建。这时候用:

BASH
docker compose up -d --build

它会在启动前重新构建所有需要构建的服务镜像,构建完成后再和现有容器做差异对比,然后启动或重建。

有个容易让人困惑的点:--build 并不意味着“每次都从零构建”。Docker 经典构建模式是有层缓存的,只要 Dockerfile 里的指令和基础上下文没变化,构建就会直接命中缓存,秒级完成。如果你改了代码但发现构建出来的镜像没变化,多半是 Dockerfile 的 COPY 路径写窄了,或者 .dockerignore 把新文件排除了。排查思路是:

BASH
docker compose build --no-cache

强制绕开缓存重新构建一次,如果这个命令能构建出新镜像,那就是缓存策略的问题。

生产环境里还有一个被迫要用的场景:某个服务的镜像由外部定时构建,远端 tag 内容已经更新,但本地的 tag 还停留在旧版。up -d 默认不会主动去拉最新镜像。此时可以:

BASH
docker compose pull --ignore-pull-failures
docker compose up -d

或者用新版 Compose 的 --pull always/--policy 参数控制拉取策略。不要指望 up 会帮你每天自动检查远端。

3.4 临时扩缩容:up --scale 可能救你一次

up 不仅是启动工具,也具备简单的扩缩容能力。比如 web 服务默认副本数为 1,临时想拉到 3 个实例,可以:

BASH
docker compose up -d --scale web=3

这会创建 web-1、web-2、web-3 三个容器。前提是服务内部不能写死端口映射,因为 Compose 不会自动处理多容器共用宿主端口的问题。如果 web 服务里定义了 ports: - "8080:80",扩容到 3 个必然端口冲突。正确做法是服务对外不暴露宿主端口,所有流量通过 Compose 网络内部的负载均衡(比如用 Nginx 反向代理所有 web 节点)或外部接入层做分发。

缩容则是:

BASH
docker compose up -d --scale web=1

Compose 会按照序号从后往前停止并删除多余容器。注意这只是临时手段,真正完整的副本管理还是交给 Kubernetes、Nomad 这类调度器更靠谱,但中小项目里 --scale 足够用一阵子。

3.5 只启动部分服务:up 服务名

实际项目里不一定要全量启动。调试时需要只跑数据库,可以:

BASH
docker compose up -d redis

Compose 会启动 redis 以及它依赖的服务(如果有)。如果你希望跳过依赖启动,加 --no-deps:

BASH
docker compose up -d --no-deps redis

这个命令在日常开发里非常省资源,不用每次把整个栈都拉起来。但要注意,只启动部分服务时,其他服务容器如果之前就在跑,up -d redis 不会停掉它们。它只管自己那部分,剩下的状态不动。

4. 实战排障:up 起不来怎么办

4.1 端口被占用

这是 up 失败里最高频的问题,直接报错一般是:

TEXT
Error response from daemon: driver failed programming external connectivity on endpoint demo-web-1: Bind for 0.0.0.0:8080 failed: port is already allocated

第一步先看谁在占用:

BASH
sudo lsof -i :8080
# 或者
ss -lntp | grep 8080

有可能是之前启动的同端口容器没删干净,也可能是宿主机上有别的进程。如果是容器造成的,用 docker ps -a 看看哪些容器还占着端口。找到问题后,扩充策略里有几条:

  • 停掉旧容器并释放端口;
  • 改 Compose 里的宿主机端口映射(容器端口不用动,只要保证内部服务不变,宿主端口随便换);
  • 检查是否有多个 Compose 项目用了同一个 COMPOSE_PROJECT_NAME。

有一点容易被忽视:docker compose down 会停止并删除容器和默认网络,但如果你手动仅 stop 容器,端口不会释放。要彻底清理得 down 后再 up。

4.2 容器一直重启(Restarting 状态)

docker compose ps 显示某个服务一直处于 Restarting,比“Exited”更让人头大。基本方向只有一个:看日志。

BASH
docker compose logs -f 服务名
docker compose logs --since 1h 服务名

常见的自重启原因:

  • 应用入口命令执行完就退出,容器没有前台进程;
  • 启动时连不上依赖服务,抛错退出,然后被 restart 策略拉起来;
  • 健康检查一直不通过;
  • 进程因为权限、配置错误立刻崩溃。

排查时不要光看最后几行日志,很多时候致命错误在日志开头的几百毫秒内输出完就退出了。可以把日志导出来滚到底:

BASH
docker compose logs 服务名 > service.log 2>&1
tail -n 200 service.log

如果日志显示正常但容器还是退出,检查镜像里的 command 和 entrypoint 写没写对。比如 nginx 镜像里写了 daemon off 才能在前台跑,如果配置有问题容器会秒退。

4.3 挂载目录权限不对(Permission denied)

容器里写入挂载目录时出现权限拒绝,是最折磨人的问题。原因很简单:容器内进程的用户 ID(UID)和宿主机上目录属主不同。

例如宿主机的 /data/mysql 所有者是 uid 1000,容器内 mysql 进程却以 uid 999 运行,写文件时自然被拒绝。解决办法不是 chmod 777(别这么干),而是把目录属主调整成容器内用户的 uid:

BASH
sudo chown -R 999:999 /data/mysql

或者更稳妥一点,在服务定义里指定 user: "1000:1000",让容器进程以宿主机用户身份运行。任何容器内写持久化数据的服务都会遇到这个问题,最快的方法是用 docker compose exec 进去看一眼进程身份:

BASH
docker compose exec 服务名 id

出来是 uid=0(root) 就说明这个镜像默认跑 root,后面权限问题通常就是目录属主对不上的事。

4.4 环境变量怎么改都不生效

这种情况经常让人怀疑人生,因为你改了 .env 或 environment 段,然后执行了 up -d,进容器里一看变量还是旧的。

先确认你改的位置对不对。Compose 有两层“环境变量”:

  • .env 文件负责 Compose 文件本身的变量替换,例如 ${MYSQL_PORT};
  • 服务定义里的 environment: 才负责写入容器的环境变量。

只改 .env,除非 Compose 文件引用了它,否则容器里的环境变量不会变。改 environment: 之后,还需要触发容器重建。如果 Compose 计算出配置没变化,容器不会重建,环境变量自然不会刷新。此时可以加 --force-recreate 强制重建:

BASH
docker compose up -d --force-recreate 服务名

再确认实际值的方法:

BASH
docker compose exec 服务名 env

如果环境变量还是旧值,八成是镜像或者应用启动脚本里对变量做了二次覆盖,那就需要检查镜像内部配置文件。

4.5 服务名解析失败,连不上其他服务

容器之间用服务名访问对方,但偶尔会遇到 UnknownHostException 或者 could not resolve host: redis。

先想一下:两个服务是否在同一个 Compose 项目里?如果分别是两个项目,又没有显式把网络连接起来,默认网络隔离,服务名当然不可见。解决方式要么把两个服务放进同一个 Compose 文件,要么在服务定义里加:

YAML
networks:
- shared_network

然后通过 docker network connect 或 Compose 里的 external: true 网络把项目打通。

还有一种非常隐蔽的情况:Compose 项目名被覆盖,导致容器虽然同名,但网络名不一样,服务彼此在不同网段。执行 docker inspect 容器名 看它的 NetworkMode,基本能确认网络问题。

5. 我平时会额外注意的几个习惯

到目前为止可能有收获不少知识,但要说真正的实战价值,我觉得这几个习惯比任何一个命令都重要。

第一,配置改动落盘前先用 docker compose config 验证。这个命令会渲染出最终生效的 Compose 配置,包含所有变量替换和合并结果。不用等 up 跑一半才报错,也不用猜合并后的端口到底是谁。我写过不少因为 override 文件数组合并导致端口被覆盖的情况,全靠它才没有制造出更混乱的现场。

第二,用 COMPOSE_PROJECT_NAME 固定项目名。靠目录名推断项目名在本地没问题,但一旦目录被复制到别处改名,所有资源名都变了。如果你发现新环境里出现一堆类似 demo_web_1 和老环境完全对不上的容器,多数就是这个原因。固定项目名之后,环境切换时资源名可预期,排障效率高很多。

第三,不是万不得已别执行 down -v。down 会删除容器和默认网络,-v 会把 Compose 里声明的命名卷一起删掉。生产环境里手误一次 down -v,数据库数据直接清零。平时清理环境我默认用 docker compose down,不带 -v,真正需要重建数据卷时才慎重考虑。

第四,给 up 做一层可记忆的封装。项目大了之后,我习惯写一个简单的 Makefile 或脚本,把常用命令包起来:

MAKEFILE
up:
docker compose up -d --remove-orphans --wait
down:
docker compose down
logs:
docker compose logs -f

团队成员不用背命令,也减少了敲错 flag 的概率。说起来很土,但这是目前小团队里成本最低、效果最稳的标准化方式。

第五,别在生产环境把容器的调试端口直接映射出来。如果只是为了临时调试,用 docker compose exec 进入容器,或者在 Compose 里定义独立的一个调试服务,不用改生产服务的端口映射。我对这个原则吃过亏,把调试用的 9000 端口留在线上,最后被扫描工具给统计了一笔无关紧要的“暴露面”,不值得。

up 命令本身不长,但理解它的配置对齐逻辑,比背多少个 flag 都重要。下次在哪台机器上执行 docker compose up -d 之前,先想想这份 Compose 文件是不是你真正想要的状态,这个习惯养成了,容器环境的种种问题大概率都能绕过去。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
白追追
博客等级 码龄10年 48粉丝 1,505原创