概述
一个 cloud-demo 集群里有网关、订单、用户、Nacos、MySQL 五个容器,手敲五条 docker run 不只是累------容器 IP 每次重建都变,微服务根本不知道该往哪儿注册。Compose 文件就是为了解决这件事:把整集群的启动参数写成一份 YAML,让容器之间用服务名互相找到对方。
纲要
- 要解决什么:手工部署微服务集群的四处别扭------启动顺序、网络互通、IP 漂移、命令不可复用
- Compose 文件 :
version/services/image/build/ports/environment/volumes/$PWD - 核心改动 :把
localhost全部换成 Compose 里的服务名(nacos、mysql) - 打包约定 :三个微服务的 jar 都必须叫
app.jar,靠 pom 里的<finalName>app</finalName>实现 - 部署动作 :上传
cloud-demo→docker compose up -d→ps/logs -f/restart/down - 关键原理:Compose 自建 bridge 网络 + 内嵌 DNS,容器内用服务名解析到对方容器 IP
- 踩坑清单 :注册中心地址写 localhost、Nacos 未就绪导致
Connection refused、端口冲突、app.jar名字/目录搞错、旧命令docker-compose(横线)与新命令docker compose(空格)
Compose 到底替你省了什么
先别急着写 YAML,看看不写会怎样。要手工跑起这套集群,你得敲这么一串:
bash
docker run -d --name nacos -e MODE=standalone -p 8848:8848 nacos/nacos-server
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=123 \
-v $PWD/mysql/data:/var/lib/mysql \
-v $PWD/mysql/conf:/etc/mysql/conf.d/ \
mysql:5.7.25
# 然后分别进三个目录 docker build,再 docker run,还得让它们连同一个自定义网络......
docker network create cloud-net
docker run -d --name userservice --network cloud-net cloud-demo-userservice
docker run -d --name orderservice --network cloud-net cloud-demo-orderservice
docker run -d --name gateway --network cloud-net -p 10010:10010 cloud-demo-gateway
对照一下:
| 问题 | 手工 docker run | docker-compose.yml |
|---|---|---|
| 命令录入 | 六条以上长命令,参数全靠记忆 | 一个文件写完,up 一条启动 |
| 容器网络 | 必须自己 docker network create 再逐个 --network |
Compose 自动创建项目专用网络,全部服务默认加入 |
| 服务发现 | 容器 IP 重启就变,写死 IP 会挂 | 内嵌 DNS,用服务名访问,重建也不变 |
| 中间件顺序 | 人肉保证先起 Nacos 再起业务服务 | 可配 depends_on 表达依赖关系(但注意它不等就绪) |
| 复用到别的机器 | 复制粘贴命令文本 | 复制整个目录 + 一条命令 |
一个判断原则:只要某组容器需要互相认识、需要成套起停,就值得放进 Compose 文件。单容器无所谓,集群就必须写。
准备部署目录
这次要部署的工程结构是这样的,day03-Docker/资料/cloud-demo 目录下每个微服务一个子目录,各自带 Dockerfile:
text
cloud-demo/
├── docker-compose.yml # 集群编排文件,唯一入口
├── gateway/ # 网关服务
│ ├── Dockerfile
│ └── app.jar # 打包后拷进来(初始为空)
├── user-service/ # 用户服务
│ ├── Dockerfile
│ └── app.jar
├── order-service/ # 订单服务
│ ├── Dockerfile
│ └── app.jar
└── mysql/ # 数据库数据与配置
├── conf/
│ └── hmy.cnf
└── data/ # 已预置 cloud_user、cloud_order 库表
├── cloud_user/
└── cloud_order/
三个微服务的 Dockerfile 内容完全一致:
dockerfile
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar
这里有个隐含约定必须记住:COPY ./app.jar 是相对构建上下文的路径 ,所以 jar 包的文件名必须叫 app.jar,且必须放在 Dockerfile 同级目录。名字错了,构建阶段就会报:
text
COPY failed: file not found in build context or excluded by .dockerignore: stat app.jar: file not found
mysql/data 里预置了 cloud_user、cloud_order,mysql/conf/hmy.cnf 是要覆盖给容器的配置。要说明的是:这份 data 目录来自课程资料包,跟着资料走的话表和数据都是现成的;如果你是自己从零搭,data 目录是空的,需要先执行建表 SQL(见同系列《服务拆分-案例Demo》一篇的 cloud-user.sql / cloud-order.sql),或者偷个懒------把 SQL 文件放进挂载到 /docker-entrypoint-initdb.d 的目录里,MySQL 镜像首次初始化时会自动执行里面的 .sql、.sh 脚本:
ini
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
skip-name-resolve 这条在容器场景里很重要。MySQL 默认会对客户端 IP 做反向域名解析,容器网络的 IP 通常解析不出 PTR 记录,连接会卡在建立阶段;关掉解析,直接走 IP 授权,连接就快也稳。
Compose 文件逐段拆
下面是 cloud-demo/docker-compose.yml 的真实内容,一字未改:
yaml
version: "3.2"
services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"
五个服务的角色和关键区别:
| service | 来源 | 是否暴露端口 | 为什么这样配 |
|---|---|---|---|
nacos |
官方镜像 nacos/nacos-server |
是,8848 | 要能从浏览器进控制台看注册实例,MODE: standalone 等价以前的 -m standalone 单机模式 |
mysql |
官方镜像 mysql:5.7.25 |
不暴露 | 只对集群内部访问,端口映射出去等于把数据库开到公网 |
userservice |
build: ./user-service |
不暴露 | 走网关转发,不允许外部直连绕过鉴权 |
orderservice |
build: ./order-service |
不暴露 | 同上 |
gateway |
build: ./gateway |
是,10010 | 全集群唯一入口,认证、鉴权、路由都在这里做 |
build: ./gateway 写的是目录不是文件 ,Compose 会进这个目录找 Dockerfile 并执行 docker build。这里的路径相对的是 compose 文件所在目录,不是你敲命令的位置。
volumes 里那个 $PWD 是个实用技巧。为什么不能写成绝对路径 /root/cloud-demo/mysql/data?因为这套目录今天是放在 Windows 的课件文件夹里,明天要扔到某台 Linux 服务器的任意路径下,写死就废了。$PWD 是 Compose 在执行时取 shell 的当前目录来替换,只要在 cloud-demo 目录下敲命令,就一定能找到正确的 mysql/data 和 mysql/conf。
version: "3.2"这行在新版 Compose(v2 及 Docker Compose Spec)里已经废弃,写了会打印一条告警,不写也完全能用。老项目里见到的基本都是这个格式。
容器之间凭什么用服务名互访
这是整个方案里最该弄清楚的一点。以前本地跑,配置写 localhost:8848;一旦进了容器,localhost 指的就是当前容器自己------userservice 在自己容器里找 8848,当然连不上 Nacos。
Compose 在 up 的时候做了两件事(用 Mermaid 表示整个拓扑和网络边界):
#mermaid-svg-89uZhdH8kXeiCIRC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-89uZhdH8kXeiCIRC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-89uZhdH8kXeiCIRC .error-icon{fill:#552222;}#mermaid-svg-89uZhdH8kXeiCIRC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-89uZhdH8kXeiCIRC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-89uZhdH8kXeiCIRC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-89uZhdH8kXeiCIRC .marker.cross{stroke:#333333;}#mermaid-svg-89uZhdH8kXeiCIRC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-89uZhdH8kXeiCIRC p{margin:0;}#mermaid-svg-89uZhdH8kXeiCIRC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-89uZhdH8kXeiCIRC .cluster-label text{fill:#333;}#mermaid-svg-89uZhdH8kXeiCIRC .cluster-label span{color:#333;}#mermaid-svg-89uZhdH8kXeiCIRC .cluster-label span p{background-color:transparent;}#mermaid-svg-89uZhdH8kXeiCIRC .label text,#mermaid-svg-89uZhdH8kXeiCIRC span{fill:#333;color:#333;}#mermaid-svg-89uZhdH8kXeiCIRC .node rect,#mermaid-svg-89uZhdH8kXeiCIRC .node circle,#mermaid-svg-89uZhdH8kXeiCIRC .node ellipse,#mermaid-svg-89uZhdH8kXeiCIRC .node polygon,#mermaid-svg-89uZhdH8kXeiCIRC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-89uZhdH8kXeiCIRC .rough-node .label text,#mermaid-svg-89uZhdH8kXeiCIRC .node .label text,#mermaid-svg-89uZhdH8kXeiCIRC .image-shape .label,#mermaid-svg-89uZhdH8kXeiCIRC .icon-shape .label{text-anchor:middle;}#mermaid-svg-89uZhdH8kXeiCIRC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-89uZhdH8kXeiCIRC .rough-node .label,#mermaid-svg-89uZhdH8kXeiCIRC .node .label,#mermaid-svg-89uZhdH8kXeiCIRC .image-shape .label,#mermaid-svg-89uZhdH8kXeiCIRC .icon-shape .label{text-align:center;}#mermaid-svg-89uZhdH8kXeiCIRC .node.clickable{cursor:pointer;}#mermaid-svg-89uZhdH8kXeiCIRC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-89uZhdH8kXeiCIRC .arrowheadPath{fill:#333333;}#mermaid-svg-89uZhdH8kXeiCIRC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-89uZhdH8kXeiCIRC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-89uZhdH8kXeiCIRC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-89uZhdH8kXeiCIRC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-89uZhdH8kXeiCIRC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-89uZhdH8kXeiCIRC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-89uZhdH8kXeiCIRC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-89uZhdH8kXeiCIRC .cluster text{fill:#333;}#mermaid-svg-89uZhdH8kXeiCIRC .cluster span{color:#333;}#mermaid-svg-89uZhdH8kXeiCIRC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-89uZhdH8kXeiCIRC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-89uZhdH8kXeiCIRC rect.text{fill:none;stroke-width:0;}#mermaid-svg-89uZhdH8kXeiCIRC .icon-shape,#mermaid-svg-89uZhdH8kXeiCIRC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-89uZhdH8kXeiCIRC .icon-shape p,#mermaid-svg-89uZhdH8kXeiCIRC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-89uZhdH8kXeiCIRC .icon-shape .label rect,#mermaid-svg-89uZhdH8kXeiCIRC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-89uZhdH8kXeiCIRC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-89uZhdH8kXeiCIRC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-89uZhdH8kXeiCIRC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Compose 自建 bridge 网络 cloud-demo_default
宿主机
10010 端口映射
8848 端口映射
http://userservice:8081
http://orderservice:8081
注册 + 拉取配置 nacos:8848
注册 + 拉取配置 nacos:8848
拉取路由 nacos:8848
jdbc:mysql://mysql:3306
jdbc:mysql://mysql:3306
解析
解析
解析
浏览器 / curl
192.168.101.xx:10010
Nacos 控制台
:8848
gateway
服务名: gateway
容器端口 10010
user-service
服务名: userservice
order-service
服务名: orderservice
注册中心
服务名: nacos
端口 8848
数据库
服务名: mysql
端口 3306
内嵌 DNS
服务名 → 容器 IP
具体机制拆开说:
- Compose 会创建一个默认网络,名字默认是
{项目目录名}_default,本例即cloud-demo_default,文件里所有 service 自动加入。 - Docker 在这个网络上起了内嵌 DNS(容器内
/etc/resolv.conf指向 Docker 的 DNS)。当 userservice 解析mysql时,DNS 直接返回名为mysql的容器当前 IP。 - 容器重建后 IP 会变,但 DNS 记录会同步更新,所以只要服务名不变,配置就不用动。这就是为什么配置里绝不能写死 IP。
- 端口一律写容器内部 端口,不是映射后的宿主机端口。gateway 访问
userservice:8081,8081 是 user-service 容器里 Tomcat 监听的端口,跟宿主机毫无关系。
一次完整的注册与调用,时序是这样的:
Browser mysql userservice nacos Compose 内嵌 DNS gateway Browser mysql userservice nacos Compose 内嵌 DNS gateway #mermaid-svg-py6jYBbPdDEXXpXS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-py6jYBbPdDEXXpXS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-py6jYBbPdDEXXpXS .error-icon{fill:#552222;}#mermaid-svg-py6jYBbPdDEXXpXS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-py6jYBbPdDEXXpXS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-py6jYBbPdDEXXpXS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-py6jYBbPdDEXXpXS .marker.cross{stroke:#333333;}#mermaid-svg-py6jYBbPdDEXXpXS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-py6jYBbPdDEXXpXS p{margin:0;}#mermaid-svg-py6jYBbPdDEXXpXS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-py6jYBbPdDEXXpXS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-py6jYBbPdDEXXpXS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-py6jYBbPdDEXXpXS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-py6jYBbPdDEXXpXS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-py6jYBbPdDEXXpXS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-py6jYBbPdDEXXpXS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-py6jYBbPdDEXXpXS .sequenceNumber{fill:white;}#mermaid-svg-py6jYBbPdDEXXpXS #sequencenumber{fill:#333;}#mermaid-svg-py6jYBbPdDEXXpXS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-py6jYBbPdDEXXpXS .messageText{fill:#333;stroke:none;}#mermaid-svg-py6jYBbPdDEXXpXS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-py6jYBbPdDEXXpXS .labelText,#mermaid-svg-py6jYBbPdDEXXpXS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-py6jYBbPdDEXXpXS .loopText,#mermaid-svg-py6jYBbPdDEXXpXS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-py6jYBbPdDEXXpXS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-py6jYBbPdDEXXpXS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-py6jYBbPdDEXXpXS .noteText,#mermaid-svg-py6jYBbPdDEXXpXS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-py6jYBbPdDEXXpXS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-py6jYBbPdDEXXpXS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-py6jYBbPdDEXXpXS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-py6jYBbPdDEXXpXS .actorPopupMenu{position:absolute;}#mermaid-svg-py6jYBbPdDEXXpXS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-py6jYBbPdDEXXpXS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-py6jYBbPdDEXXpXS .actor-man circle,#mermaid-svg-py6jYBbPdDEXXpXS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-py6jYBbPdDEXXpXS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段一:启动注册 阶段二:外部请求 解析 nacos 返回 nacos 容器 IP 注册实例 gateway / 拉取路由配置 解析 mysql 返回 mysql 容器 IP 建立数据源连接池 注册实例 userservice GET /user/1 (宿主机:10010) 解析 userservice 返回 userservice 容器 IP 转发 GET /user/1 SELECT * FROM tb_user 结果集 JSON JSON
depends_on 要不要加?这份原始文件里没写。想表达"userservice 依赖 nacos 和 mysql",可以补一段:
yaml
userservice:
build: ./user-service
depends_on:
- nacos
- mysql
但要注意一句:depends_on 只保证启动顺序 (先创建并启动 nacos 容器),不保证就绪 (Nacos 的 8848 端口已经能接受连接)。所以加了它,下面那个 Nacos 连接失败的坑照样会踩。真要解决得用 healthcheck + condition: service_healthy,或者在微服务侧配注册重试。
改微服务配置:localhost 全部换成服务名
这一步漏了,集群起来也是坏的。要改三个服务的配置,规则很简单:写 Compose 里的服务名,端口写容器内端口。
user-service/src/main/resources/application.yml:
yaml
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://mysql:3306/cloud_user?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice
cloud:
nacos:
server-addr: nacos:8848 # 原来是 localhost:8848
如果注册中心地址配在 bootstrap 里(SpringCloud Alibaba 的常见写法),那 bootstrap.yml 同样要改:
yaml
spring:
application:
name: userservice
cloud:
nacos:
server-addr: nacos:8848 # 原来是 localhost:8848
config:
file-extension: yaml
order-service 同构,只是库和用户名不同:
yaml
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice
cloud:
nacos:
server-addr: nacos:8848
gateway 不连库,只有一个 Nacos 地址和路由断言:
yaml
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: nacos:8848
gateway:
routes:
- id: user-service
uri: lb://userservice # lb:// 后面的名字要在 Nacos 里能查到
predicates:
- Path=/user/**
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
注意 lb://userservice 里的名字是服务在注册中心注册的名字 (spring.application.name),而 jdbc:mysql://mysql 里的 mysql 是Compose 的服务名。两者恰好同名只是巧合------userservice 和 orderservice 刚好也注册了和 Compose 服务名一样的名字,方便起见保持一致就好,别理解为同一个东西。
打包:让每个微服务都产出 app.jar
Dockerfile 里写死了 COPY ./app.jar,所以打出来的 jar 必须叫这个名字。默认打包名是 {artifactId}-{version}.jar,得在 pom 里指定:
xml
<build>
<!-- 服务打包的最终名称,必须与 Dockerfile 中 COPY 的文件名一致 -->
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这段要加到 user-service、order-service、gateway 三个模块的 pom.xml 里,父工程的 pom 加了没用。
三个都叫 app.jar 会不会冲突?不会。它们落在各自模块的 target/ 目录下,路径天然隔离:
text
cloud-demo-project/
├── user-service/target/app.jar
├── order-service/target/app.jar
└── gateway/target/app.jar
先 clean 再 package,避免上一次的产物干扰:
bash
mvn clean package -DskipTests
部署流程图
#mermaid-svg-rzxCtZKJCpzrPeUd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rzxCtZKJCpzrPeUd .error-icon{fill:#552222;}#mermaid-svg-rzxCtZKJCpzrPeUd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rzxCtZKJCpzrPeUd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rzxCtZKJCpzrPeUd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rzxCtZKJCpzrPeUd .marker.cross{stroke:#333333;}#mermaid-svg-rzxCtZKJCpzrPeUd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rzxCtZKJCpzrPeUd p{margin:0;}#mermaid-svg-rzxCtZKJCpzrPeUd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster-label text{fill:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster-label span{color:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster-label span p{background-color:transparent;}#mermaid-svg-rzxCtZKJCpzrPeUd .label text,#mermaid-svg-rzxCtZKJCpzrPeUd span{fill:#333;color:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd .node rect,#mermaid-svg-rzxCtZKJCpzrPeUd .node circle,#mermaid-svg-rzxCtZKJCpzrPeUd .node ellipse,#mermaid-svg-rzxCtZKJCpzrPeUd .node polygon,#mermaid-svg-rzxCtZKJCpzrPeUd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rzxCtZKJCpzrPeUd .rough-node .label text,#mermaid-svg-rzxCtZKJCpzrPeUd .node .label text,#mermaid-svg-rzxCtZKJCpzrPeUd .image-shape .label,#mermaid-svg-rzxCtZKJCpzrPeUd .icon-shape .label{text-anchor:middle;}#mermaid-svg-rzxCtZKJCpzrPeUd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rzxCtZKJCpzrPeUd .rough-node .label,#mermaid-svg-rzxCtZKJCpzrPeUd .node .label,#mermaid-svg-rzxCtZKJCpzrPeUd .image-shape .label,#mermaid-svg-rzxCtZKJCpzrPeUd .icon-shape .label{text-align:center;}#mermaid-svg-rzxCtZKJCpzrPeUd .node.clickable{cursor:pointer;}#mermaid-svg-rzxCtZKJCpzrPeUd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rzxCtZKJCpzrPeUd .arrowheadPath{fill:#333333;}#mermaid-svg-rzxCtZKJCpzrPeUd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rzxCtZKJCpzrPeUd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rzxCtZKJCpzrPeUd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rzxCtZKJCpzrPeUd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rzxCtZKJCpzrPeUd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rzxCtZKJCpzrPeUd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster text{fill:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd .cluster span{color:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rzxCtZKJCpzrPeUd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rzxCtZKJCpzrPeUd rect.text{fill:none;stroke-width:0;}#mermaid-svg-rzxCtZKJCpzrPeUd .icon-shape,#mermaid-svg-rzxCtZKJCpzrPeUd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rzxCtZKJCpzrPeUd .icon-shape p,#mermaid-svg-rzxCtZKJCpzrPeUd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rzxCtZKJCpzrPeUd .icon-shape .label rect,#mermaid-svg-rzxCtZKJCpzrPeUd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rzxCtZKJCpzrPeUd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rzxCtZKJCpzrPeUd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rzxCtZKJCpzrPeUd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
修改微服务配置
localhost → 服务名
pom 加 finalName
打 app.jar
拷贝 jar 到
各服务 Dockerfile 同级目录
上传 cloud-demo 到 Linux
docker compose up -d
自动 build 镜像
创建网络 + 启动容器
Nacos 是否已就绪?
微服务注册失败
Connection refused
docker compose restart
userservice orderservice gateway
docker compose ps / logs -f
curl 网关验证
拷 jar 包到部署目录
这一步容易手滑:三个 app.jar 长得一模一样,拷错了目录服务就错位了。
text
# 项目名称 → cloud-demo 下的目标目录
user-service → cloud-demo/user-service/app.jar
order-service → cloud-demo/order-service/app.jar
gateway → cloud-demo/gateway/app.jar
用命令拷(在项目根目录执行),比拖拽靠谱:
bash
DEPLOY=/path/to/cloud-demo
cp user-service/target/app.jar $DEPLOY/user-service/app.jar
cp order-service/target/app.jar $DEPLOY/order-service/app.jar
cp gateway/target/app.jar $DEPLOY/gateway/app.jar
拷完别嫌麻烦,逐个确认一下文件确实落到了对应目录:
bash
ls -lh cloud-demo/*/app.jar
一键部署
把整个 cloud-demo 目录传到 Linux(FTP、scp、rz 均可),进目录执行:
bash
cd /tmp/cloud-demo
docker compose up -d
-d 是后台运行。不加的话输出会一直挂在终端,Ctrl+C 就会把整个集群停掉。执行过程能看到 Compose 在逐个 build 镜像:gateway、userservice、orderservice 各自的 app.jar 被打进 java:8-alpine 基础镜像,然后全部容器一次性拉起。
集群起停、查日志的完整命令:
bash
# 后台构建镜像并启动全部服务
docker compose up -d
# 查看本项目下所有容器状态(不写 service 名就是全部)
docker compose ps
# 实时跟踪全部服务日志
docker compose logs -f
# 只看某个服务,比如排查 user-service
docker compose logs -f userservice
# 重启指定服务(Nacos 就绪后重试注册用得上)
docker compose restart gateway userservice orderservice
# 停止并移除容器、网络(-v 会连匿名数据卷一起删,谨慎)
docker compose down
# 只停不清,容器还在,方便 docker compose start 拉回
docker compose stop
# 进容器内部排障
docker compose exec userservice sh
# 配置改了想重建,加 --build 强制重打镜像
docker compose up -d --build userservice
验证服务真的活了:
bash
docker compose ps
curl http://localhost:10010/user/1
curl http://localhost:10010/order/101
两条 curl 的预期输出:
json
{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
json
{"id":101,"price":699900,"name":"Apple 苹果 iPhone 12","num":1,"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}}
第二条里 user 字段有值,说明网关 → 订单服务 → 用户服务整条链路都通了。
还可以去 Nacos 控制台核对注册情况:浏览器打开 http://localhost:8848/nacos,默认账号密码是 nacos/nacos,服务列表里应该能看到 userservice 和 orderservice 各一个实例。
远程访问就把 localhost 换成虚拟机 IP,例如 http://192.168.101.100:10010/user/1。
Compose 子命令速览
Compose 的子命令跟单容器操作几乎是一一对应的,把 docker <动词> <容器名> 换成 docker compose <动词> <服务名> 就行:
| 目的 | 单容器命令 | Compose 命令 |
|---|---|---|
| 创建并启动 | docker run |
docker compose up |
| 停止 | docker stop |
docker compose stop |
| 启动已有容器 | docker start |
docker compose start |
| 重启 | docker restart |
docker compose restart |
| 查看日志 | docker logs |
docker compose logs -f |
| 查看状态 | docker ps |
docker compose ps |
| 进入容器 | docker exec -it |
docker compose exec |
| 删除 | docker rm |
docker compose down |
有一个版本差异值得单独说:老教程、包括在 2023 年以前的资料里,命令写作 docker-compose up -d(中间是横线 ),那是一个独立的 Python 二进制。Docker 后来把它作为插件集成进 CLI,命令变成 docker compose up -d(中间是空格)。
| 对比项 | docker-compose |
docker compose |
|---|---|---|
| 形态 | 独立二进制,需单独下载到 /usr/local/bin/ 并 chmod +x |
Docker CLI 插件,装了新版 Docker 自带 |
| 版本标识 | docker-compose version |
docker compose version |
| 现状 | Docker Compose v1,2023 年 7 月起停止维护 | Docker Compose v2,官方推荐写法 |
| 兼容性 | 两者读取的 compose 文件基本一致 | 国内很多镜像加速器把 v2 插件一并放进 Docker 源 |
两者的参数设置大体兼容,唯一常见的区别是 docker-compose ps 默认输出格式跟 v2 略不同。踩到 docker: 'compose' is not a docker command 这个报错,说明环境里只有老的 v1 二进制,继续用横线写法即可。
实战踩坑清单
注册中心地址还写着 localhost
现象:容器都起来了,docker compose ps 看着也没 Exit,但 Nacos 控制台上服务列表空的,日志里刷:
text
java.net.ConnectException: Connection refused
at com.alibaba.nacos.client.config.impl.ClientWorker.getServerConfig
原因就是配置文件里 server-addr 还停在 localhost:8848,容器内的 localhost 是它自己。改成 nacos:8848 后要重新打包,改了 yml 不重新打 jar 是没有效果的------配置是被打进 jar 里的。
Nacos 启动太慢,微服务注册失败后不重试
这是课程里实测复现过的坑。首次 up -d 时,Compose 并发拉起所有容器,userservice 和 orderservice 的启动速度远快于 Nacos 初始化。等 Nacos 慢慢起来时,这两个服务早注册失败卡住了:
text
userservice | 启动中... 连接 nacos:8848 Connection refused
nacos | Nacos started successfully in stand alone mode
SpringCloud Alibaba 在注册失败后并没有做重试,服务进程虽然没退出,但已经不参与注册了。两条处置路子:
bash
# 快速办法:Nacos 就绪后,重启三个业务服务让他们重新注册
docker compose restart gateway userservice orderservice
docker compose logs -f userservice
生产环境更稳的作法是分阶段部署 :先把 nacos、mysql 单独 up 起来,确认控制台能打开后再 up 业务服务;其余微服务可以一次性部署。
bash
# 先起基础设施
docker compose up -d nacos mysql
# 等 Nacos 控制台可访问后再起业务服务
docker compose up -d userservice orderservice gateway
端口冲突
up 时报 Bind for 0.0.0.0:10010 failed: port is already allocated。宿主机上 10010 或 8848 已经被别的进程占了(很可能是上次没 down 干净的容器)。定位和处理:
bash
# 查谁占了端口
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep -E '10010|8848'
lsof -i:10010
# 停掉旧的再起
docker compose down
另外记住:Compose 内部多个 service 映射同一个宿主机端口会冲突,但容器内部端口相同完全没关系------它们在不同的网络命名空间里。userservice 和 orderservice 都用 8081,这是常态。
jar 包名字或摆放位置不对
COPY failed: file not found in build context 或镜像起来了却是旧代码,通常是两件事:忘了加 <finalName>app</finalName>(打出来叫 user-service-1.0.0.jar),或者 jar 拷到了 cloud-demo/ 根目录而不是对应子目录。逐个确认:
bash
ls -l cloud-demo/gateway/ cloud-demo/user-service/ cloud-demo/order-service/
MySQL 挂载目录为空导致表不见了
$PWD/mysql/data 依赖你在 cloud-demo 目录下执行命令。如果在别处执行(比如 docker compose -f cloud-demo/docker-compose.yml up -d),$PWD 取到的是上级目录,挂载进去就是空目录,MySQL 会重新初始化一个全新实例,cloud_user 表自然查不到。始终先 cd 到 compose 文件所在目录。
MySQL 连不上时还要留意:jdbc:mysql://mysql:3306 里的用户名和密码要与 MYSQL_ROOT_PASSWORD 对得上,本例是 root/123。
官方文档
总结
- Compose 文件本质就是把一堆
docker run参数翻译成 YAML,up一条命令起一整个集群。 - 集群内部通信一律走服务名,别写 IP 也别写
localhost;Compose 的内嵌 DNS 才是这套方案能跑起来的基础。 - 只有该对外暴露的东西才配
ports:网关 10010、Nacos 8848 可以,MySQL 和业务微服务不行。 build指向的是 Dockerfile 所在目录,路径相对 compose 文件;$PWD保证挂载路径到哪台机器都能用,代价是必须在 compose 文件所在目录执行命令。- Nacos 这类启动慢的基础设施,最好单独先部署再拉业务服务;
depends_on解决不了就绪等待。 - 命令写法取决于环境:新版用
docker compose(空格),只有老二进制时才用docker-compose(横线)。