Mac/Linux 使用 Docker Jenkins 自动部署 Spring Boot 后端
Mac/Linux 使用 Docker Jenkins 自动部署 Spring Boot 后端
本文以项目名称 joy-admin-backend 为例,搭建一套可重复使用的 Jenkins 自动部署流程,包含:
- Docker 部署 Jenkins
- Jenkins 通过 SSH 拉取阿里云 Codeup 代码
- JDK 21、Maven 3.9.9 构建
- Docker Compose 发布 Spring Boot 后端
- 版本化 Docker 镜像
- 健康检查
- 发布失败自动回滚
- 手动回滚历史版本
后端仓库:
git@codeup.aliyun.com:xxxxx/xxxxx.git
一、整体架构
Codeup
↓ SSH
Docker Jenkins
↓ docker compose build
joy-admin-backend:构建号-GitCommit
↓ docker compose up
后端容器
↓ 健康检查失败
自动恢复上一个 Docker 镜像
代码每次从 Codeup 拉取,环境配置保存在 Jenkins Credentials,不依赖本机已有项目。
二、环境要求
推荐配置:
Docker Desktop / Docker Engine
Docker Compose 2.20+
Jenkins LTS + JDK 21
Maven 3.9.9
至少 4 核 CPU
至少 6 GB 内存
至少 50 GB 可用磁盘
在宿主机检查环境:
1. 检查机器环境
执行:
uname -m
sw_vers
sysctl -n hw.memsize
df -h /
uname -m 输出 arm64:Apple Silicon 或 x86_64:Intel
2. 检查Docker
先启动Docker Desktop,然后执行:
docker version
docker compose version
docker info --format 'OS={{.OSType}}, Arch={{.Architecture}}, CPUs={{.NCPU}}, Memory={{.MemTotal}}'
docker run --rm hello-world
要求:
docker version同时显示Client和Server。
docker compose version正常输出。
hello-world运行成功。
Docker运行Linux容器。
3. 检查构建镜像
docker run --rm eclipse-temurin:21-jdk java -version
docker run --rm \
maven:3.9.9-eclipse-temurin-21 \
mvn -version
docker run --rm \
node:16.20.2-bullseye \
sh -c 'node -v && npm -v'
预期输出:
Java 21
Apache Maven 3.9.9
Node v16.20.2
npm 8.x
4. 检查Maven仓库挂载
先确认目录存在:
ls -ld /Users/mac/Documents/work/repo
测试容器读取:
docker run --rm \
-v /Users/mac/Documents/work/repo:/maven-repo:ro \
alpine:3.20 \
sh -c 'test -d /maven-repo && ls -la /maven-repo | head'
如果出现Mounts denied,在Docker Desktop中打开:
Settings
→ Resources
→ File Sharing
加入:
/Users/mac/Documents/work
然后Apply并重启Docker Desktop。
5. 检查端口
lsof -nP -iTCP:8080 -sTCP:LISTEN
lsof -nP -iTCP:1024 -sTCP:LISTEN
lsof -nP -iTCP:9080 -sTCP:LISTEN
规划:
8080:Jenkins
1024:Joy Admin前端
9080:后端,仅在需要本机调试时映射
没有输出代表端口空闲。如果8080已占用,后面改用18080。
三、配置 Codeup SSH
1. 生成 Jenkins 专用密钥
在宿主机执行:
ssh-keygen \
-t ed25519 \
-C "jenkins-codeup" \
-f ~/.ssh/joy-admin-jenkins-codeup
密钥位置:
~/.ssh/joy-admin-jenkins-codeup 私钥
~/.ssh/joy-admin-jenkins-codeup.pub 公钥
登陆Aliyun Codeup 的个人设置 》添加 SSH 公钥,将 .pub 公钥内容添加进去。
2. 验证 Codeup 主机指纹(机子能否链接到Codeup拉取代码)
Codeup 返回的是 RSA 主机密钥:
ssh-keyscan -T 10 -t rsa codeup.aliyun.com \
> /tmp/codeup_known_hosts
ssh-keygen -lf /tmp/codeup_known_hosts
返回下面官方 SHA256 指纹就说明是正常的:
SHA256:yEGmgQNVrc3QAvDvoBrTxxxxxxx+AbWi9vSt/fE
验证访问codeup:
ssh -T \
-i ~/.ssh/joy-admin-jenkins-codeup \
-o IdentitiesOnly=yes \
git@codeup.aliyun.com
出现 Welcome to Codeup 表示成功。
验证访问自己的仓库:
GIT_SSH_COMMAND='ssh -i ~/.ssh/joy-admin-jenkins-codeup -o IdentitiesOnly=yes' \
git ls-remote \
git@codeup.aliyun.com:xxxxxx/xxxxxx.git \
refs/heads/main
出现类似 9e7f396c0226cfa7cfebefeeebcef341752cb58 refs/heads/main 表示成功。
以上确认完成后,加入本机信任列表:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
ssh-keygen -F codeup.aliyun.com >/dev/null \
|| cat /tmp/codeup_known_hosts >> ~/.ssh/known_hosts
四、创建 Jenkins Docker 环境
目录结构预览:
joy-admin-deploy/
├── .env
├── compose.jenkins.yml
└── jenkins/
├── Dockerfile
└── known_hosts
创建上面的预览目录结构:
mkdir -p /Users/mac/Documents/work/joy-admin-deploy/jenkins
cd /Users/mac/Documents/work/joy-admin-deploy
cp /tmp/codeup_known_hosts jenkins/known_hosts
chmod 644 jenkins/known_hosts
1. 创建Jenkins镜像Jenkins Dockerfile
可以使用命令创建
nano jenkins/Dockerfile
写入:
FROM maven:3.9.9-eclipse-temurin-21 AS maven_tool
FROM node:16.20.2-bullseye AS node_tool
FROM jenkins/jenkins:lts-jdk21
USER root
RUN printf '%s\n' \
'Acquire::Retries "5";' \
'Acquire::http::Timeout "60";' \
'Acquire::https::Timeout "60";' \
> /etc/apt/apt.conf.d/80-network-retries \
&& sed -i \
's|http://deb.debian.org|https://deb.debian.org|g' \
/etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl gnupg git openssh-client rsync \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl --retry 5 --retry-delay 3 --retry-all-errors \
-fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
docker-ce-cli \
docker-buildx-plugin \
docker-compose-plugin \
&& rm -rf /var/lib/apt/lists/*
COPY --from=maven_tool /usr/share/maven /usr/share/maven
COPY --from=node_tool /usr/local /usr/local
ENV MAVEN_HOME=/usr/share/maven
ENV PATH=/usr/share/maven/bin:/usr/local/bin:${PATH}
RUN mkdir -p /root/.ssh \
&& chmod 700 /root/.ssh \
&& java -version \
&& mvn -version \
&& node -v \
&& npm -v \
&& docker --version \
&& docker buildx version \
&& docker compose version
RUN jenkins-plugin-cli --plugins \
workflow-aggregator \
pipeline-stage-view \
git \
ssh-credentials \
ssh-agent \
credentials-binding \
docker-workflow \
timestamper \
ws-cleanup
USER root
docker-buildx-plugin不能省略,因为目前我的后端项目Dockerfile配置了
RUN --mount=type=cache,target=/root/.m2
该语法只能通过 BuildKit 执行。
2. Jenkins Compose
创建 compose.jenkins.yml:
services:
jenkins:
build:
context: ./jenkins
dockerfile: Dockerfile
image: joy-jenkins:lts-jdk21
container_name: joy-jenkins
user: root
restart: unless-stopped
ports:
- "${JENKINS_BIND_ADDRESS}:${JENKINS_HTTP_PORT}:8080"
environment:
TZ: UTC
JAVA_OPTS: >-
-Duser.timezone=UTC
-Djenkins.install.runSetupWizard=true
volumes:
- jenkins_home:/var/jenkins_home
- jenkins_npm_cache:/root/.npm
- "${HOST_MAVEN_REPO}:/maven-repo"
- /var/run/docker.sock:/var/run/docker.sock
- ./jenkins/known_hosts:/root/.ssh/known_hosts:ro
volumes:
jenkins_home:
name: joy_jenkins_home
jenkins_npm_cache:
name: joy_jenkins_npm_cache
创建 .env:
JENKINS_BIND_ADDRESS=127.0.0.1
JENKINS_HTTP_PORT=8080
HOST_MAVEN_REPO=/Users/mac/Documents/work/repo
在其他机器构建时只需要修改 HOST_MAVEN_REPO。
3. 构建并启动 Jenkins
cd /Users/mac/Documents/work/joy-admin-deploy
构建(如果compose.jenkins.yml或者Dockfile有改动,这个命令也可以重新构建):
docker compose \
--env-file .env \
-f compose.jenkins.yml \
build --pull
启动:
docker compose \
--env-file .env \
-f compose.jenkins.yml \
up -d
检查状态:
docker compose \
--env-file .env \
-f compose.jenkins.yml \
ps
docker logs --tail 100 joy-jenkins
获取初始化密码:
docker exec joy-jenkins \
cat /var/jenkins_home/secrets/initialAdminPassword
打开:
http://127.0.0.1:8080
完成管理员创建和推荐插件安装。
验证工具:
docker exec joy-jenkins bash -lc '
java -version
mvn -version
node -v
npm -v
git --version
docker version
docker buildx version
docker compose version
'
验证Jenkins中的主机指纹:
docker exec joy-jenkins \
ssh-keygen -lf /root/.ssh/known_hosts
输出必须包含类似:
SHA256:yEGmgQNVrc3QAvDvoBrTCF2s07Kwxxxxxx/fE
五、配置 Jenkins 凭据
http://127.0.0.1:8080先登陆Jenkins
1. Codeup 私钥凭据
进入:
Manage Jenkins
→ Credentials
→ System
→ Global credentials
→ Add Credentials
填写:
Kind:SSH Username with private key
Username:git
#可以命令行 pbcopy < ~/.ssh/joy-admin-jenkins-codeup 复制内容
Private Key:粘贴不带 .pub 后缀的私钥
ID:codeup-ssh
Description:Codeup只读拉取凭据
注意:
joy-admin-jenkins-codeup 私钥,可以填入Jenkins
joy-admin-jenkins-codeup.pub 公钥,不能填入Jenkins
如果错误粘贴公钥,会出现:
Load key: error in libcrypto
Permission denied (publickey)
2. 后端环境配置凭据
根据实际生成 .env
关键配置示例(以下内容是我的后端项目里面已有的内容,我就直接使用了,你们根据自己的配置来生成 .env内容):
# Docker image and host binding
JOY_ADMIN_IMAGE=joy-admin-backend
JOY_ADMIN_IMAGE_TAG=latest
JOY_ADMIN_BIND_ADDRESS=0.0.0.0
JOY_ADMIN_HOST_PORT=9080
# Docker宿主机公共工作根目录
HOST_MACHINE_ROOT=/Users/mac/Documents/work/joy-admin-backend-deploy
# Docker公共工作根目录
DOCKER_ROOT=/app/work/joy-admin-backend-deploy
# JVM. Adjust these percentages to the server capacity.
JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=25.0 -XX:+ExitOnOutOfMemoryError -Djava.io.tmpdir=${DOCKER_ROOT}/tmp
# Business calendar. JVM/database timestamps remain UTC; business dates use this zone.
BUSINESS_TIME_ZONE=Asia/Kolkata
# Remote PostgreSQL. Use a private IP or internal DNS name.
# For TLS, use sslmode=require or preferably verify-full with the required certificates.
DB_URL=jdbc:postgresql://192.168.1.114:5432/onevone?sslmode=disable&connectTimeout=10&socketTimeout=120&tcpKeepAlive=true
DB_USER=onevone
DB_PASSWORD=onevone
DB_POOL_INITIAL_SIZE=5
DB_POOL_MIN_IDLE=10
DB_POOL_MAX_ACTIVE=20
# Remote Redis. Never expose Redis directly to the public internet.
REDIS_HOST=host.docker.internal
REDIS_PORT=6379
REDIS_DATABASE=3
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_SSL=false
REDIS_TIMEOUT=10s
# Quartz uses the PostgreSQL connection above.
QUARTZ_INSTANCE_NAME=QuartzScheduler-admin-production
QUARTZ_CLUSTERED=true
QUARTZ_THREAD_COUNT=10
QUARTZ_DB_MAX_CONNECTIONS=10
# Application security. Generate a long random value for production.
TOKEN_SECRET=replace_with_a_long_random_secret
TOKEN_EXPIRE_MINUTES=43200
# Upstream service called by joy-admin.
UPSTREAM_SERVICE_URL=http://localhost:9080
UPSTREAM_SERVICE_SIGNATURE=replace_me
# Optional external integrations. Leave blank only when the feature is unused.
OSS_PREFIX_CDN=
OSS_PREFIX_OSS=
OSS_PREFIX_HTTP=
OSS_BUCKET_NAME=
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ENDPOINT=
FEISHU_WEBHOOK_URL=
# Production switches
SWAGGER_ENABLED=false
DRUID_CONSOLE_ENABLED=false
DRUID_CONSOLE_USERNAME=
DRUID_CONSOLE_PASSWORD=
# 开放域名CORS跨域
CORS_ALLOWED_ORIGINS=https://xxx.com
# Docker宿主机持久化目录
# JOY_ADMIN_LOG_DIR:Docker宿主机路径
JOY_ADMIN_DATA_DIR=${HOST_MACHINE_ROOT}/data
JOY_ADMIN_LOG_DIR=${HOST_MACHINE_ROOT}/logs
# Docker持久化目录
# DOCKER_LOG_DIR:Docker路径
DOCKER_DATA_DIR=${DOCKER_ROOT}/data
DOCKER_LOG_DIR=${DOCKER_ROOT}/logs
# Docker容器内部日志路径
# APP_LOG_FILE:容器内部Linux路径
APP_LOG_FILE=${DOCKER_LOG_DIR}/joy-admin.log
APP_LOG_FILE_PATTERN=${DOCKER_LOG_DIR}/archive/joy-admin.%d{yyyy-MM-dd}.%i.log.gz
APP_LOG_LEVEL=info
APP_LOG_MAX_FILE_SIZE=100MB
APP_LOG_MAX_HISTORY=30
APP_LOG_TOTAL_SIZE_CAP=3GB
如果数据库或 Redis 运行在 Docker 宿主机,容器内地址使用:
host.docker.internal
创建持久化目录:
mkdir -p /Users/mac/Documents/work/joy-admin-backend-deploy/data
将 .env 上传到 Jenkins:
Kind:Secret file
File:选择准备好的.env
ID:joy-admin-backend-env
Description:Joy Admin后端运行配置
.env本地文件路径只在上传时使用,Jenkins 保存的是文件内容,流水线不会依赖上传路径。
六、创建后端 Pipeline
创建任务:
New Item
→ joy-admin-backend
→ Pipeline
Pipeline Script:
pipeline {
agent any
parameters {
choice(
name: 'ACTION',
choices: ['DEPLOY', 'ROLLBACK'],
description: '发布新版本或回滚旧版本'
)
string(
name: 'GIT_BRANCH',
defaultValue: 'main',
description: '构建分支'
)
string(
name: 'ROLLBACK_TAG',
defaultValue: '',
description: '回滚标签,例如15-a8c37f21'
)
}
options {
timestamps()
timeout(time: 60, unit: 'MINUTES')
disableConcurrentBuilds()
skipDefaultCheckout(true)
buildDiscarder(logRotator(numToKeepStr: '30'))
}
environment {
IMAGE_NAME = 'joy-admin-backend'
DOCKER_BUILDKIT = '1'
TZ = 'UTC'
}
stages {
stage('拉取代码') {
steps {
deleteDir()
sshagent(credentials: ['codeup-ssh']) {
sh '''
set -eu
export GIT_SSH_COMMAND="ssh \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile=/root/.ssh/known_hosts"
git clone \
--depth 1 \
--branch "$GIT_BRANCH" \
git@codeup.aliyun.com:xxxxx/xxxxx.git \
backend
'''
}
script {
env.GIT_SHORT = sh(
script: 'git -C backend rev-parse --short=8 HEAD',
returnStdout: true
).trim()
env.NEW_TAG = "${env.BUILD_NUMBER}-${env.GIT_SHORT}"
}
}
}
stage('准备环境配置') {
steps {
withCredentials([
file(
credentialsId: 'joy-admin-backend-env',
variable: 'BACKEND_ENV_FILE'
)
]) {
sh '''
set -eu
install -m 600 "$BACKEND_ENV_FILE" backend/.env
'''
}
}
}
stage('执行发布或回滚') {
steps {
dir('backend') {
script {
if (params.ACTION == 'ROLLBACK') {
if (!params.ROLLBACK_TAG.trim()) {
error('回滚必须填写ROLLBACK_TAG')
}
env.TARGET_TAG = params.ROLLBACK_TAG.trim()
sh '''
set -eu
export JOY_ADMIN_IMAGE="$IMAGE_NAME"
export JOY_ADMIN_IMAGE_TAG="$TARGET_TAG"
docker image inspect \
"$IMAGE_NAME:$TARGET_TAG" >/dev/null
docker compose up \
-d --no-build --force-recreate \
--wait --wait-timeout 300 \
joy-admin
CONTAINER_ID="$(docker compose ps -q joy-admin)"
docker exec "$CONTAINER_ID" \
curl --fail --silent \
http://127.0.0.1:9080/ >/dev/null
echo "回滚成功:$IMAGE_NAME:$TARGET_TAG"
'''
} else {
env.TARGET_TAG = env.NEW_TAG
sh '''
set -u
export JOY_ADMIN_IMAGE="$IMAGE_NAME"
export JOY_ADMIN_IMAGE_TAG="$TARGET_TAG"
CURRENT_ID="$(
docker compose ps -q joy-admin 2>/dev/null || true
)"
PREVIOUS_IMAGE=""
if [ -n "$CURRENT_ID" ]; then
PREVIOUS_IMAGE="$(
docker inspect \
--format '{{.Config.Image}}' \
"$CURRENT_ID"
)"
fi
docker compose config >/dev/null || exit 1
docker compose build --pull joy-admin || exit 1
docker image inspect \
"$IMAGE_NAME:$TARGET_TAG" >/dev/null || exit 1
DEPLOY_OK=false
if docker compose up \
-d --no-build \
--wait --wait-timeout 300 \
joy-admin
then
CONTAINER_ID="$(
docker compose ps -q joy-admin
)"
if docker exec "$CONTAINER_ID" \
curl --fail --silent \
http://127.0.0.1:9080/ >/dev/null
then
DEPLOY_OK=true
fi
fi
if [ "$DEPLOY_OK" = "true" ]; then
echo "发布成功:$IMAGE_NAME:$TARGET_TAG"
docker inspect \
--format='镜像={{.Config.Image}} 健康={{.State.Health.Status}}' \
"$CONTAINER_ID"
exit 0
fi
echo "新版本启动失败"
docker compose logs --tail=200 joy-admin || true
if [ -z "$PREVIOUS_IMAGE" ]; then
echo "首次发布,没有旧版本可回滚"
exit 1
fi
if ! docker image inspect \
"$PREVIOUS_IMAGE" >/dev/null 2>&1
then
echo "旧镜像不存在:$PREVIOUS_IMAGE"
exit 1
fi
PREVIOUS_TAG="${PREVIOUS_IMAGE##*:}"
export JOY_ADMIN_IMAGE_TAG="$PREVIOUS_TAG"
if docker compose up \
-d --no-build --force-recreate \
--wait --wait-timeout 300 \
joy-admin
then
echo "已自动回滚:$PREVIOUS_IMAGE"
else
echo "自动回滚失败,需要人工处理"
fi
exit 1
'''
}
}
}
}
}
}
post {
always {
sh 'rm -f backend/.env 2>/dev/null || true'
deleteDir()
}
}
}
七、执行第一次发布
选择:
Build with Parameters
填写:
ACTION=DEPLOY
GIT_BRANCH=main
ROLLBACK_TAG=留空
每次发布生成唯一镜像:
joy-admin-backend:构建号-Git短Commit
例如:
joy-admin-backend:7-9c0226cf
joy-admin-backend:8-a321bc42
八、验证发布
docker ps --filter name=joy-admin-backend
docker images joy-admin-backend
curl http://127.0.0.1:9080/
查看当前运行镜像:
docker inspect \
--format '{{.Config.Image}}' \
"$(docker ps -q --filter name=joy-admin-backend)"
查看日志:
docker logs \
--tail 200 \
"$(docker ps -q --filter name=joy-admin-backend)"
九、执行回滚
查看历史镜像:
docker images joy-admin-backend
比如返回所有镜像:
joy-admin-backend 15-a8c37f21
joy-admin-backend 16-d529bf10
登陆Jenkins,再次选择Build with Parameters,参数界面填入:
ACTION=ROLLBACK
GIT_BRANCH=main
ROLLBACK_TAG=需要恢复的镜像标签,如 15-a8c37f21
回滚使用已有镜像,不重新编译。
发布新版本健康检查失败时,流水线会自动恢复发布前正在运行的镜像。
十、常见问题
1. ssh-keyscan提示不是公钥文件
错误:
(stdin) is not a public key file
原因是 Codeup 没有提供 ED25519 主机密钥,只提供 RSA:
ssh-keyscan -T 10 -t rsa codeup.aliyun.com
2. Jenkins拉取代码报libcrypto错误
错误:
Load key: error in libcrypto
Permission denied (publickey)
检查是否错误上传了 .pub 公钥。Jenkins需要不带.pub后缀的私钥。
3. Debian下载返回502
错误:
502 Bad Gateway
Unable to fetch some archives
Jenkins Dockerfile中配置APT重试和HTTPS源;重新执行构建即可。
4. BuildKit不可用
错误:
Docker Compose requires buildx plugin
the --mount option requires BuildKit
Jenkins镜像必须安装:
docker-buildx-plugin
验证:
docker exec joy-jenkins docker buildx version
5. 容器无法连接宿主机数据库或Redis
容器中的127.0.0.1表示容器自己。Mac和Docker Desktop使用:
host.docker.internal
Linux通过现有Compose配置:
extra_hosts:
- "host.docker.internal:host-gateway"
6. 测试失败导致构建失败
目前我的后端Dockerfile使用:
-Dmaven.test.skip=true
生产镜像构建不会运行测试。测试应作为独立质量阶段,修复后再决定是否阻断发布。
十一、迁移到另一台机器
新机器需要:
- 安装Docker和Docker Compose。
- 创建同样的
joy-admin-deploy目录。 - 构建并启动Jenkins。
- 配置同样的
known_hosts。 - 配置凭据
codeup-ssh。 - 上传新机器对应环境的
.env,凭据ID仍为joy-admin-backend-env。 - 创建同样的Pipeline任务。
- 创建
JOY_ADMIN_DATA_DIR目录。 - 执行
ACTION=DEPLOY。
流水线不依赖本地项目目录,所有代码均从Codeup获取。
十二、安全和维护建议
.env禁止提交Git。- Jenkins不要直接暴露公网。
- Jenkins挂载Docker Socket后拥有宿主机Docker管理权限。
- 定期备份
joy_jenkins_home卷。 - 至少保留最近5至10个后端镜像。
- 不要随意执行
docker system prune -a(用于彻底清理 Docker 系统中未使用的资源,包括未被任何容器引用的镜像、停止的容器)。 - Docker镜像回滚不会回滚数据库。
- SQL变更应保持新旧应用版本兼容。
- 跨机器保留历史镜像时,需要增加Docker Registry。
查看Docker磁盘占用:
docker system df
官方参考:
更多推荐
所有评论(0)