【部署实战】Docker + AI Agent:让AI一键部署Spring Boot到服务器,从打包到上线只要一条指令
文章目录
写在前面
写过Spring Boot的都懂------开发一时爽,部署火葬场。
打完jar包忘了改配置文件的环境变量,上去了连不上数据库。改了代码忘了更新Docker镜像,上线后跑的还是旧版本。服务器磁盘满了没发现,部署到一半直接卡死。
这些破事每个开发者都遇到过。我们团队最近搞了一个方案:把Docker操作包装成MCP Tool,让AI Agent接手部署全流程------从打包到启动到健康检查,全部自动化,出错了还会自己回滚。
这篇文章完整拆解这套方案。环境:Spring Boot 3.3.0 + Docker 26 + MCP协议。服务器:Linux(CentOS 7/Ubuntu 22.04均可)。
一、手工部署到底有多烦
回忆一下你上次部署的流程。
本地 mvn clean package 打jar包。把jar包scp到服务器。修改application-prod.yml里的数据库地址。docker build 构建镜像。docker stop 停掉旧容器。docker run 启动新容器。docker logs -f 盯着日志看有没有报错。发现数据库连不上,查了半天发现是环境变量没设对。重新改了环境变量,再跑一遍。
整个过程20分钟起步,如果中间出问题,半小时一小时很正常。人还会漏步骤------忘了备份、忘了健康检查、忘了清理旧镜像导致磁盘满了。
AI Agent都能帮你干。
二、Dockerfile模板
dockerfile
FROM eclipse-temurin:21-jre-alpine
设置时区
RUN apk add --no-cache tzdata &&
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&
echo "Asia/Shanghai" > /etc/timezone
创建应用目录
WORKDIR /app
复制jar包
COPY target/*.jar app.jar
创建非root用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
JVM参数:限制堆内存、开启GC日志
ENTRYPOINT ["java",
"-Xms256m", "-Xmx512m",
"-XX:+UseG1GC",
"-XX:MaxGCPauseMillis=200",
"-Djava.security.egd=file:/dev/./urandom",
"-jar", "app.jar"]
docker-compose.yml:
yaml
version: '3.8'
services:
app:
build: .
container_name: A P P N A M E : − m y a p p p o r t s : − " {APP_NAME:-myapp} ports: - " APPNAME:−myappports:−"{APP_PORT:-8080}:8080"
environment:
-
SPRING_PROFILES_ACTIVE= S P R I N G P R O F I L E S A C T I V E : − p r o d − D B U R L = {SPRING_PROFILES_ACTIVE:-prod} - DB_URL= SPRINGPROFILESACTIVE:−prod−DBURL={DB_URL}
-
DB_USERNAME= D B U S E R N A M E − D B P A S S W O R D = {DB_USERNAME} - DB_PASSWORD= DBUSERNAME−DBPASSWORD={DB_PASSWORD}
-
REDIS_HOST=${REDIS_HOST:-redis}
volumes:
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: "CMD", "curl", "-f", "http://localhost:8080/actuator/health"
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
networks:
- app-network
networks:
app-network:
driver: bridge
三、部署操作Tool------给AI的操作接口
java
@Component
public class DockerDeployTool {
@Tool(description = "执行完整的部署流程:1.编译打包 2.构建Docker镜像 3.停止旧容器 4.启动新容器 5.健康检查。" +
"任何一步失败都会自动回滚到上一个可用版本。" +
"部署前确保.gitignore已配置,避免提交敏感文件")
public String deploy(
@ToolParam(description = "项目路径,如/home/app/myproject") String projectPath,
@ToolParam(description = "环境:dev/test/prod") String env,
@ToolParam(description = "是否跳过测试(紧急修复时可跳过)") boolean skipTests) {
StringBuilder log = new StringBuilder();
String oldContainerId = null;
try {
// 1. 编译打包
log.append(execCommand(projectPath,
skipTests ? "mvn clean package -DskipTests -q" : "mvn clean package -q"));
// 2. 备份当前运行的容器ID
oldContainerId = execCommand(projectPath,
"docker ps -q --filter name=myapp").trim();
// 3. 构建新镜像(带版本标签,方便回滚)
String version = String.valueOf(System.currentTimeMillis());
log.append(execCommand(projectPath,
"docker build -t myapp:" + version + " -t myapp:latest ."));
// 4. 停旧容器
if (!oldContainerId.isEmpty()) {
log.append(execCommand(projectPath, "docker stop " + oldContainerId));
}
// 5. 启动新容器
log.append(execCommand(projectPath,
"docker compose up -d"));
// 6. 等待健康检查
Thread.sleep(10000);
String health = execCommand(projectPath,
"docker inspect --format='{{.State.Health.Status}}' myapp").trim();
if (!"healthy".equals(health)) {
throw new RuntimeException("健康检查失败,状态:" + health);
}
log.append("✅ 部署成功!新版本:" + version);
log.append("✅ 健康检查通过");
// 7. 清理旧镜像(保留最近3个版本)
execCommand(projectPath,
"docker images myapp --format '{{.Tag}}' | sort -r | tail -n +4 | xargs -r docker rmi");
return log.toString();
} catch (Exception e) {
log.append("❌ 部署失败:" + e.getMessage());
// 自动回滚
if (oldContainerId != null && !oldContainerId.isEmpty()) {
log.append("\n🔄 正在回滚到上一个版本...");
execCommand(projectPath, "docker start " + oldContainerId);
log.append("✅ 已回滚");
}
return log.toString();
}
}
@Tool(description = "查看服务器磁盘和内存使用情况。" +
"Docker镜像和容器日志经常占满磁盘,需要定期清理。" +
"磁盘使用率超过80%时需要关注,超过90%需要紧急清理")
public String checkServerHealth() {
StringBuilder report = new StringBuilder("服务器健康报告:\n\n");
// 磁盘
report.append("【磁盘使用】\n");
report.append(execCommand(null, "df -h / /var/lib/docker | tail -n +2"));
// 内存
report.append("\n【内存使用】\n");
report.append(execCommand(null, "free -h"));
// Docker磁盘占用
report.append("\n【Docker磁盘占用】\n");
report.append(execCommand(null, "docker system df"));
return report.toString();
}
@Tool(description = "清理Docker垃圾:未使用的镜像、停止的容器、悬挂的卷、构建缓存。" +
"建议每周执行一次,避免磁盘被占满。" +
"清理前确认没有需要保留的旧版本")
public String cleanDocker() {
StringBuilder log = new StringBuilder("Docker清理报告:\n");
log.append("清理前:\n");
log.append(execCommand(null, "docker system df"));
log.append("\n开始清理...\n");
log.append(execCommand(null, "docker system prune -f --volumes"));
log.append("\n清理后:\n");
log.append(execCommand(null, "docker system df"));
return log.toString();
}
@Tool(description = "查看应用实时日志。支持过滤关键词(如ERROR、WARN)。" +
"tailLines控制返回最近多少行,避免日志量过大")
public String viewLogs(
@ToolParam(description = "过滤关键词,如ERROR/WARN/Exception,留空查看全部")
String keyword,
@ToolParam(description = "显示最近N行,建议20-100") int tailLines) {
String cmd = "docker logs myapp --tail " + tailLines;
if (keyword != null && !keyword.isEmpty()) {
cmd += " 2>&1 | grep -i " + keyword;
}
return execCommand(null, cmd);
}
@Tool(description = "回滚到指定版本。version为docker image的tag。")
public String rollback(
@ToolParam(description = "目标版本号(image tag)") String version) {
StringBuilder log = new StringBuilder();
log.append(execCommand(null, "docker stop myapp"));
log.append(execCommand(null, "docker rm myapp"));
log.append(execCommand(null, "docker run -d --name myapp -p 8080:8080 myapp:" + version));
return "已回滚到版本:" + version + "\n" + log.toString();
}
private String execCommand(String workingDir, String command) {
try {
ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c", command);
if (workingDir != null) {
pb.directory(new java.io.File(workingDir));
}
Process process = pb.start();
String output = new String(process.getInputStream().readAllBytes());
String error = new String(process.getErrorStream().readAllBytes());
process.waitFor(5, java.util.concurrent.TimeUnit.MINUTES);
return output + (error.isEmpty() ? "" : "\n" + error);
} catch (Exception e) {
return "命令执行失败:" + e.getMessage();
}
}
}
四、AI Agent的部署全流程
你只需要说一句话:"帮我把项目部署到测试环境。"
Agent自动执行:编译打包(跳过测试或全量跑取决于环境)、构建Docker镜像、备份当前运行版本、停止旧容器、启动新容器、等待健康检查、健康检查通过→清理旧镜像、健康检查失败→自动回滚。
整个过程你喝着咖啡看日志就行,出错了还能自动回滚。
五、定时巡检
java
@Component
public class ServerCronJob {
@Autowired
private DockerDeployTool deployTool;
@Scheduled(cron = "0 0 9 * * ?") // 每天早上9点
public void dailyHealthCheck() {
String health = deployTool.checkServerHealth();
// 如果磁盘使用率超80%,发告警
if (health.contains("80%") || health.contains("90%")) {
sendAlert("服务器磁盘告警", health);
}
}
@Scheduled(cron = "0 0 3 ? * SUN") // 每周日凌晨3点
public void weeklyCleanup() {
deployTool.cleanDocker();
}
}
六、踩坑记录
坑1:mvn package慢得离谱。 小项目还好,大项目一次打包两分钟。如果每次都全量打包,CI/CD流水线会被拖慢。解决方法:本地开发用dev profile,跳过不必要的插件。
坑2:docker build的层层缓存。 Dockerfile里COPY命令顺序错了,每次改代码都会让所有层重建。正确顺序:先COPY pom.xml → RUN mvn dependency → 再COPY src → RUN mvn package。
坑3:健康检查的start_period设太短。 Spring Boot启动到完全就绪可能需要30-60秒。如果start_period只设10秒,容器还没启动完就被判定为unhealthy然后又被重启,进入死亡循环。
yaml
healthcheck:
test: "CMD", "curl", "-f", "http://localhost:8080/actuator/health"
interval: 30s
timeout: 5s
retries: 5
start_period: 60s # ← 给够启动时间!
七、总结
这套方案把部署从"手动20分钟易出错"变成了"一句话指令自动完成还带回滚"。三个核心组件:Docker Tool包装操作命令、健康检查确保新版本可用、自动回滚保证不出生产事故。
适合所有用Docker部署Spring Boot应用的团队。个人开发者用这套也能省下大量运维时间------把精力放在写代码上,部署交给AI。
有用的话点赞收藏。下一篇《AI Agent + Elasticsearch:MCP协议实现智能全文搜索》。