1)gitea+postgres、jenkins软件安装
[root@localhost soft]# cat docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: gitea-postgres
restart: always
volumes:
- ./postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=gitea_prod_2026
- POSTGRES_DB=gitea
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
gitea:
image: gitea/gitea:1.22.3
container_name: gitea-prod
restart: always
depends_on:
postgres:
condition: service_healthy
volumes:
- ./gitea-data:/data
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "222:22"
environment:
# 时区
- TZ=Asia/Shanghai
# 🔥 核心:自动指定数据库为 Postgres 🔥
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=gitea_prod_2026
# 自动配置站点 URL(生产必须)
- GITEA__server__DOMAIN=192.168.3.203
- GITEA__server__ROOT_URL=http://192.168.3.203:3000
- GITEA__server__HTTP_PORT=3000
- GITEA__server__SSH_PORT=222
jenkins:
build:
context: ./jenkins
dockerfile: Dockerfile
image: my-jenkins:ansible-jdk21
container_name: jenkins-jdk21
restart: always
ports:
- "8080:8080"
volumes:
- ./jenkins-data:/var/jenkins_home
environment:
- TZ=Asia/Shanghai
user: root
[root@localhost soft]#
2)由于jenkins是安装在docker内的,jenkins所在机器内部需要执行ansible,所以需要python环境
所以这个jenkins软件还需要内部安装ansible和python
[root@localhost jenkins]# cat Dockerfile
FROM jenkins/jenkins:jdk21
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip sshpass \
# 关键:使用阿里云 PyPI 镜像加速安装 ansible
&& python3 -m pip install --break-system-packages "ansible>=9,<11" \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER jenkins
3)jenkins下安装maven插件

4)jenkins设置全局环境变量,如:dingding

5)新建"流水线"项目,这样子就可以使用流水线jenkinsfile了

6)abc项目下的jenkinsfile配置 // 自动1分钟拉取一次,检查是否有新提交
pipeline {
agent any
triggers {
pollSCM('* * * * *')
}
tools {
maven 'maven3.9.14'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Collect Git Info') {
steps {
script {
env.GIT_BRANCH_NAME = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
env.GIT_LAST_COMMIT = sh(returnStdout: true, script: 'git log -1 --pretty=format:"%h %s (%an, %ad)" --date=iso').trim()
}
}
}
}
post {
success {
script {
if (env.DINGTALK_WEBHOOK?.trim()) {
env.BUILD_NOTIFY_MSG = """test Jenkins build succeeded
Job: ${env.JOB_NAME}
Branch: ${env.GIT_BRANCH_NAME}
Last commit: ${env.GIT_LAST_COMMIT}
Build number: #${env.BUILD_NUMBER}
Build URL: ${env.BUILD_URL}""".stripIndent().trim()
sh '''
payload=$(cat <<EOF
{"msgtype":"text","text":{"content":"${BUILD_NOTIFY_MSG}"}}
EOF
)
curl -sS -X POST "$DINGTALK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$payload" >/dev/null
'''
} else {
echo 'DINGTALK_WEBHOOK is empty, skip DingTalk notification.'
}
}
}
}
}
7)打包工程ansible

8)hosts.ini
[abc_workers]
192.168.3.204
192.168.3.205
[abc_workers:vars]
ansible_user=root
ansible_port=22
ansible_become=true
ansible_ssh_private_key_file=/var/jenkins_home/.ssh/id_ed25519
9)deploy-abc.yml // 免密登录,jdk自动安装等
---
- name: Deploy abc Spring Boot app
hosts: abc_workers
become: true
vars:
app_name: abc
app_user: abc
app_group: abc
app_dir: /opt/{{ app_name }}
app_jar: "{{ app_dir }}/{{ app_name }}.jar"
log_dir: /var/log/{{ app_name }}
log_file: "{{ log_dir }}/{{ app_name }}.log"
app_ports:
- 8080
app_port: "{{ app_ports[0] }}"
java_opts: "-Xms256m -Xmx512m"
java_package_by_family:
RedHat: java-21-openjdk-devel
Debian: openjdk-21-jdk-headless
pre_tasks:
- name: Validate artifact path argument
delegate_to: localhost
become: false
run_once: true
ansible.builtin.assert:
that:
- artifact_path is defined
- artifact_path | length > 0
fail_msg: "You must pass artifact_path, for example: -e artifact_path=/path/to/abc.jar"
- name: Check artifact exists on Jenkins node
delegate_to: localhost
become: false
run_once: true
ansible.builtin.stat:
path: "{{ artifact_path }}"
register: local_artifact_stat
- name: Fail when artifact does not exist
delegate_to: localhost
become: false
run_once: true
ansible.builtin.fail:
msg: "Artifact not found: {{ artifact_path }}"
when: not local_artifact_stat.stat.exists
tasks:
- name: Check Java runtime is installed
ansible.builtin.command: java -version
register: java_check
changed_when: false
failed_when: false
- name: Check jps command is available
ansible.builtin.command: jps -l
register: jps_check
changed_when: false
failed_when: false
- name: Install Java 21 JDK when java or jps is missing
ansible.builtin.package:
name: "{{ java_package_by_family[ansible_facts.os_family] }}"
state: present
when: java_check.rc != 0 or jps_check.rc != 0
- name: Re-check Java runtime after install
ansible.builtin.command: java -version
register: java_check_after_install
changed_when: false
when: java_check.rc != 0 or jps_check.rc != 0
- name: Re-check jps after install
ansible.builtin.command: jps -l
register: jps_check_after_install
changed_when: false
when: java_check.rc != 0 or jps_check.rc != 0
- name: Fail when Java 21/JPS installation did not succeed
ansible.builtin.fail:
msg: "Java/JPS install failed on {{ inventory_hostname }}. Please check package repositories and network."
when:
- java_check.rc != 0 or jps_check.rc != 0
- java_check_after_install.rc != 0 or jps_check_after_install.rc != 0
- name: Ensure app group exists
ansible.builtin.group:
name: "{{ app_group }}"
state: present
- name: Ensure app user exists
ansible.builtin.user:
name: "{{ app_user }}"
group: "{{ app_group }}"
shell: /sbin/nologin
create_home: false
system: true
state: present
- name: Ensure app directory exists
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0755"
- name: Ensure log directory exists
ansible.builtin.file:
path: "{{ log_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0755"
- name: Check firewalld CLI exists
ansible.builtin.command: bash -lc "command -v firewall-cmd"
register: firewall_cmd_check
changed_when: false
failed_when: false
- name: Open app ports in firewalld
ansible.builtin.command: firewall-cmd --permanent --add-port={{ item }}/tcp
register: firewall_add_port
changed_when: "'success' in firewall_add_port.stdout"
loop: "{{ app_ports }}"
when: firewall_cmd_check.rc == 0
- name: Reload firewalld
ansible.builtin.command: firewall-cmd --reload
changed_when: false
when: firewall_cmd_check.rc == 0
- name: Upload app jar
ansible.builtin.copy:
src: "{{ artifact_path }}"
dest: "{{ app_jar }}"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: "0644"
notify: Restart app
- name: Render systemd service
ansible.builtin.template:
src: ../templates/abc.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: "0644"
notify:
- Reload systemd
- Restart app
- name: Ensure service enabled
ansible.builtin.systemd:
name: "{{ app_name }}"
enabled: true
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart app
ansible.builtin.systemd:
name: "{{ app_name }}"
state: restarted
10)abc.service.j2
[Unit]
Description={{ app_name }} Spring Boot Service
After=network.target
[Service]
Type=simple
User={{ app_user }}
WorkingDirectory={{ app_dir }}
ExecStart=/usr/bin/java {{ java_opts }} -jar {{ app_jar }} --server.port={{ app_port }}
SuccessExitStatus=143
Restart=always
RestartSec=5
StandardOutput=append:{{ log_file }}
StandardError=append:{{ log_file }}
[Install]
WantedBy=multi-user.target
11)ansible.cfg
[defaults]
inventory = inventory/hosts.ini
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
interpreter_python = auto_silent
[ssh_connection]
pipelining = True
12)ansible工程下的jenkinsfile // 实现真正的打包
pipeline {
agent any
tools {
maven 'maven3.9.14'
}
parameters {
string(name: 'ABC_REPO_URL', defaultValue: 'http://192.168.3.203:3000/root/abc.git', description: 'abc repository URL, for example: http://<gitea-host>:3000/<user>/abc.git')
string(name: 'ABC_BRANCH', defaultValue: 'main', description: 'abc branch to deploy')
string(name: 'ABC_GIT_CREDENTIALS_ID', defaultValue: 'ad6a2c23-e0fc-41cd-ae2c-40444bc1ac51', description: 'Jenkins credentialsId for abc git repo (Username with password or token)')
booleanParam(name: 'AUTO_BOOTSTRAP_SSH', defaultValue: true, description: 'Automatically setup SSH key-based login to worker nodes before deploy')
string(name: 'WORKER_BOOTSTRAP_CREDENTIALS_ID', defaultValue: 'root', description: 'Jenkins Username/Password credentialsId for initial worker SSH login')
string(name: 'ANSIBLE_INVENTORY', defaultValue: 'inventory/hosts.ini', description: 'Ansible inventory path in ansible repo')
string(name: 'APP_PORT', defaultValue: '8080', description: 'Spring Boot service port')
}
stages {
stage('Validate Parameters') {
steps {
script {
if (!params.ABC_REPO_URL?.trim()) {
error 'ABC_REPO_URL is required.'
}
}
}
}
stage('Checkout abc') {
steps {
dir('abc-src') {
git branch: params.ABC_BRANCH, credentialsId: params.ABC_GIT_CREDENTIALS_ID, url: params.ABC_REPO_URL
}
}
}
stage('Build abc') {
steps {
dir('abc-src') {
sh 'mvn clean package -DskipTests'
script {
env.ARTIFACT_RELATIVE_PATH = sh(
returnStdout: true,
script: "ls -1 target/*.jar | grep -v 'original-' | head -n 1"
).trim()
if (!env.ARTIFACT_RELATIVE_PATH) {
error 'No jar artifact found under target/.'
}
}
}
}
}
stage('Check Runtime') {
steps {
sh '''
set -e
python3 --version
ansible-playbook --version
sshpass -V
'''
}
}
stage('Bootstrap SSH Trust') {
when {
expression { return params.AUTO_BOOTSTRAP_SSH }
}
steps {
withCredentials([usernamePassword(credentialsId: params.WORKER_BOOTSTRAP_CREDENTIALS_ID, usernameVariable: 'SSH_USER', passwordVariable: 'SSH_PASS')]) {
sh '''
set -e
mkdir -p /var/jenkins_home/.ssh
chmod 700 /var/jenkins_home/.ssh
if [ ! -f /var/jenkins_home/.ssh/id_ed25519 ]; then
ssh-keygen -t ed25519 -f /var/jenkins_home/.ssh/id_ed25519 -N ''
fi
chmod 600 /var/jenkins_home/.ssh/id_ed25519
chmod 644 /var/jenkins_home/.ssh/id_ed25519.pub
for host in 192.168.3.204 192.168.3.205; do
sshpass -p "$SSH_PASS" ssh-copy-id \
-i /var/jenkins_home/.ssh/id_ed25519.pub \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
"$SSH_USER@$host"
done
'''
}
}
}
stage('Deploy to workers') {
steps {
script {
env.ARTIFACT_ABS_PATH = sh(
returnStdout: true,
script: "readlink -f abc-src/${env.ARTIFACT_RELATIVE_PATH}"
).trim()
}
sh '''
ansible-playbook -i "${ANSIBLE_INVENTORY}" playbooks/deploy-abc.yml \
-e "artifact_path=${ARTIFACT_ABS_PATH}" \
-e "app_port=${APP_PORT}"
'''
}
}
}
post {
success {
script {
if (env.DINGTALK_WEBHOOK?.trim()) {
env.DEPLOY_NOTIFY_MSG = """test Jenkins deploy succeeded
Job: ${env.JOB_NAME}
Build number: #${env.BUILD_NUMBER}
ABC branch: ${params.ABC_BRANCH}
Targets: 192.168.3.204, 192.168.3.205
Build URL: ${env.BUILD_URL}""".stripIndent().trim()
sh '''
payload=$(cat <<EOF
{"msgtype":"text","text":{"content":"${DEPLOY_NOTIFY_MSG}"}}
EOF
)
curl -sS -X POST "$DINGTALK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$payload" >/dev/null
'''
} else {
echo 'DINGTALK_WEBHOOK is empty, skip DingTalk notification.'
}
}
}
}
}
13)成功

14)并且2个机器也能顺利访问
