文章目录
-
- 一、开篇:从"开发环境能跑"到"生产环境上线"
- 二、Dockerfile生产级编写
-
- [2.1 为什么需要多阶段构建](#2.1 为什么需要多阶段构建)
- [2.2 生产级Dockerfile模板](#2.2 生产级Dockerfile模板)
- [2.3 为每个服务编写Dockerfile](#2.3 为每个服务编写Dockerfile)
- 三、Maven插件自动化镜像构建
-
- [3.1 使用spring-boot-maven-plugin构建分层JAR](#3.1 使用spring-boot-maven-plugin构建分层JAR)
- [3.2 使用jib-maven-plugin构建镜像](#3.2 使用jib-maven-plugin构建镜像)
- [3.3 镜像版本管理策略](#3.3 镜像版本管理策略)
- [四、Docker Compose全栈编排](#四、Docker Compose全栈编排)
-
- [4.1 编排架构设计](#4.1 编排架构设计)
- [4.2 环境变量文件](#4.2 环境变量文件)
- [4.3 基础设施编排文件](#4.3 基础设施编排文件)
- [4.4 微服务编排文件](#4.4 微服务编排文件)
- [4.5 环境变量覆盖配置](#4.5 环境变量覆盖配置)
- 五、日志挂载与生产启动脚本
-
- [5.1 日志挂载策略](#5.1 日志挂载策略)
- [5.2 生产启动脚本](#5.2 生产启动脚本)
- 六、线上运维配置
-
- [6.1 资源限制与重启策略](#6.1 资源限制与重启策略)
- [6.2 健康检查与依赖顺序](#6.2 健康检查与依赖顺序)
- [6.3 滚动更新与回滚](#6.3 滚动更新与回滚)
- [6.4 生产环境安全加固](#6.4 生产环境安全加固)
- 七、踩坑指南
-
- 坑一:容器内无法解析`localhost`
- 坑二:MySQL初始化脚本未执行
- 坑三:时区不正确导致日志时间偏差8小时
- [坑四:JVM内存未限制导致容器被OOM Killer杀死](#坑四:JVM内存未限制导致容器被OOM Killer杀死)
- 坑五:健康检查失败导致容器反复重启
- 坑六:镜像构建时Maven依赖下载慢
- 八、课后作业
- 九、下节预告
- [🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航](#🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航)

适配版本 :Spring Boot 4.0.8、Spring Cloud 2025.1.3(Oakwood)、Spring Cloud Alibaba 2025.1.0.0、Docker 27.x、Docker Compose 2.x、JDK 21
课程定位:实战阶段部署课,从Dockerfile编写到Compose编排,从环境变量注入到生产启动脚本,完成微服务项目的容器化上线
一、开篇:从"开发环境能跑"到"生产环境上线"
第33课我们完成了全链路整合调试。在开发环境中,ecommerce-platform的5个微服务、Nacos、Seata、RocketMQ、Sentinel、Prometheus、Grafana、SkyWalking、ELK全部正常运行,下单请求可以从网关一路贯通到数据库,熔断限流和分布式事务也验证通过。
但"开发环境能跑"和"生产环境上线"之间,隔着一条巨大的鸿沟:
- 环境差异:开发环境的JDK路径、配置文件位置、依赖服务地址,与生产环境完全不同。手工部署时,运维人员需要逐台服务器安装JDK、配置环境变量、上传JAR包、修改配置文件------这个过程极易出错。
- 依赖管理:每个微服务都依赖Nacos、MySQL、Redis、RocketMQ等中间件。手工部署时,中间件的版本、配置、网络互通全靠人工保证,一旦某个中间件地址变更,所有服务都要重新配置。
- 扩展困难:大促时需要将订单服务从2个实例扩展到10个实例。手工部署时,需要逐台服务器复制JAR包、启动进程、注册到Nacos------耗时且容易遗漏。
- 回滚缓慢:新版本上线后发现严重Bug,需要紧急回滚。手工部署时,需要重新上传旧版本JAR包、重启所有实例------故障恢复时间(MTTR)可能长达数十分钟。
Docker容器化 正是解决这些问题的标准方案。它将每个微服务及其运行环境打包为一个不可变的镜像 ,在任何支持Docker的机器上都能以完全相同的方式运行。配合Docker Compose,可以一键启动所有服务及其依赖,实现"基础设施即代码"。
本课将完成ecommerce-platform的完整容器化部署。你将学会:为Spring Boot微服务编写生产级Dockerfile、使用Maven插件自动化构建镜像、用Docker Compose编排全部服务与中间件、通过环境变量注入配置、挂载日志到宿主机、编写生产启动脚本、以及线上运维的关键配置。
二、Dockerfile生产级编写
2.1 为什么需要多阶段构建
传统的Dockerfile将编译和运行放在同一个镜像中,导致两个问题:
- 镜像体积巨大:包含了Maven、JDK完整版、源码等,镜像可能超过800MB。
- 安全风险:构建工具和源码留在生产镜像中,增加了攻击面。
多阶段构建(Multi-stage Build) 将Dockerfile分为两个阶段:
- 构建阶段(Builder) :使用完整的JDK和Maven,编译源码、打包JAR。
- 运行阶段(Runtime) :只复制构建好的JAR和精简的JRE,生成最终镜像。
这样最终镜像只包含运行所需的最小内容,体积可缩小到200MB以内。
2.2 生产级Dockerfile模板
在service-order模块的根目录下创建Dockerfile:
dockerfile
# ========== 阶段一:构建 ==========
FROM maven:3.9.9-eclipse-temurin-21 AS builder
WORKDIR /build
# 先复制POM文件,利用Docker缓存层加速依赖下载
COPY pom.xml .
COPY platform-dependencies/pom.xml platform-dependencies/
COPY common-core/pom.xml common-core/
COPY common-api/pom.xml common-api/
COPY common-web/pom.xml common-web/
COPY service-order/pom.xml service-order/
# 下载依赖(如果POM未变化,这一层会被缓存)
RUN mvn dependency:go-offline -B
# 复制源码并编译打包
COPY . .
RUN mvn clean package -pl service-order -am -DskipTests -B
# ========== 阶段二:运行 ==========
FROM eclipse-temurin:21-jre-jammy
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建非root用户运行应用
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# 从构建阶段复制JAR包
COPY --from=builder /build/service-order/target/*.jar app.jar
# 创建日志目录并授权
RUN mkdir -p /app/logs && chown -R appuser:appuser /app
USER appuser
# 暴露端口
EXPOSE 8083
# JVM 参数(可通过环境变量覆盖)
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
# 健康检查(依赖 Actuator)
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8083/actuator/health/liveness || exit 1
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar app.jar"]
关键优化点:
| 优化项 | 作用 |
|---|---|
mvn dependency:go-offline |
单独一层下载依赖,源码变化时不会重新下载 |
-pl service-order -am |
只构建service-order及其依赖模块,减少构建时间 |
eclipse-temurin:21-jre-jammy |
只包含JRE,比完整JDK小很多 |
| 非root用户 | 安全加固,避免容器内root权限 |
HEALTHCHECK |
容器健康检查,配合Compose的依赖管理 |
2.3 为每个服务编写Dockerfile
按照相同模式,为service-user、service-product、service-payment、gateway-server编写Dockerfile。各服务的差异在于:
EXPOSE端口不同:8081、8082、8084、9000HEALTHCHECK中的端口和路径不同JAVA_OPTS可根据服务特点调整(网关需要更大的堆内存)
三、Maven插件自动化镜像构建
3.1 使用spring-boot-maven-plugin构建分层JAR
Spring Boot 4.0支持分层JAR(Layered JAR),将依赖、资源、应用代码分开存放,配合Docker的分层缓存可以进一步加速构建。在父POM中配置:
xml
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
3.2 使用jib-maven-plugin构建镜像
Google的Jib插件可以直接从Maven构建Docker镜像,无需安装Docker守护进程,且自动优化镜像分层。在父POM中添加:
xml
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.3</version>
<configuration>
<from>
<image>eclipse-temurin:21-jre-jammy</image>
</from>
<to>
<image>registry.example.com/ecommerce/${project.artifactId}</image>
<tags>
<tag>${project.version}</tag>
<tag>latest</tag>
</tags>
</to>
<container>
<jvmFlags>
<jvmFlag>-Xms512m</jvmFlag>
<jvmFlag>-Xmx1024m</jvmFlag>
</jvmFlags>
<environment>
<TZ>Asia/Shanghai</TZ>
</environment>
<user>appuser</user>
</container>
</configuration>
</plugin>
构建命令:
bash
# 构建并推送到镜像仓库
mvn compile jib:build -pl service-order
# 构建到本地Docker守护进程
mvn compile jib:dockerBuild -pl service-order
Jib的优势:无需编写Dockerfile,自动分层,构建速度快,且支持直接推送到镜像仓库。
3.3 镜像版本管理策略
| 策略 | 示例 | 适用场景 |
|---|---|---|
| 语义化版本 | 1.0.0、1.0.1 |
正式发布 |
| Git Commit SHA | a1b2c3d |
CI/CD自动构建 |
| 日期时间 | 20260913-1030 |
每日构建 |
| latest | latest |
开发环境,生产环境禁用 |
生产建议 :使用语义化版本 + Git SHA 的组合,例如1.0.0-a1b2c3d,既方便识别版本,又精确对应代码提交。
四、Docker Compose全栈编排
4.1 编排架构设计
ecommerce-platform的完整部署包含基础设施 和微服务两大部分。基础设施包括MySQL、Nacos、Seata、RocketMQ、Sentinel、Prometheus、Grafana、SkyWalking、ELK。微服务包括网关和4个业务服务。
Docker Compose文件结构:
ecommerce-platform/
├── docker/
│ ├── docker-compose-infra.yml # 基础设施
│ ├── docker-compose-app.yml # 微服务
│ ├── .env # 环境变量
│ ├── mysql/
│ │ └── init.sql # 数据库初始化脚本
│ ├── nacos/
│ │ └── custom.env # Nacos 环境变量
│ ├── seata/
│ │ └── application.yml # Seata 配置
│ ├── prometheus/
│ │ └── prometheus.yml # Prometheus 配置
│ └── logstash/
│ └── logstash.conf # Logstash 管道配置
4.2 环境变量文件
创建docker/.env:
properties
# ===== 镜像版本 =====
IMAGE_TAG=1.0.0
REGISTRY=registry.example.com/ecommerce
# ===== 数据库 =====
MYSQL_ROOT_PASSWORD=Root@2026!
MYSQL_HOST=mysql
MYSQL_PORT=3306
# ===== Nacos =====
NACOS_HOST=nacos
NACOS_PORT=8848
NACOS_USERNAME=nacos
NACOS_PASSWORD=Nacos@2026!
# ===== Seata =====
SEATA_HOST=seata-server
SEATA_PORT=8091
# ===== RocketMQ =====
ROCKETMQ_NAMESRV=rocketmq-namesrv:9876
# ===== 服务端口 =====
GATEWAY_PORT=9000
USER_PORT=8081
PRODUCT_PORT=8082
ORDER_PORT=8083
PAYMENT_PORT=8084
4.3 基础设施编排文件
创建docker/docker-compose-infra.yml:
yaml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: ecommerce-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- ecommerce-net
nacos:
image: nacos/nacos-server:v3.1.1
container_name: ecommerce-nacos
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_TOKEN: "SecretKey012345678901234567890123456789012345678901234567890123456789"
NACOS_AUTH_IDENTITY_KEY: "nacos"
NACOS_AUTH_IDENTITY_VALUE: "nacos"
ports:
- "8848:8848"
- "9848:9848"
- "8080:8080"
depends_on:
mysql:
condition: service_healthy
networks:
- ecommerce-net
seata-server:
image: seataio/seata-server:2.5.0
container_name: ecommerce-seata
environment:
SEATA_PORT: 8091
STORE_MODE: db
DB_HOST: mysql
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
DB_NAME: seata
SEATA_IP: seata-server
ports:
- "8091:8091"
- "7091:7091"
depends_on:
mysql:
condition: service_healthy
networks:
- ecommerce-net
rocketmq-namesrv:
image: apache/rocketmq:5.3.1
container_name: ecommerce-rocketmq-namesrv
command: sh mqnamesrv
ports:
- "9876:9876"
networks:
- ecommerce-net
rocketmq-broker:
image: apache/rocketmq:5.3.1
container_name: ecommerce-rocketmq-broker
command: sh mqbroker -n rocketmq-namesrv:9876 -c /home/rocketmq/conf/broker.conf
environment:
NAMESRV_ADDR: rocketmq-namesrv:9876
ports:
- "10909:10909"
- "10911:10911"
volumes:
- ./rocketmq/broker.conf:/home/rocketmq/conf/broker.conf
depends_on:
- rocketmq-namesrv
networks:
- ecommerce-net
sentinel-dashboard:
image: bladex/sentinel-dashboard:1.8.9
container_name: ecommerce-sentinel
ports:
- "8858:8858"
networks:
- ecommerce-net
prometheus:
image: prom/prometheus:v3.2.0
container_name: ecommerce-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- ecommerce-net
grafana:
image: grafana/grafana:11.5.2
container_name: ecommerce-grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: admin123
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- ecommerce-net
skywalking-oap:
image: apache/skywalking-oap-server:10.4.0
container_name: ecommerce-skywalking-oap
environment:
SW_STORAGE: elasticsearch
SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
ports:
- "11800:11800"
- "12800:12800"
depends_on:
- elasticsearch
networks:
- ecommerce-net
skywalking-ui:
image: apache/skywalking-ui:10.4.0
container_name: ecommerce-skywalking-ui
environment:
SW_OAP_ADDRESS: http://skywalking-oap:12800
ports:
- "8088:8080"
depends_on:
- skywalking-oap
networks:
- ecommerce-net
elasticsearch:
image: elasticsearch:8.15.0
container_name: ecommerce-elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms2g -Xmx2g
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
networks:
- ecommerce-net
logstash:
image: logstash:8.15.0
container_name: ecommerce-logstash
volumes:
- ./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "5044:5044"
depends_on:
- elasticsearch
networks:
- ecommerce-net
kibana:
image: kibana:8.15.0
container_name: ecommerce-kibana
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- ecommerce-net
volumes:
mysql-data:
es-data:
networks:
ecommerce-net:
driver: bridge
4.4 微服务编排文件
创建docker/docker-compose-app.yml:
yaml
version: '3.8'
services:
gateway-server:
image: ${REGISTRY}/gateway-server:${IMAGE_TAG}
container_name: ecommerce-gateway
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_HOST=${NACOS_HOST}
- NACOS_PORT=${NACOS_PORT}
- NACOS_USERNAME=${NACOS_USERNAME}
- NACOS_PASSWORD=${NACOS_PASSWORD}
- JAVA_OPTS=-Xms512m -Xmx1024m
ports:
- "${GATEWAY_PORT}:9000"
volumes:
- ./logs/gateway:/app/logs
depends_on:
- nacos
- seata-server
networks:
- ecommerce-net
service-user:
image: ${REGISTRY}/service-user:${IMAGE_TAG}
container_name: ecommerce-user
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_HOST=${NACOS_HOST}
- NACOS_PORT=${NACOS_PORT}
- NACOS_USERNAME=${NACOS_USERNAME}
- NACOS_PASSWORD=${NACOS_PASSWORD}
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- SEATA_HOST=${SEATA_HOST}
- SEATA_PORT=${SEATA_PORT}
- JAVA_OPTS=-Xms512m -Xmx1024m
ports:
- "${USER_PORT}:8081"
volumes:
- ./logs/user:/app/logs
depends_on:
- nacos
- seata-server
- mysql
networks:
- ecommerce-net
service-product:
image: ${REGISTRY}/service-product:${IMAGE_TAG}
container_name: ecommerce-product
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_HOST=${NACOS_HOST}
- NACOS_PORT=${NACOS_PORT}
- NACOS_USERNAME=${NACOS_USERNAME}
- NACOS_PASSWORD=${NACOS_PASSWORD}
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- SEATA_HOST=${SEATA_HOST}
- SEATA_PORT=${SEATA_PORT}
- ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
- JAVA_OPTS=-Xms512m -Xmx1024m
ports:
- "${PRODUCT_PORT}:8082"
volumes:
- ./logs/product:/app/logs
depends_on:
- nacos
- seata-server
- mysql
- rocketmq-namesrv
networks:
- ecommerce-net
service-order:
image: ${REGISTRY}/service-order:${IMAGE_TAG}
container_name: ecommerce-order
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_HOST=${NACOS_HOST}
- NACOS_PORT=${NACOS_PORT}
- NACOS_USERNAME=${NACOS_USERNAME}
- NACOS_PASSWORD=${NACOS_PASSWORD}
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- SEATA_HOST=${SEATA_HOST}
- SEATA_PORT=${SEATA_PORT}
- ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
- JAVA_OPTS=-Xms512m -Xmx1024m
ports:
- "${ORDER_PORT}:8083"
volumes:
- ./logs/order:/app/logs
depends_on:
- nacos
- seata-server
- mysql
- rocketmq-namesrv
networks:
- ecommerce-net
service-payment:
image: ${REGISTRY}/service-payment:${IMAGE_TAG}
container_name: ecommerce-payment
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_HOST=${NACOS_HOST}
- NACOS_PORT=${NACOS_PORT}
- NACOS_USERNAME=${NACOS_USERNAME}
- NACOS_PASSWORD=${NACOS_PASSWORD}
- MYSQL_HOST=${MYSQL_HOST}
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- SEATA_HOST=${SEATA_HOST}
- SEATA_PORT=${SEATA_PORT}
- ROCKETMQ_NAMESRV=${ROCKETMQ_NAMESRV}
- JAVA_OPTS=-Xms512m -Xmx1024m
ports:
- "${PAYMENT_PORT}:8084"
volumes:
- ./logs/payment:/app/logs
depends_on:
- nacos
- seata-server
- mysql
networks:
- ecommerce-net
networks:
ecommerce-net:
external: true
name: ecommerce-net
4.5 环境变量覆盖配置
在application-prod.yml中,所有依赖地址都通过环境变量注入:
yaml
spring:
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
config:
server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
datasource:
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${MYSQL_ROOT_PASSWORD:root}
seata:
service:
grouplist:
default: ${SEATA_HOST:localhost}:${SEATA_PORT:8091}
rocketmq:
name-server: ${ROCKETMQ_NAMESRV:localhost:9876}
环境变量优先级 :环境变量 > application-prod.yml中的默认值。这样既保证了本地开发的便利性,又实现了生产环境的灵活配置。
五、日志挂载与生产启动脚本
5.1 日志挂载策略
容器中的日志默认存储在容器内部,容器删除后日志丢失。生产环境需要将日志挂载到宿主机,便于ELK采集和问题排查:
yaml
volumes:
- ./logs/order:/app/logs
日志目录结构:
docker/
├── logs/
│ ├── gateway/
│ │ └── gateway-server.log
│ ├── user/
│ │ └── service-user.log
│ ├── product/
│ │ └── service-product.log
│ ├── order/
│ │ └── service-order.log
│ └── payment/
│ └── service-payment.log
Filebeat采集配置(在宿主机上安装Filebeat):
yaml
filebeat.inputs:
- type: log
enabled: true
paths:
- /opt/ecommerce/docker/logs/*/*.log
fields:
env: prod
multiline:
pattern: '^\d{4}-\d{2}-\d{2}'
negate: true
match: after
output.logstash:
hosts: ["localhost:5044"]
5.2 生产启动脚本
创建docker/start.sh,实现一键启动全部服务:
bash
#!/bin/bash
set -e
COMPOSE_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$COMPOSE_DIR"
echo "=========================================="
echo " ecommerce-platform 生产环境启动脚本"
echo "=========================================="
# 1. 创建网络(如果不存在)
if ! docker network inspect ecommerce-net >/dev/null 2>&1; then
echo "[1/6] 创建 Docker 网络..."
docker network create ecommerce-net
else
echo "[1/6] Docker 网络已存在,跳过"
fi
# 2. 创建日志目录
echo "[2/6] 创建日志目录..."
mkdir -p logs/{gateway,user,product,order,payment}
# 3. 启动基础设施
echo "[3/6] 启动基础设施(MySQL、Nacos、Seata、RocketMQ 等)..."
docker compose -f docker-compose-infra.yml --env-file .env up -d
# 4. 等待基础设施就绪
echo "[4/6] 等待基础设施就绪(60秒)..."
sleep 60
# 5. 检查基础设施健康状态
echo "[5/6] 检查基础设施状态..."
docker compose -f docker-compose-infra.yml ps
# 6. 启动微服务
echo "[6/6] 启动微服务..."
docker compose -f docker-compose-app.yml --env-file .env up -d
echo "=========================================="
echo " 全部服务启动完成!"
echo " 网关地址: http://localhost:${GATEWAY_PORT:-9000}"
echo " Nacos: http://localhost:8080"
echo " Grafana: http://localhost:3000"
echo " SkyWalking: http://localhost:8088"
echo " Kibana: http://localhost:5601"
echo "=========================================="
停止脚本 docker/stop.sh:
bash
#!/bin/bash
cd "$(dirname "$0")"
echo "停止微服务..."
docker compose -f docker-compose-app.yml --env-file .env down
echo "停止基础设施..."
docker compose -f docker-compose-infra.yml --env-file .env down
echo "全部服务已停止"
六、线上运维配置
6.1 资源限制与重启策略
在docker-compose-app.yml中为每个服务添加资源限制和重启策略:
yaml
services:
service-order:
# ... 其他配置
deploy:
resources:
limits:
cpus: '2'
memory: 1536M
reservations:
cpus: '0.5'
memory: 512M
restart: unless-stopped
资源限制建议:
| 服务 | CPU限制 | 内存限制 | 说明 |
|---|---|---|---|
| gateway-server | 2 | 1.5G | 网关需要处理高并发 |
| service-order | 2 | 1.5G | 核心业务,堆内存1G |
| service-user | 1 | 1G | 查询为主 |
| service-product | 2 | 1.5G | 涉及库存扣减 |
| service-payment | 1 | 1G | 支付回调 |
重启策略:
| 策略 | 含义 | 适用场景 |
|---|---|---|
no |
不自动重启 | 开发环境 |
always |
总是重启 | 关键服务 |
unless-stopped |
除非手动停止,否则重启 | 生产环境推荐 |
on-failure |
失败时重启 | 非关键服务 |
6.2 健康检查与依赖顺序
Docker Compose的depends_on只保证启动顺序 ,不保证依赖服务已就绪 。例如,service-order依赖MySQL,但MySQL启动后可能需要10秒才能接受连接。如果service-order在MySQL就绪前启动,会因连接失败而崩溃。
解决方案:使用healthcheck + condition: service_healthy:
yaml
services:
mysql:
# ...
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
service-order:
# ...
depends_on:
mysql:
condition: service_healthy
nacos:
condition: service_started
6.3 滚动更新与回滚
滚动更新:在不中断服务的情况下,逐个替换旧版本实例。
bash
# 1. 构建新版本镜像
mvn clean package jib:build -pl service-order -Dimage.tag=1.0.1
# 2. 修改 .env 中的 IMAGE_TAG=1.0.1
# 3. 滚动更新 service-order(只更新该服务)
docker compose -f docker-compose-app.yml --env-file .env up -d --no-deps service-order
回滚 :将IMAGE_TAG改回旧版本,重新执行up -d即可。
bash
# 回滚到 1.0.0
sed -i 's/IMAGE_TAG=1.0.1/IMAGE_TAG=1.0.0/' .env
docker compose -f docker-compose-app.yml --env-file .env up -d --no-deps service-order
6.4 生产环境安全加固
| 加固项 | 措施 |
|---|---|
| 非root用户 | Dockerfile中创建appuser并以非root运行 |
| 镜像扫描 | 使用Trivy扫描镜像漏洞:trivy image registry.example.com/ecommerce/service-order:1.0.0 |
| 网络隔离 | 微服务只在内部网络通信,仅网关暴露端口 |
| 敏感信息 | 数据库密码通过环境变量注入,不写入镜像 |
| 镜像仓库认证 | 使用私有镜像仓库,配置认证 |
| Actuator安全 | 生产环境只暴露health、info、prometheus端点 |
七、踩坑指南
坑一:容器内无法解析localhost
现象 :服务启动时报Connection refused,配置的localhost:8848无法连接Nacos。
原因 :容器内的localhost指向容器自身,而非宿主机或其他容器。容器间通信必须使用服务名 (Docker Compose中定义的服务名)或自定义网络。
解决 :将配置中的localhost替换为Docker Compose中的服务名(如nacos、mysql、seata-server)。
坑二:MySQL初始化脚本未执行
现象:数据库表不存在,服务启动失败。
原因 :docker-entrypoint-initdb.d中的脚本只在数据卷为空时执行 。如果mysql-data卷已存在,脚本不会重新执行。
解决 :删除旧数据卷(docker volume rm ecommerce-platform_mysql-data)后重新启动;或手动进入容器执行初始化脚本。
坑三:时区不正确导致日志时间偏差8小时
现象:容器日志中的时间比北京时间早8小时。
原因:容器默认使用UTC时区。
解决 :在Dockerfile中设置ENV TZ=Asia/Shanghai并创建时区链接;或在Compose中挂载/etc/localtime:
yaml
volumes:
- /etc/localtime:/etc/localtime:ro
坑四:JVM内存未限制导致容器被OOM Killer杀死
现象 :容器突然退出,docker logs中显示Killed。
原因:JVM默认使用宿主机内存的1/4作为堆内存,容器内存限制为1G时,JVM可能尝试使用超过1G的内存,触发OOM Killer。
解决 :显式设置-Xmx小于容器内存限制(如容器1.5G,JVM堆设1G);添加-XX:+UseContainerSupport(JDK 21默认开启)让JVM感知容器限制。
坑五:健康检查失败导致容器反复重启
现象:容器启动后不断重启,日志显示健康检查失败。
原因 :HEALTHCHECK中的curl命令在容器中不存在(基础镜像未安装curl)。
解决 :使用wget替代curl,或安装curl;或使用Actuator的健康端点配合wget:
dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD wget -qO- http://localhost:8083/actuator/health/liveness || exit 1
坑六:镜像构建时Maven依赖下载慢
现象 :Docker构建时卡在mvn dependency:go-offline,下载依赖耗时过长。
原因:Maven默认从中央仓库下载,网络慢。
解决 :在Docker构建阶段配置阿里云镜像。在settings.xml中配置镜像,并在Dockerfile中复制:
dockerfile
COPY settings.xml /root/.m2/settings.xml
八、课后作业
作业一 :为service-order编写生产级Dockerfile(多阶段构建),构建镜像并验证镜像体积小于300MB。
作业二:使用Docker Compose编排MySQL、Nacos、Seata三个基础设施,验证三者可以正常启动并互通。
作业三 :将ecommerce-platform的5个微服务打包为Docker镜像,使用Docker Compose编排全部服务。通过网关发起下单请求,验证全链路贯通。
作业四:配置日志挂载,将容器日志输出到宿主机。使用Filebeat采集日志到ELK,在Kibana中验证日志可检索。
作业五(进阶) :编写滚动更新脚本,实现service-order从1.0.0到1.0.1的滚动更新,验证更新过程中服务不中断。然后回滚到1.0.0,验证回滚流程。
九、下节预告
第35课将进入SpringCloud2025架构复盘、性能调优 & 面试核心总结。内容包括全套架构优缺点复盘、高频性能瓶颈优化、GC优化、接口调优、集群优化、SpringCloud高频面试题、生产避坑大全和进阶学习路线。本课完成了微服务项目的容器化部署和生产上线,第35课将对整个专栏的技术体系进行复盘,提炼面试核心考点,为你的微服务架构师之路画上句号。
🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航
第一部分:微服务前置基础 & 新版环境搭建(第1-5课)
第二部分:注册中心核心(Nacos 最新版)(第6-9课)
第三部分:配置中心核心(Nacos配置中心)(第10-12课)
第四部分:服务通信核心(OpenFeign + LoadBalancer)(第13-16课)
第五部分:网关核心(SpringCloud Gateway 新版)(第17-20课)
第六部分:熔断、限流、降级(Sentinel 新版)(第21-24课)
第七部分:微服务监控、链路追踪、日志体系(第25-28课)
第八部分:微服务高阶特性 & 分布式核心能力(第29-31课)
第九部分:企业级完整项目实战 & 架构复盘(第32-35课)