Docker进阶,很多人卡在存储、网络、Dockerfile、Compose、Swarm这五道坎上。容器能跑起来只是第一步,真正要把开发、测试、交付流程串起来,把微服务架构落地,光靠docker run和几个基础镜像远远不够。我自己在维护几十个容器服务时,踩过无数坑:容器重启后数据丢了、跨主机容器互相访问超时、镜像体积越构建越大、docker-compose在Linux和Windows上表现不一致、Swarm里的服务怎么都发现不了对端。这篇文章就是针对这些痛点,把Docker体系里的存储、网络、Dockerfile、Compose、Swarm五个主题拆开揉碎,讲清楚原理,也给出可以直接抄作业的命令和配置。适合已经会docker run、docker build这类基础操作的进阶读者,也适合正在准备容器化改造的运维和开发同学。
要说清楚的是,我不会只贴官方文档翻译,而是把我在内网部署、生产环境调优时实际验证过的方案写出来。Docker版本差异大,本文统一以Docker 24.x和Compose v2为基准,如果你用的是老版本,部分命令和配置格式需要回退适配,这点后面具体说。
1. Docker进阶内容整体设计与思路拆解
1.1 从单容器到集群,进阶到底要学什么
很多人学Docker进阶,看到“存储、网络、Dockerfile、Compose、Swarm”这堆名词就慌,其实它们之间有清晰的递进关系。先想一个问题:为什么单容器跑着跑着就要学这些?因为容器是临时性的。进程退出、容器删除、节点宕机,数据怎么办?服务怎么恢复?对外怎么提供服务?跨机器怎么沟通?这些问题单靠docker run参数无法优雅解决,于是有了卷、网络模型、镜像构建规范、多容器编排、集群调度。
存储解决的是“容器挂了数据不丢”的问题,网络解决的是“容器之间以及容器与外界怎么通”的问题,Dockerfile解决的是“怎么把应用打包成稳定、可复现、体积可控的镜像”的问题,Compose解决的是“多个容器作为一个项目一起启动、配置、更新”的问题,Swarm解决的是“在多台机器上让容器以服务形式运行、伸缩、故障自愈”的问题。这五块不是孤立的:Compose会在内部编排网络和数据卷,Swarm的服务定义会复用Compose的yaml结构,Dockerfile构建出来的镜像必须配合正确的网络和存储配置才能真正跑生产负载。
我的经验是,学习顺序不用照搬章节,但一定要有几个贯通点。比如你搭建一个企业内部博客系统,第一步写Dockerfile构建应用镜像,第二步用Compose定义数据库和web服务,数据目录用卷持久化,服务挂在自定义网络上通信,第三步扩容两个副本,用Swarm来调度,这就是一个完整的进阶闭环。每做一遍,对概念的理解就深一层。
1.2 绕不开的底层原理:Namespace与Cgroup
进阶到存储和网络,必须理解Docker的隔离机制,否则很多“为什么”解释不了。Docker不是虚拟机,它靠Linux内核的Namespace做资源隔离。一个容器启动后,会创建独立的PID、Network、Mount、UTS、IPC、User这几个命名空间,容器内看起来像完整操作系统,实际是共享宿主内核的进程组。网络隔离靠Network Namespace,每个容器有独立的网络栈、端口、路由表;存储隔离靠Mount Namespace,容器内的文件系统挂载关系是独立的。
Cgroup是另一根支柱,负责限制容器能用的CPU、内存、IO带宽。你写docker run --memory=512m,背后就是在对应Cgroup目录里写了限制值。进阶阶段不需要把内核源码啃完,但必须弄懂这两个概念:你做的卷挂载,本质是跨Namespace把宿主机目录共享给容器;你做的端口映射,本质是把宿主机网络协议栈的流量转给容器Namespace内的进程;你限制容器资源,实际操作的是Cgroup文件。知道这些,遇到“容器里面看到的网卡为什么和宿主机不一样”“为什么容器里能操作内核参数但权限受限”这类问题,就不会一脸懵。
1.3 进阶学习路线与常见误区
我见过太多人走了弯路,典型误区有三类。第一类是把容器当虚拟机用,进去apt install一堆东西,重启后配置全没了,因为没把可变数据放卷里。第二类是用IP地址做容器间通信,一旦容器重建IP就变了,程序立刻断连,正确做法是用服务名或这个网络内置的DNS解析。第三类是不管镜像构建规范,一个Dockerfile里堆几百个RUN指令,镜像几个G,出问题后完全无法定位是哪步装的哪个包。
所以学习路线要围绕“生产可用”来定:先掌握存储和网络这两个基础设施,再规范Dockerfile构建,然后用Compose整合项目,最后再上Swarm做调度。不要一上来就在Swarm里跑一个状态有问题的容器,你会分不清是集群问题还是应用问题。下面我按这个次序把核心细节铺开。
2. Docker存储深度解析:卷、绑定挂载与tmpfs
2.1 三类存储方式对比与选型
Docker的存储方案分三大类:数据卷(named volume)、绑定挂载(bind mount)、临时文件系统(tmpfs)。区别在于数据放在哪、生命周期如何、适用场景是什么。我直接给一张对照表,后面详细展开:
| 维度 | 数据卷 | 绑定挂载 | tmpfs |
|---|---|---|---|
| 数据位置 | Docker管理(/var/lib/docker/volumes/) | 宿主机任意路径 | 内存(非磁盘) |
| 删除容器 | 卷默认保留,不随容器删除 | 宿主文件保留 | 容器删除即销毁 |
| 适用场景 | 数据库持久化、跨容器共享配置 | 开发环境热更新、把日志输出到宿主机 | 存放临时缓存、密钥文件(需内存) |
| 备份迁移 | 适合,需要专门命令 | 直接复制目录即可 | 无法备份 |
| 权限控制 | 创建时指定,较灵活 | 继承目录权限,易踩坑 | 默认严格 |
选型逻辑很简单:生产环境持久化首选数据卷,因为Docker帮你管理存储生命周期,且支持跨平台的卷驱动插件(NFS、Ceph等);开发调试场景用绑定挂载,因为可以本机改代码容器内即时生效;敏感临时数据用tmpfs,比如环境变量中的密钥文件,不想落盘。千万不要把数据库数据直接写在容器可写层里,一旦容器被删就是数据灾难。
2.2 数据卷的创建、备份与迁移
数据卷(named volume)用docker volume create创建,也可以在docker run -v时自动创建。举个例子,我把一个PostgreSQL数据独立成卷:
docker volume create pgdata docker run -d --name postgres \ -v pgdata:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=secret \ postgres:15此时数据实际写进/var/lib/docker/volumes/pgdata/_data。如果容器启动参数错了、想换镜像版本,只要--volumes-from或复用同一个卷挂载到新容器,数据就还在。这里有个经验:给卷起名字要带业务前缀,比如blog_db_data,别用data这么抽象的名字,否则时间长了根本不知道这个卷是干什么的。
卷的备份是很多新手忽视的点。热备份时,直接打包宿主机的_data目录,可能因为数据库缓冲未刷盘导致数据损坏。正确做法是用一个临时容器挂载卷,再执行备份工具。比如备份MySQL:
docker run --rm \ -v dbdata:/var/lib/mysql \ -v $(pwd)/backup:/backup \ mysql:8.0 \ sh -c 'mysqldump --single-transaction -u root -psecret -h 127.0.0.1 -P 3306 --all-databases > /backup/all.sql'注意,上面这个临时容器的网络要和MySQL实例在同一个自定义网络里,否则连不上。迁移卷也一样,用--rm临时容器把旧卷的内容tar出来,再恢复到新机器的卷里,比用docker export整容器迁移更干净,只带走数据,不带历史镜像层。
2.3 绑定挂载在开发环境中的正确用法
绑定挂载(bind mount)就是把宿主机目录直接挂进容器,命令简单:
docker run -d -p 8080:80 \ -v /home/user/myapp:/usr/share/nginx/html \ nginx:latest这在开发框架时很好用,比如前端改代码,刷新就能看到新效果,不用重新构建镜像。但谁用谁知道,这个方案有整整一坑的权限问题。容器里的进程默认是root,挂载目录里的文件如果权限是1000:1000,容器内root能读写,但如果镜像指定了非root用户运行,可能就提示Permission denied。我以前调试过一个问题:Node.js应用容器内以node用户运行,宿主机上的node_modules目录是root所有,结果fs.access就报错。解决方法是让镜像用户ID和宿主机文件属主一致,或者干脆用--user参数指定UID:
docker run -d -p 3000:3000 \ --user $(id -u):$(id -g) \ -v $(pwd):/app \ myapp:dev另一个坑是挂载目录覆盖了容器内已存在的目录内容。比如镜像里/etc/nginx/conf.d有默认配置,你把宿主机一个空目录挂上去,默认配置就被“隐藏”了,导致服务异常。所以绑定挂载前,一定要ls -la看目标目录里有没有镜像自带文件,必要时先复制宿主机目录内容再挂载。
2.4 存储驱动与读写性能调优
Docker宿主机用什么存储驱动,决定了容器的读写性能。主流默认是OverlayFS2(也叫overlay2),它把镜像层和容器层合并为一个视图。存储驱动配置在/etc/docker/daemon.json里:
{ "storage-driver": "overlay2", "storage-opts": [ "overlay2.size=100G" ] }这里overlay2.size限制容器可写层的最大容量,避免某个容器写满磁盘导致宿主机无法服务。生产环境建议单独给/var/lib/docker划分独立分区或独立磁盘,否则系统盘一满,容器直接卡死。我踩过这个坑:日志驱动默认是json-file,单日志文件能涨到几十GB,磁盘被占满后,所有容器批量OOM。后来加了"max-size": "100m", "max-file": "3"限制日志大小,再配置日志轮转,顺便加个定时清理任务,才把问题解决。
关于性能,还有两个小技巧。一是对需要频繁写的目录(比如Elasticsearch的data目录),可以挂载临时卷并设置volume-nocopy标记,减少拷贝开销;二是XFS文件系统挂载时建议加上pquota选项,这样Docker的overlay2配额管理才能生效。这些细节很冷门,但真在生产上遇到容量问题时,能救命。
3. Docker网络模型与容器互联实操
3.1 五大网络模式解析:bridge、host、none、overlay、macvlan
Docker的网络模式是进阶重灾区,很多人只知道端口映射,不理解背后链路。默认的bridge模式本质是docker0网桥加NAT。启动一个容器时,Docker创建一对veth虚拟网卡,一端接容器网络命名空间,另一端接桥接设备。容器内网卡分配一个类似172.17.0.x的地址,流量要出外网,就要经过本机iptables的NAT规则做地址伪装。这也是为什么容器内看到的外网IP与宿主不一致。
host模式是让容器直接共享宿主网络栈,没有独立IP,也没有veth,性能最高,适合对延迟敏感的服务。代价是端口直接占用宿主机端口,没什么隔离性。none模式是剥夺网络,只给一个lo回环接口,适合完全不需要网络的离线任务。macvlan模式让容器拥有宿主机物理网络的独立MAC和IP,相当于一台接入交换机的小主机,但宿主机和容器之间通信反而被隔离,应用层要注意。
最后一个overlay是Swarm跨主机通信的核心。它利用Linux VXLAN技术把多台宿主机的二层网络叠加起来,生成一个虚拟子网。后面第六节会详细演示。
3.2 自定义bridge网络与DNS解析
进阶阶段第一件事,就是别再用默认的bridge网络。默认网络里虽然容器能互通,但没有内置的DNS服务名解析,只能靠--link,而且--link早已被标记过时。正确做法是建立自定义bridge:
docker network create --driver bridge --subnet 172.22.0.0/24 --gateway 172.22.0.1 mynet docker run -d --name web --network mynet nginx docker run -d --name app --network mynet alpine tail -f /dev/null docker exec app ping web # 能通,且直接用容器名当主机名这个DNS解析由Docker自带的嵌入式DNS服务提供,该服务监听127.0.0.11。容器内请求web时,DNS会把它解析为web容器在mynet上的IP。这样容器怎么重建IP变不变都不影响服务。要注意,不同自定义网络之间默认隔离,除非两个网络都用同名字连着同一容器,否则无法互通。
3.3 overlay网络与跨主机通信
当节点组成Swarm集群后,可以创建overlay网络:
docker network create -d overlay --attachable my-overlay--attachable允许非Swarm服务的普通容器也连接这个网络,方便测试。overlay网络的通信路径是:源容器IP包进入veth,到主机上的overlay网桥,然后经VXLAN封装成UDP包,通过宿主机的物理网络发给目标主机,再解封装后送进目标容器。这个模式有性能开销,但换来了跨主机服务发现的灵活性。在生产环境,我一般会让数据库走物理网络或macvlan,而应用服务走overlay,数据库不用暴露端口,应用内网直连。
实现跨主机通信时,还要注意防火墙开放VXLAN端口(通常是4789/udp)和一些其他端口,这在第六节详细说。如果节点在云上,安全组必须配好,否则节点永远Inactive。
3.4 网络排查三板斧
容器网络出问题,按三步排查。第一,docker network inspect <网络名>,查看哪些容器连接、网关是什么、有没有错误的Peer项,这一步能快速判断容器是否在同一个网络。第二,docker exec <容器> ping <目标>、docker exec <容器> nslookup <服务名>,验证DNS解析和连通性。第三,进到宿主机,用nsenter进入容器的网络命名空间,直接抓包或看路由:
pid=$(docker inspect -f '{{.State.Pid}}' <容器>) nsenter -t $pid -n ip addr nsenter -t $pid -n tcpdump -i eth0这种方法比exec里用抓包工具可靠,因为精简镜像可能连ping都没有。另外不要忘了容器内/etc/resolv.conf指向127.0.0.11,如果自定义了dns选项,覆盖了Docker内置DNS,服务名解析会失效,这是常见的“两条命令都一样但有时好有时坏”的元凶。
4. Dockerfile实战:镜像构建的细节与优化
4.1 Dockerfile核心指令与执行逻辑
Dockerfile的本质,是从基础镜像出发,按指令一层层生成只读层,每一条RUN、COPY、ADD都会创建一个中间层。指令执行顺序与缓存机制是理解优化的关键。举个例子:
FROM ubuntu:22.04 RUN apt-get update && apt-get install -y curl COPY . /app CMD ["curl", "-s", "https://example.com"]修改任何一个COPY文件内容,会导致后续所有指令缓存失效重跑。所以通常把“不常变的依赖安装”放前面,“频繁变的代码拷贝”放后面,这样开发迭代时能复用apt缓存层,构建速度快很多。
还有一个最隐蔽的坑:RUN指令中执行命令出错,不会自动清理已安装的临时包,镜像会残留无用文件。所以安装依赖时要把rm -rf /var/lib/apt/lists/*写在同一条RUN里,利用&&串联。我见过很多镜像体积虚胖,就是因为清理工作被拆到了单独的RUN,结果由于层缓存机制,清理层覆盖不了前面层里的文件,反而体积更大。
4.2 多阶段构建与依赖缓存
多阶段构建是现阶段公认的最佳实践,核心是“一个Dockerfile里写多个FROM”。比如Go项目:
FROM golang:1.21 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app/server . FROM alpine:3.18 COPY --from=build /app/server /usr/bin/server EXPOSE 8080 CMD ["server"]这里alpine为基础的精简镜像只包含运行二进制和最小运行库,体积从1GB级别降到50MB级别。依赖下载单独放在go mod download,是为了利用Docker构建缓存:只要go.mod和go.sum不变,就重复使用下载层的缓存,大幅提高编译效率。
但多阶段构建不仅仅是“第二个FROM选小镜像”。记住一个技巧:构建阶段的镜像可以基于开发工具链的发行版,而运行阶段如果没有特殊要求,优先用distroless或alpine。distroless镜像连shell都没有,攻击面更小,排障时要用docker cp把二进制拷出来调试。我建议线上镜像宁可牺牲一点便利,也要保证最小化和安全性。
4.3 构建上下文与.dockerignore
构建上下文是一个隐含性能杀手。docker build会先把整个当前目录(包括node_modules、.git、dist等)发送给守护进程,如果你不写.dockerignore,构建过程会慢到怀疑人生。.dockerignore的语法类似.gitignore:
node_modules .git *.md Dockerfile .dockerignore dist .vscode这里的坑是:如果你的Dockerfile需要复制某些被ignore的目录,它就会被忽略导致构建失败。比如上面ignore了dist,但你的应用依赖docker build时动态编译的dist,就需要要么不ignore,要么在复制前自己执行编译。我通常的做法是保留构建产物目录,但把缓存目录通通ignore掉,例如**/__pycache__、*.pyc、.venv。
另一个需要注意的点:ADD指令会自动解压本地tar包,也会从远程URL下载文件,这些都是不可预测的,会破坏缓存,且带来安全风险。所以现代实践中,下载文件建议用RUN curl加--checksum校验,本地解压包就用COPY后解压。少用ADD的自动解压特性,这在官方文档里也被反复强调。
4.4 生产镜像的加固技巧
很多人把改造运行镜像理解为“换小基础镜像”,远远不够。安全加固的核心是不以root身份运行。默认Dockerfile如果不指定用户,容器进程就是root,一旦容器被攻破,内网横向移动就是root权限在操作。建议在Dockerfile最后加入:
RUN groupadd -r app && useradd -r -g app -M -s /sbin/nologin app USER app如果应用要写日志,需要确保挂载卷的属主是app用户,或者启动命令里用chown。除了用户,还要控制权限能力:使用--cap-drop=ALL --cap-add=NET_BIND_SERVICE,只保留绑定低端口的能力。这虽然是对docker run的参数,但也可以用Dockerfile的ENTRYPOINT配合脚本实现。
另外,容器镜像里不要存放密钥。密钥在生产环境应该通过Secret或环境变量注入。因为镜像本身可能被推送进registry,一旦泄漏,历史层仍是黑盒,几乎无法清洗。我在这上面栽过跟头:把数据库密码写在Dockerfile的ENV里,镜像push到私有仓库后被误投到公开仓库,看到日志里的敏感信息时冷汗直流,整批重新构建才解决。
5. Compose多容器编排:从单机到项目级
5.1 Compose文件结构与版本选择
Compose是单机多容器编排的标配,它把多个容器的启动参数写进一个YAML文件,一条docker compose up -d就全部拉起。注意现在的Compose v2是Docker CLI插件,不再叫docker-compose,这是很多人的混淆点。新装Docker后,直接执行docker compose version验证即可,如果提示unknown command: docker compose,说明没装插件,需要单独装docker-compose-plugin这个包。
Compose YAML里的结构一般是这样:
services: web: build: . ports: - "8080:80" depends_on: - db db: image: mysql:8.0 volumes: - dbdata:/var/lib/mysql volumes: dbdata:顶层有services、volumes、networks三个关键字段。版本的兼容性主要看Docker Compose自身,而不是文件里的version字段。Compose v2已经不再强制要求写version:,写了也没有实际规则作用。旧教程里的version: '3.8'在新版本里会被忽略,功能不受影响。
5.2 常用配置项与依赖控制
depends_on是基础依赖控制,只保证启动顺序,不等服务真正可用。比如web依赖db,如果db启动但还没初始化完成,web连接就会失败。正确做法是加上healthcheck:
services: db: image: mysql:8.0 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-psecret"] interval: 10s timeout: 5s retries: 5 web: build: . depends_on: db: condition: service_healthy这样web会在db健康后才被创建。这个机制被大量用在测试环境,真实环境如果业务量不大,我建议直接让应用具备重试连接的能力,而不是完全依赖编排层,因为编排层的健康检查本身有延迟。
环境变量配置也有讲究。推荐用env_file读外置的.env文件,不要把敏感信息直接硬编码在compose文件里。Compose项目根目录的.env文件会自动被读取用于变量替换。例如:
services: app: image: myapp:latest environment: - DB_HOST=db - DB_PASSWORD=${DB_PASSWORD}5.3 真实案例:用Compose部署MySQL 8.0主从
光说理论不够,我们做一个MySQL主从的例子。它演示了Compose在单机上的应用,也涉及存储、网络、init脚本这几个主题的贯通。目录结构:
mysql-repl/ ├── master/ │ └── my.cnf ├── slave/ │ └── my.cnf └── docker-compose.yml主节点配置master/my.cnf:
[mysqld] server-id=1 log-bin=mysql-bin binlog-format=ROW从节点配置slave/my.cnf:
[mysqld] server-id=2 relay-log=relay-bin read-only=1Compose文件:
services: master: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass volumes: - ./master/my.cnf:/etc/mysql/conf.d/my.cnf - master_data:/var/lib/mysql networks: - replnet slave: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass volumes: - ./slave/my.cnf:/etc/mysql/conf.d/my.cnf - slave_data:/var/lib/mysql networks: - replnet depends_on: - master networks: replnet: volumes: master_data: slave_data:启动后两个容器都在同一个自定义网络replnet,彼此可以用服务名master和slave访问。在主库执行SHOW MASTER STATUS获取log file和position,再在从库执行CHANGE MASTER TO,这就是主从复制的建立。这个例子最大的坑是:两个容器挂在同一个宿主目录上会互相覆盖数据文件,必须用不同卷,像我上面那样。另一个坑是MySQL 8.0默认使用caching_sha2_password认证协议,低版本客户端连不上,所以生产环境用5.7做从库或升级客户端时要注意兼容性。
5.4 Compose项目升级与迁移注意事项
Compose项目升级时,最怕“改了一个服务,带崩了其他服务”。升级前一定要先docker compose config验证语法,它会渲染出最终配置。还会发现变量是否缺失。我一般还会加上一个--dry-run风格的检查:对比docker compose ps的容器数量,确认没有意外扩缩容。
迁移到其他机器时,不要简单把整个目录拷走就完事。数据保留在命名卷里,需要先把卷里的数据导出,再在新机器上创建同名卷。迁移流程是:停服务、备份卷、拷贝源码目录、恢复卷、重新docker compose up -d。如果涉及构建镜像,那镜像仓库也要提前设置好私有Registry,不要在目标机器上裸build,耗时且难以复现。
6. Swarm集群模式:从零搭建与服务发现
6.1 Swarm核心概念:Manager与Worker节点
很多人以为Swarm是过时技术,但它在中小规模集群里依然很实用,尤其在不想引入K8s的运维团队。Swarm把宿主机分成Manager和Worker,Manager负责调度、维持集群状态、处理服务变更,Worker只运行任务。Manager之间用Raft协议做共识,至少3个Manager才能容忍一台挂了。如果你只有一台机器,也可以初始化单节点Swarm,Manager同时充当Worker。
核心调度单位是“任务”(task),一个服务(service)可以指定副本数,比如--replicas 3,Swarm会在可用节点上启动3个任务,每个任务是一个容器。如果某个节点宕机,Swarm会把这个节点上的任务重新调度到其他节点,这就是自愈。这个能力解决的不只是高可用问题,也是配置管理的统一:你不再需要逐台登录机器记住一长串启动参数。
6.2 初始化集群与节点加入流程
初始化Swarm集群只需要一行命令:
docker swarm init --advertise-addr 192.168.1.10--advertise-addr是告诉其他节点通过哪个IP访问当前Manager。这里最容易踩坑的是选了错误的网卡IP,比如虚拟机NAT网络,导致其他节点连不上。建议先用ip addr确认内网IP,再用docker swarm join-token worker获取加入命令。比如输出:
docker swarm join --token SWMTKN-1-xxxx 192.168.1.10:2377在Worker节点上执行这条命令,就能加入集群。别忘了开放端口:2377/tcp用于集群管理,7946/tcp和7946/udp用于节点之间的通信,4789/udp用于overlay网络VXLAN。很多人在云上配了安全组却漏了4789,导致服务间跨主机不通,这就是上一节说过的坑。
节点加进来后,用docker node ls查看。如果某个节点状态显示Unreachable,检查防火墙和advertise-addr。如果是IP变了,要在/etc/docker/daemon.json里设置,然后重启Docker,重新向Manager申请加入。
6.3 服务部署、滚动更新与伸缩
Swarm的服务命令和普通docker run很相似,但多了编排属性。用一个nginx服务做例子:
docker service create \ --name web \ --replicas 3 \ --publish published=8080,target=80 \ --network my-overlay \ nginx:1.25这里的--publish采用published和target两个参数,用逗号分隔,这是新版语法,旧版本里用80:8080也会被自动转换。服务创建后,Swarm会在三个可用节点上各启动一个容器,由Ingress负载均衡统一接收8080端口的流量,再分发给各节点的nginx容器。
滚动更新是Swarm的杀手级功能:
docker service update --image nginx:1.26 --update-delay 5s --update-parallelism 1 web每5秒更新1个副本,适合零停机发布。如果更新过程中发现问题,用docker service rollback web一键回滚。这个回滚命令在实际运维中非常救命。我所在的服务因为更新镜像版本后暴露了配置不兼容问题,用了三次rollback才定位到问题,如果没有这个功能,就需要手动停服务换版本,运维压力完全不同。
伸缩也简单,把副本数从3改成5:
docker service scale web=5Swarm会立刻在剩余节点上调度新任务。要注意的是,使用overlay网络的服务副本扩展起来没有端口冲突,因为Ingress端口是共享的,这是 bridge 网络不具备的优势。
6.4 Swarm与Compose的配合使用
Swarm可以直接使用Compose文件部署服务栈,只需要额外声明顶层deploy字段。比如:
services: app: image: app:1.0 deploy: replicas: 3 update_config: parallelism: 1 delay: 10s resources: limits: cpus: "1.0" memory: 512M networks: - mynet networks: mynet: driver: overlay然后执行:
docker stack deploy -c docker-compose.yml mystackdocker stack命令会把Compose文件中的deploy字段映射成Swarm服务。注意stack部署时,自动创建的overlay网络要用driver: overlay明确指定。还有一个大坑:docker stack不支持build字段,它只能部署已构建好的镜像,所以需要在部署前先docker build并push到仓库。这一点让很多从docker compose转过来的同学迷惑,习惯后倒还算顺手。
Swarm服务发现方面,服务名就是集群内的DNS名。比如上面创建一个app服务,另一个服务里直接curl http://app:3000,Swarm内置的任务和虚拟IP机制会自动把流量分发到其中一个副本。配合滚动更新,它还能在更新时自动摘除旧任务、加入新任务,服务无感知。
7. 常见问题与排查技巧实录
7.1 容器重启后数据丢失
这是最高频的问题,几乎每个从虚拟化时代过来的开发者都遇到过。现象是:docker run创建的容器一旦删除,重新拉起后配置、日志、数据库数据全部消失。原因就是你没有把数据写在卷或绑定挂载里,而是写在了容器可写层。容器删除时可写层被回收,数据自然没了。
排查命令很直接:先docker inspect <容器>,看Mounts字段里有没有列出卷映射。如果没有,再docker exec <容器> sh -c 'echo test > /tmp/test'然后docker restart该容器,如果重启后cat /tmp/test还在,说明数据只是存在于容器层,删除容器还是会丢。遇到这种情况,最稳妥的补救方式是把当前数据复制到卷里:
docker cp <容器>:/var/lib/mysql /tmp/mysql-data docker volume create mysql_data docker run --rm -v mysql_data:/var/lib/mysql -v /tmp/mysql-data:/backup alpine \ sh -c 'cp -a /backup/. /var/lib/mysql/'然后再用-v mysql_data:/var/lib/mysql启动新容器。以后务必用卷,不要心存侥幸。
7.2 容器间网络不通
容器间网络不通的表现通常是:在一个容器里ping另一个容器的IP能通,但按服务名解析失败;或者解析成功但TCP连接超时。按经验,先看是不是自定义网络没有shared。默认bridge网络没有DNS服务名解析,所以服务名ping不通很正常,这是设计如此,不是故障。要解决,创建自定义网络后重新连接容器。
如果是同一个自定义网络下按IP能通但TCP端口不通,那就要考虑防火墙或目标容器没监听。用docker exec进入目标容器,ss -tlnp检查端口监听情况。如果目标服务绑定了127.0.0.1,容器网络栈就访问不到它,必须绑定0.0.0.0。例如有的Node.js应用默认监听IPv6的::1,容器内从IPv4去访问就会一直Pending,这也是很隐蔽的坑。
还有一种是跨主机overlay网络不通。优先检查4789端口,然后docker service logs看服务错误。实在不行,暂时把需要通信的服务部署在同一节点上测试,排除物理网络因素。
7.3 Compose版本依赖导致的命令失败
docker compose up时出现unknown command: docker compose,多数是环境变量PATH里没有Docker CLI插件,或者是老版本Docker没有安装compose-plugin。Ubuntu下执行apt install docker-compose-plugin即可,CentOS则用yum install docker-compose-plugin。安装后验证:
docker compose version另一个关联问题是docker stack deploy不支持build字段、不支持volumes的某些语法。如果出现unsupported config option,多半是把Swarm部署和Compose单机混为一谈。Swarm栈的compose文件里,build必须删掉,镜像必须提前构建好推送。此外,version:字段在部分老版本中被严格要求,如果新版Docker提示版本无关或报错,就直接去掉version行。
7.4 Swarm服务端口暴露异常
Swarm服务端口暴露异常经常是服务创建后才发现端口没生效。可能是因为docker service create时用了旧式--publish 8080:80,语法在新版其实也能用,但如果你在Compose里写过ports: - "8080:80",在docker stack部署时,Swarm会把它视为普通的容器到主机映射,而不是Ingress映射,于是每个节点都尝试绑定同一端口,导致部分节点冲突。
正确的做法是使用docker service create --publish published=8080,target=80,或者在Compose的deploy字段里指定endpoint_mode: vip以及ports下的published和target。如果要用Ingress,确保服务使用的网络是driver: overlay。还有一个小技巧:服务卷挂载会丢失Ingress的负载均衡能力,因为Ingress只能基于TCP层的端口转发,不感知路径,这不影响功能,但要心里有数。
另外,Swarm服务一直处于New状态却起不来,优先用docker service ps <service> --no-trunc看错误信息。最常见的错误是镜像拉取失败或端口被占用。--no-trunc参数不要漏,不然错误消息被截断,你看到的只是半句话,排查效率会打折扣。
写到这里,Docker进阶在存储、网络、Dockerfile、Compose、Swarm这五个方向的核心细节就都覆盖了。我在实际使用时,最常被朋友问“这些概念能不能简化记忆”,我的建议是把五类问题绑定到五条命令:数据持久化找docker volume,网络互通找docker network,镜像构建优化看Dockerfile,多容器管理看docker compose,跨主机调度看docker swarm。遇到不通、不挂、不生效时,先跑一遍我上面写过的排查命令,大多数问题都能定位到根因。如果还有没覆盖到的细枝末节,欢迎在评论区把你的具体报错发出来,我帮你拆。