Jenkins + Docker + Nginx 实现 Nuxt2 自动化部署实战
一、引言
在前端工程化实践中,自动化部署是提升开发效率、保证部署一致性的关键环节。本文将以 Nuxt2 项目为例,详细介绍如何使用 Jenkins、Docker 和 Nginx 搭建一套完整的自动化部署流程。
二、技术栈概览
| 技术 | 作用 | 版本 |
|---|---|---|
| Jenkins | CI/CD 流水线管理 | 最新稳定版 |
| Docker | 应用容器化 | 20.x+ |
| Nginx | 反向代理与静态资源服务 | alpine |
| Nuxt2 | Vue.js 服务端渲染框架 | 2.x |
三、环境准备与安装步骤
3.1 环境支持要求
| 项目 | 最低要求 | 推荐配置 |
|---|---|---|
| 操作系统 | Ubuntu 18.04+/CentOS 7+/macOS 10.15+/Windows 10+ | Ubuntu 22.04 LTS |
| CPU | 2 核 | 4 核及以上 |
| 内存 | 4GB | 8GB 及以上 |
| 存储 | 20GB 可用空间 | 50GB 及以上 |
| 网络 | 能够访问 GitHub/Docker Hub | 稳定的网络连接 |
3.2 Docker 安装步骤
3.2.1 Ubuntu/Debian 系统
bash
# 更新系统包
sudo apt update && sudo apt upgrade -y
# 安装依赖
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# 添加 Docker GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 添加 Docker 源
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装 Docker Engine
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
# 启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker
# 验证安装
docker --version
3.2.2 CentOS/RHEL 系统
bash
# 安装依赖
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 添加 Docker 源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 安装 Docker Engine
sudo yum install -y docker-ce docker-ce-cli containerd.io
# 启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker
# 验证安装
docker --version
3.2.3 macOS 系统
通过 Docker Desktop for Mac 下载安装,或使用 Homebrew:
bash
# 使用 Homebrew 安装
brew install --cask docker
# 启动 Docker
open /Applications/Docker.app
3.2.4 Windows 系统
通过 Docker Desktop for Windows 下载安装。
3.3 Jenkins 安装步骤
推荐使用 Docker 方式安装 Jenkins,以下是完整的安装脚本:
bash
#!/bin/bash
# 文件名: start-jenkins.sh
set -e
CONTAINER_NAME="my-jenkins"
IMAGE_TAG="jenkins/jenkins:lts-jdk17"
DATA_DIR="$HOME/jenkins_home"
PORT_WEB=8080
PORT_JNLP=50000
echo "🔍 检查 Docker 是否运行..."
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker 未运行,请先启动 Docker 服务"
exit 1
fi
echo "📂 准备数据目录..."
mkdir -p "$DATA_DIR"
sudo chown -R 1000:1000 "$DATA_DIR"
# 检查容器是否已存在
if docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
echo "⚠️ 容器 '$CONTAINER_NAME' 已存在"
read -p "是否重新创建?(y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🗑️ 移除旧容器..."
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
else
echo "ℹ️ 使用现有容器,启动服务..."
docker start "$CONTAINER_NAME"
exit 0
fi
fi
echo "🐳 拉取镜像..."
docker pull "$IMAGE_TAG"
echo "🚀 启动 Jenkins..."
# 获取宿主机 docker 组的 GID
docker_gid=$(getent group docker | cut -d: -f3 || echo 999)
docker run -d \
--name "$CONTAINER_NAME" \
-p "${PORT_WEB}:8080" \
-p "${PORT_JNLP}:50000" \
-v "${DATA_DIR}:/var/jenkins_home" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-v $(which docker):/usr/bin/docker \
--group-add "$docker_gid" \
--restart unless-stopped \
"$IMAGE_TAG"
echo ""
echo "🎉 Jenkins 部署完成!"
echo "👉 请在浏览器访问: http://localhost:${PORT_WEB}"
echo ""
echo "🔑 获取初始密码:"
echo " docker logs $CONTAINER_NAME | grep -i 'initialAdminPassword'"
echo ""
echo "📝 查看实时日志: docker logs -f $CONTAINER_NAME"
使用方法:
bash
# 保存为 start-jenkins.sh
chmod +x start-jenkins.sh
./start-jenkins.sh
3.4 Jenkins 初始化配置
3.4.1 获取初始密码
bash
docker logs my-jenkins | grep -i 'initialAdminPassword'
输出示例:
makefile
initialAdminPassword: 1234567890abcdef1234567890abcdef
3.4.2 安装必要插件
首次访问 Jenkins 时,选择"安装推荐插件",然后安装以下额外插件:
- Git Plugin - Git 代码仓库支持
- Docker Pipeline - Docker 流水线支持
- Pipeline - Jenkins Pipeline 核心插件
- NodeJS Plugin - Node.js 环境支持
3.4.3 配置 Node.js
- 进入 Manage Jenkins > Global Tool Configuration
- 找到 NodeJS 配置区域
- 点击 Add NodeJS,设置名称和版本(推荐 Node.js 18.x)
- 点击 Save 保存配置
3.4.4 配置 Docker 权限
确保 Jenkins 用户能够执行 Docker 命令:
bash
# 将 Jenkins 用户添加到 docker 组
sudo usermod -aG docker jenkins
# 重启 Jenkins 容器
docker restart my-jenkins
3.5 Jenkins SSH 密钥配置
为了让 Jenkins 能够从 Git 仓库拉取代码(尤其是私有仓库),需要配置 SSH 密钥。
3.5.1 生成 Jenkins 专属 SSH 密钥
在 Jenkins 容器内生成 SSH 密钥对:
bash
# 进入 Jenkins 容器
docker exec -it my-jenkins bash
# 切换到 jenkins 用户并生成 SSH 密钥
su - jenkins
# 生成 SSH 密钥(使用 ed25519 算法,安全性更高)
ssh-keygen -t ed25519 -C "jenkins@example.com" -f ~/.ssh/id_ed25519
# 或者使用 RSA 算法(兼容性更好)
ssh-keygen -t rsa -b 4096 -C "jenkins@example.com" -f ~/.ssh/id_rsa
# 按提示操作,建议设置 passphrase 或留空(自动化场景可留空)
生成过程示例:
vbnet
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): [直接回车留空]
Enter same passphrase again: [再次回车]
Your identification has been saved in /var/jenkins_home/.ssh/id_ed25519
Your public key has been saved in /var/jenkins_home/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx jenkins@example.com
3.5.2 查看并复制公钥
bash
# 查看公钥内容
cat ~/.ssh/id_ed25519.pub
# 输出示例:
# ssh-ed25519 AAAAC3NzaC... jenkins@example.com
复制输出的公钥内容,添加到 Git 仓库的 Deploy Keys 中:
- GitHub: 仓库 Settings → Deploy keys → Add deploy key
- GitLab: 仓库 Settings → Repository → Deploy Keys → Add key
- Gitee: 仓库 Settings → Deploy Keys → Add deploy key
3.5.3 配置 Jenkins 凭据(Credentials)
- 进入 Manage Jenkins → Manage Credentials
- 点击 (global) → Add Credentials
- 选择类型:
- SSH Username with private key(推荐)
- 或 Secret file(上传私钥文件)
SSH Username with private key 配置:
| 字段 | 值 |
|---|---|
| Username | git 或你的 Git 用户名 |
| Private Key | Enter directly → 粘贴私钥内容 |
| ID | jenkins-ssh-key(供 Jenkinsfile 引用) |
| Description | Jenkins SSH Key for Git |
获取私钥内容:
bash
# 在 Jenkins 容器内执行
cat ~/.ssh/id_ed25519
3.5.4 测试 SSH 连接
bash
# 在 Jenkins 容器内测试连接 GitHub
docker exec -it my-jenkins bash
su - jenkins
ssh -T git@github.com
# 首次连接会出现提示,输入 yes
# 成功后会显示:Hi username! You've successfully authenticated...
# 测试 GitLab
docker exec -it my-jenkins bash
su - jenkins
ssh -T git@gitlab.com
3.5.5 在 Jenkinsfile 中使用 SSH 凭据
groovy
pipeline {
agent any
environment {
// 使用 credentials 插件引用 SSH 私钥
GIT_SSH_KEY = credentials('jenkins-ssh-key')
}
stages {
stage('Checkout') {
steps {
// 方式一:使用 checkout scm(推荐,配合 Jenkins 项目配置)
checkout scm
// 方式二:手动指定 Git URL 和凭据
git(
url: 'git@github.com:your-org/your-repo.git',
branch: 'main',
credentialsId: 'jenkins-ssh-key'
)
}
}
}
}
3.6 jenkins-up 快速启动脚本
为了简化 Jenkins 的启动和管理,可以使用以下 jenkins-up 脚本:
3.6.1 jenkins-up.sh 脚本
bash
#!/bin/bash
# 文件名: jenkins-up.sh
# 功能: Jenkins Docker 容器的快速启动/停止/重启/状态管理
set -e
# 配置变量
CONTAINER_NAME="${JENKINS_CONTAINER_NAME:-my-jenkins}"
IMAGE_TAG="${JENKINS_IMAGE:-jenkins/jenkins:lts-jdk17}"
DATA_DIR="${JENKINS_HOME:-$HOME/jenkins_home}"
PORT_WEB="${JENKINS_PORT:-8080}"
PORT_JNLP="${JENKINS_JNLP_PORT:-50000}"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 打印带颜色的消息
print_msg() {
echo -e "${2}${1}${NC}"
}
# 检查 Docker 是否运行
check_docker() {
if ! docker info > /dev/null 2>&1; then
print_msg "❌ Docker 未运行,请先启动 Docker 服务" "$RED"
exit 1
fi
}
# 获取容器状态
get_container_status() {
docker ps -a --format '{{.State}}' --filter "name=$CONTAINER_NAME" 2>/dev/null || echo "not_exist"
}
# 启动 Jenkins
cmd_start() {
check_docker
local status=$(get_container_status)
if [ "$status" = "running" ]; then
print_msg "ℹ️ Jenkins 容器已在运行中" "$YELLOW"
cmd_status
return 0
fi
if [ "$status" != "not_exist" ]; then
print_msg "🚀 启动已存在的 Jenkins 容器..." "$BLUE"
docker start "$CONTAINER_NAME"
print_msg "✅ Jenkins 已启动" "$GREEN"
cmd_status
return 0
fi
print_msg "📂 准备 Jenkins 数据目录: $DATA_DIR" "$BLUE"
mkdir -p "$DATA_DIR"
# 设置目录权限(Jenkins 容器内用户 UID 为 1000)
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
sudo chown -R 1000:1000 "$DATA_DIR" 2>/dev/null || true
fi
# 获取宿主机 docker 组的 GID
local docker_gid=$(getent group docker | cut -d: -f3 2>/dev/null || echo 999)
print_msg "🐳 拉取 Jenkins 镜像: $IMAGE_TAG" "$BLUE"
docker pull "$IMAGE_TAG"
print_msg "🚀 启动 Jenkins 容器..." "$BLUE"
docker run -d \
--name "$CONTAINER_NAME" \
-p "${PORT_WEB}:8080" \
-p "${PORT_JNLP}:50000" \
-v "${DATA_DIR}:/var/jenkins_home" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-v "$(which docker):/usr/bin/docker" \
--group-add "$docker_gid" \
--restart unless-stopped \
"$IMAGE_TAG"
print_msg ""
print_msg "🎉 Jenkins 启动成功!" "$GREEN"
print_msg "👉 访问地址: http://localhost:${PORT_WEB}" "$GREEN"
print_msg ""
print_msg "🔑 获取初始密码命令:" "$YELLOW"
print_msg " docker logs $CONTAINER_NAME | grep -i 'initialAdminPassword'" "$BLUE"
print_msg ""
print_msg "📝 查看实时日志: docker logs -f $CONTAINER_NAME" "$BLUE"
}
# 停止 Jenkins
cmd_stop() {
check_docker
local status=$(get_container_status)
if [ "$status" = "not_exist" ]; then
print_msg "❌ Jenkins 容器不存在" "$RED"
return 1
fi
if [ "$status" != "running" ]; then
print_msg "ℹ️ Jenkins 容器已停止" "$YELLOW"
return 0
fi
print_msg "🛑 停止 Jenkins 容器..." "$BLUE"
docker stop "$CONTAINER_NAME"
print_msg "✅ Jenkins 已停止" "$GREEN"
}
# 重启 Jenkins
cmd_restart() {
cmd_stop
sleep 2
cmd_start
}
# 查看状态
cmd_status() {
check_docker
local status=$(get_container_status)
if [ "$status" = "not_exist" ]; then
print_msg "❌ Jenkins 容器不存在" "$RED"
return 1
fi
print_msg "📊 Jenkins 容器状态:" "$BLUE"
docker ps -a --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
if [ "$status" = "running" ]; then
print_msg ""
print_msg "🔗 访问地址: http://localhost:${PORT_WEB}" "$GREEN"
print_msg "📁 数据目录: $DATA_DIR" "$GREEN"
fi
}
# 查看日志
cmd_logs() {
check_docker
local follow=""
if [ "$1" = "-f" ] || [ "$1" = "--follow" ]; then
follow="-f"
fi
print_msg "📝 Jenkins 日志:" "$BLUE"
docker logs $follow "$CONTAINER_NAME"
}
# 进入容器 shell
cmd_shell() {
check_docker
local status=$(get_container_status)
if [ "$status" != "running" ]; then
print_msg "❌ Jenkins 容器未运行" "$RED"
return 1
fi
print_msg "🐚 进入 Jenkins 容器..." "$BLUE"
docker exec -it "$CONTAINER_NAME" bash
}
# 重置/重新创建容器
cmd_reset() {
check_docker
print_msg "⚠️ 警告: 这将删除 Jenkins 容器并重新创建" "$YELLOW"
read -p "是否继续? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_msg "❎ 操作已取消" "$RED"
return 0
fi
print_msg "🗑️ 删除 Jenkins 容器..." "$BLUE"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
cmd_start
}
# 备份 Jenkins 数据
cmd_backup() {
check_docker
local backup_dir="${JENKINS_BACKUP_DIR:-$HOME/jenkins_backups}"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="jenkins_backup_${timestamp}.tar.gz"
mkdir -p "$backup_dir"
print_msg "💾 备份 Jenkins 数据..." "$BLUE"
print_msg " 源目录: $DATA_DIR" "$BLUE"
print_msg " 备份文件: $backup_dir/$backup_file" "$BLUE"
tar -czf "$backup_dir/$backup_file" -C "$DATA_DIR" .
print_msg "✅ 备份完成: $backup_dir/$backup_file" "$GREEN"
}
# 显示帮助信息
cmd_help() {
cat << EOF
Jenkins Docker 管理脚本
用法: ./jenkins-up.sh [命令] [选项]
命令:
start 启动 Jenkins 容器(如果不存在则创建)
stop 停止 Jenkins 容器
restart 重启 Jenkins 容器
status 查看 Jenkins 容器状态
logs 查看 Jenkins 日志
logs -f 实时跟踪 Jenkins 日志
shell 进入 Jenkins 容器 shell
reset 删除并重新创建 Jenkins 容器
backup 备份 Jenkins 数据目录
help 显示帮助信息
环境变量:
JENKINS_CONTAINER_NAME 容器名称 (默认: my-jenkins)
JENKINS_IMAGE 镜像标签 (默认: jenkins/jenkins:lts-jdk17)
JENKINS_HOME 数据目录 (默认: ~/jenkins_home)
JENKINS_PORT Web 端口 (默认: 8080)
JENKINS_JNLP_PORT JNLP 端口 (默认: 50000)
JENKINS_BACKUP_DIR 备份目录 (默认: ~/jenkins_backups)
示例:
./jenkins-up.sh start # 启动 Jenkins
./jenkins-up.sh logs -f # 实时查看日志
JENKINS_PORT=9090 ./jenkins-up.sh start # 使用自定义端口启动
EOF
}
# 主逻辑
case "${1:-help}" in
start)
cmd_start
;;
stop)
cmd_stop
;;
restart)
cmd_restart
;;
status)
cmd_status
;;
logs)
cmd_logs "$2"
;;
shell)
cmd_shell
;;
reset)
cmd_reset
;;
backup)
cmd_backup
;;
help|--help|-h)
cmd_help
;;
*)
print_msg "❌ 未知命令: $1" "$RED"
cmd_help
exit 1
;;
esac
3.6.2 使用示例
bash
# 保存脚本
chmod +x jenkins-up.sh
# 启动 Jenkins
./jenkins-up.sh start
# 查看状态
./jenkins-up.sh status
# 查看日志
./jenkins-up.sh logs
# 实时跟踪日志
./jenkins-up.sh logs -f
# 进入容器 shell(用于生成 SSH 密钥等操作)
./jenkins-up.sh shell
# 停止 Jenkins
./jenkins-up.sh stop
# 重启 Jenkins
./jenkins-up.sh restart
# 备份数据
./jenkins-up.sh backup
# 使用自定义端口启动
JENKINS_PORT=9090 ./jenkins-up.sh start
3.6.3 脚本特性
| 特性 | 说明 |
|---|---|
| 一键启动 | 自动检查、拉取镜像、创建容器 |
| 状态管理 | 支持 start/stop/restart/status |
| 日志查看 | 支持实时跟踪日志 |
| 数据备份 | 内置备份功能,保护 Jenkins 配置 |
| 环境变量 | 支持通过环境变量自定义配置 |
| 容器交互 | 提供 shell 命令方便进入容器操作 |
四、核心配置文件详解
4.1 Dockerfile --- 多阶段构建优化
dockerfile
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm config set registry https://registry.npmmirror.com/ && \
npm install
COPY . .
RUN npm run build
# 运行阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.nuxt ./.nuxt
COPY --from=builder /app/static ./static
COPY --from=builder /app/nuxt.config.js ./
COPY --from=builder /app/package*.json ./
RUN npm config set registry https://registry.npmmirror.com/ && \
npm install
RUN apk add --no-cache curl
EXPOSE 3000
CMD ["npm", "start"]
设计要点:
- 使用多阶段构建,分离构建环境和运行环境
- 构建阶段:安装所有依赖并执行
npm run build - 运行阶段:仅复制构建产物,减小镜像体积
- 使用国内 npm 镜像加速依赖安装
4.2 Jenkinsfile --- 流水线核心配置
groovy
pipeline {
agent any
environment {
DOCKER_REGISTRY = ''
IMAGE_NAME = 'nuxt2-app'
IMAGE_TAG = "${BUILD_NUMBER}"
BUILD_TIMEOUT_MINUTES = '30'
COMPOSE_PROJECT_NAME = 'nuxt2'
}
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout') {
steps {
script {
echo "📥 开始拉取代码"
echo "=== 拉取参数 ==="
echo " 仓库: ${env.GIT_URL}"
echo " 分支: ${env.GIT_BRANCH}"
try {
echo "开始从 ${env.GIT_URL} 拉取代码..."
checkout scm
echo "=== 拉取结果 ==="
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
sh '''
echo "完整提交哈希:"
git rev-parse HEAD
echo "当前分支:"
git branch --show-current
echo "最近提交:"
git log -1 --oneline
echo "文件统计:"
git status
'''
}
echo "✅ 代码拉取完成 (提交: ${env.GIT_COMMIT_SHORT})"
} catch (Exception e) {
echo "❌ 代码拉取失败!"
echo "错误信息: ${e.getMessage()}"
echo "=== 拉取失败诊断信息 ==="
def currentDir = pwd()
echo "当前目录: ${currentDir}"
sh '''
echo "文件列表:"
ls -la
echo "Git 版本:"
git --version
echo "Git 配置:"
git config --list
echo "网络状态:"
ping -c 3 github.com 2>/dev/null || echo "无法 ping github.com"
'''
error("代码拉取阶段失败")
}
}
}
}
stage('Build Docker Image') {
steps {
script {
echo "🏗️ 开始构建 Docker 镜像..."
echo "构建参数:"
echo " 镜像名称: ${IMAGE_NAME}"
echo " 镜像标签: ${IMAGE_TAG}"
echo " 构建目录: ${pwd()}"
try {
// 清理无用镜像
sh '''
echo "=== 清理无用的镜像 ==="
docker image prune -f 2>/dev/null || true
echo "当前镜像列表:"
docker images --format "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}"
'''
// 构建镜像
sh """
echo "=== 构建镜像: ${IMAGE_NAME}:${IMAGE_TAG} ==="
echo "检查 Dockerfile:"
ls -la Dockerfile
cat Dockerfile
echo "开始构建..."
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} . || {
echo "❌ 构建命令失败,查看详细日志"
exit 1
}
"""
// 标记为 latest
sh """
echo "=== 标记为 latest 版本 ==="
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
echo "构建结果:"
docker images ${IMAGE_NAME}
"""
echo "✅ Docker 镜像构建成功: ${IMAGE_NAME}:${IMAGE_TAG}"
} catch (Exception e) {
echo "❌ Docker 镜像构建失败!"
echo "错误信息: ${e.getMessage()}"
echo "=== 构建失败诊断信息 ==="
def currentDir = pwd()
echo "当前目录: ${currentDir}"
sh '''
echo "文件列表:"
ls -la
echo "Docker 版本:"
docker --version
echo "Docker 信息:"
docker info || true
'''
error("Docker 构建阶段失败")
}
}
}
}
stage('Deploy') {
steps {
script {
echo "🚀 开始部署..."
echo "部署参数:"
echo " 镜像名称: ${IMAGE_NAME}"
echo " 镜像标签: ${IMAGE_TAG}"
echo " 项目名称: ${COMPOSE_PROJECT_NAME ?: 'nuxt2'}"
try {
if (!fileExists('deploy.sh')) {
echo "❌ deploy.sh 文件不存在"
echo "当前目录文件:"
sh 'ls -la'
error("deploy.sh 文件不存在")
}
echo "=== 检查部署脚本 ==="
sh '''
echo "脚本权限:"
ls -la deploy.sh
echo "脚本内容:"
cat deploy.sh
'''
// 传递环境变量给 deploy.sh
sh """
echo "=== 执行部署脚本 ==="
export IMAGE_NAME='${IMAGE_NAME}'
export IMAGE_TAG='${IMAGE_TAG}'
export COMPOSE_PROJECT_NAME='${COMPOSE_PROJECT_NAME ?: "nuxt2"}'
if [ ! -x "./deploy.sh" ]; then
echo "🔧 修复部署脚本权限"
chmod +x ./deploy.sh
fi
echo "环境变量:"
env | grep -E 'IMAGE_|COMPOSE_'
echo "开始执行部署..."
./deploy.sh || {
echo "❌ 部署脚本执行失败"
exit 1
}
"""
echo "✅ 部署完成"
} catch (Exception e) {
echo "❌ 部署失败!"
echo "错误信息: ${e.getMessage()}"
echo "=== 部署失败诊断信息 ==="
def currentDir = pwd()
echo "当前目录: ${currentDir}"
sh '''
echo "文件列表:"
ls -la
echo "Docker 状态:"
docker ps -a
echo "Docker Compose 状态:"
docker-compose ps 2>/dev/null || true
echo "部署脚本内容:"
cat deploy.sh 2>/dev/null || true
'''
error("部署阶段失败")
}
}
}
}
}
post {
success {
echo "🎉 构建部署成功!"
echo "构建号: ${BUILD_NUMBER}"
echo "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
}
failure {
echo "❌ 构建部署失败!"
echo "构建号: ${BUILD_NUMBER}"
}
always {
cleanWs()
}
}
}
流水线三大阶段:
阶段一:代码拉取
groovy
stage('Checkout') {
steps {
script {
checkout scm
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
sh '''
echo "完整提交哈希:"
git rev-parse HEAD
echo "当前分支:"
git branch --show-current
echo "最近提交:"
git log -1 --oneline
'''
}
}
}
阶段二:Docker 镜像构建
groovy
stage('Build Docker Image') {
steps {
script {
// 清理无用镜像
sh 'docker image prune -f 2>/dev/null || true'
// 构建镜像
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ."
// 标记为 latest
sh "docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest"
}
}
}
阶段三:应用部署
groovy
stage('Deploy') {
steps {
script {
sh """
export IMAGE_NAME='${IMAGE_NAME}'
export IMAGE_TAG='${IMAGE_TAG}'
chmod +x ./deploy.sh
./deploy.sh
"""
}
}
}
4.3 deploy.sh --- 容器化部署脚本
bash
#!/bin/bash
set -e
# 容器化部署脚本 - 在 Jenkins 容器内执行 Docker 命令
# 环境变量由 Jenkinsfile 传入:
# - IMAGE_NAME: 镜像名称
# - IMAGE_TAG: 镜像标签
# - COMPOSE_PROJECT_NAME: docker-compose 项目名称
echo "=== 开始容器化部署 ==="
echo "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
echo "项目: ${COMPOSE_PROJECT_NAME}"
# 检查环境变量
if [ -z "$IMAGE_NAME" ] || [ -z "$IMAGE_TAG" ]; then
echo "❌ 错误: IMAGE_NAME 或 IMAGE_TAG 未设置"
exit 1
fi
# 清理旧容器
echo "🛑 停止旧容器..."
docker stop nuxt2-app 2>/dev/null || true
docker rm nuxt2-app 2>/dev/null || true
docker stop nuxt2-nginx 2>/dev/null || true
docker rm nuxt2-nginx 2>/dev/null || true
docker network rm app-network 2>/dev/null || true
# 清理旧镜像(保留最近3个版本)
echo "🧹 清理旧镜像..."
docker images "${IMAGE_NAME}" --format "{{.Repository}}:{{.Tag}}\t{{.ID}}" | \
grep -v "latest" | sort -r | tail -n +4 | awk '{print $2}' | \
xargs -r docker rmi -f 2>/dev/null || true
# 创建网络
echo "🔗 创建 Docker 网络..."
docker network create app-network 2>/dev/null || true
# 启动 Nuxt 应用容器
echo "🚀 启动 Nuxt 应用容器..."
docker run -d \
--name nuxt2-app \
--network app-network \
-p 3000:3000 \
-e NODE_ENV=production \
-e HOST=0.0.0.0 \
-e PORT=3000 \
--restart unless-stopped \
${IMAGE_NAME}:${IMAGE_TAG}
# 等待 Nuxt 应用启动
echo "=== 等待 Nuxt 应用启动 ==="
sleep 5
MAX_RETRIES=30
RETRY_COUNT=0
HEALTHY=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
# 检查 Nuxt 应用健康状态 - 使用容器内部网络地址
if docker exec nuxt2-app curl -sf http://localhost:3000/ > /dev/null 2>&1; then
echo "✅ Nuxt 应用启动成功"
HEALTHY=true
break
fi
# 检查容器是否还在运行
if ! docker ps | grep -q "nuxt2-app"; then
echo "❌ Nuxt 容器已停止,查看日志:"
docker logs nuxt2-app
exit 1
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "等待服务就绪... (${RETRY_COUNT}/${MAX_RETRIES})"
sleep 2
done
if [ "$HEALTHY" = false ]; then
echo "❌ 服务启动超时,查看日志:"
docker logs nuxt2-app
exit 1
fi
# 创建 Nginx 配置文件
echo "📝 创建 Nginx 配置文件..."
cat > ./nginx.conf << 'EOF'
upstream nuxt_app {
server nuxt2-app:3000 max_fails=3 fail_timeout=30s;
}
server {
listen 81;
server_name localhost;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# 日志配置
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# 代理到 Nuxt 应用
location / {
proxy_pass http://nuxt_app;
proxy_http_version 1.1;
# 代理头设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket 支持(用于 Nuxt 热更新,生产环境可选)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# 静态资源缓存(Nuxt 构建的静态文件)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ {
proxy_pass http://nuxt_app;
proxy_set_header Host $host;
# 缓存设置
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 健康检查端点
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
EOF
echo "✅ Nginx 配置文件创建成功"
# 启动 Nginx 容器
echo "🚀 启动 Nginx 容器..."
docker run -d \
--name nuxt2-nginx \
--network app-network \
-p 81:81 \
-p 443:443 \
--restart unless-stopped \
nginx:alpine
# 等待 Nginx 容器启动
sleep 3
# 复制 Nginx 配置文件到容器
echo "📤 复制 Nginx 配置文件到容器..."
docker cp ./nginx.conf nuxt2-nginx:/etc/nginx/conf.d/default.conf
# 重启 Nginx 容器以应用新配置
echo "🔄 重启 Nginx 容器以应用新配置..."
docker restart nuxt2-nginx
# 等待 Nginx 容器重启完成
sleep 3
# 检查 Nginx 是否正常运行
sleep 2
if docker ps | grep -q "nuxt2-nginx"; then
echo "✅ Nginx 容器运行正常"
else
echo "⚠️ Nginx 容器可能未启动,查看日志:"
docker logs nuxt2-nginx
fi
# 检查 Nginx 服务
sleep 2
echo "🩺 检查 Nginx 服务..."
if docker exec nuxt2-nginx curl -sf http://localhost/ > /dev/null 2>&1; then
echo "✅ Nginx 服务正常"
else
echo "⚠️ Nginx 服务可能无法访问"
docker logs nuxt2-nginx
fi
echo ""
echo "=== 部署完成 ==="
docker ps --filter name=nuxt2-
echo ""
echo "=========================================="
echo " 🎉 部署成功!"
echo " 🌐 Nuxt 应用: http://localhost:3000"
echo " 🌐 Nginx 代理: http://localhost:81"
echo "=========================================="
部署脚本核心流程:
- 停止并清理旧容器和网络
- 清理历史镜像(保留最近3个版本)
- 创建专用网络
- 启动 Nuxt 应用容器
- 等待应用启动并进行健康检查
- 配置并启动 Nginx 容器
4.4 docker-compose.yml --- 多容器编排
yaml
version: '3.8'
services:
nuxt-app:
image: ${IMAGE_NAME:-nuxt2-app}:${IMAGE_TAG:-latest}
container_name: nuxt2-app
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- HOST=0.0.0.0
- PORT=3000
restart: unless-stopped
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
nginx:
image: nginx:alpine
container_name: nuxt2-nginx
ports:
- "81:81"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
nuxt-app:
condition: service_started
restart: unless-stopped
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
app-network:
driver: bridge
配置亮点:
- 定义了 nuxt-app 和 nginx 两个服务
- 配置健康检查机制,确保服务可用性
- 使用
depends_on控制服务启动顺序 - 挂载 Nginx 配置文件实现反向代理
4.5 Nginx 配置(内嵌于脚本中)
nginx
server {
listen 81;
server_name localhost;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/json;
# 反向代理到 Nuxt 应用
location / {
proxy_pass http://nuxt_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
proxy_pass http://nuxt_app;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 健康检查端点
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
五、自动化部署流程
arduino
┌─────────────────────────────────────────────────────────────────┐
│ 代码提交 (Git Push) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段一: Checkout │
│ - 拉取最新代码 │
│ - 记录提交信息 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段二: Build Docker Image │
│ - 清理无用镜像 │
│ - 构建镜像 (标签: BUILD_NUMBER) │
│ - 标记为 latest │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段三: Deploy │
│ - 停止旧容器 │
│ - 清理旧镜像 (保留最近3个) │
│ - 创建网络 │
│ - 启动 Nuxt 容器 │
│ - 等待启动并健康检查 │
│ - 启动 Nginx 容器 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 部署完成 │
│ Nuxt: http://localhost:3000 │
│ Nginx: http://localhost:81 │
└─────────────────────────────────────────────────────────────────┘
六、服务访问方式
| 服务 | 地址 | 说明 |
|---|---|---|
| Nuxt 应用 | http://localhost:3000 | 直接访问应用 |
| Nginx 代理 | http://localhost:81 | 通过反向代理访问 |
| Nginx 健康检查 | http://localhost:81/nginx-health | 服务状态检查 |
七、故障排查指南
7.1 常见问题处理
问题 1:代码拉取失败
bash
# 检查网络连接
ping -c 3 github.com
# 检查 Git 配置
git config --list
# 检查 Jenkins 工作目录
ls -la
问题 2:镜像构建失败
bash
# 查看 Dockerfile
cat Dockerfile
# 检查 Docker 版本
docker --version
# 查看 Docker 状态
docker info
问题 3:应用启动失败
bash
# 查看容器日志
docker logs nuxt2-app
# 检查容器状态
docker ps -a
# 进入容器调试
docker exec -it nuxt2-app sh
问题 4:Nginx 配置错误
bash
# 查看容器内配置
docker exec nuxt2-nginx cat /etc/nginx/conf.d/default.conf
# 测试配置
docker exec nuxt2-nginx nginx -t
# 查看 Nginx 日志
docker logs nuxt2-nginx
7.2 诊断命令汇总
bash
# 查看所有相关容器
docker ps --filter name=nuxt2-
# 查看网络配置
docker network inspect app-network
# 查看镜像列表
docker images nuxt2-app
# 验证服务可用性
curl http://localhost:3000
curl http://localhost:81
八、手动部署方案
当 Jenkins 自动构建成功但部署失败时,可使用备用脚本:
bash
# 启动 Nginx 容器反向代理
./start-nginx.sh
start-nginx.sh 功能:
- 检查 Nuxt 应用容器状态
- 创建 Nginx 配置文件
- 启动 Nginx 容器
- 进行健康检查
九、注意事项
- 环境变量配置 :确保 Jenkins 环境中设置了
IMAGE_NAME、IMAGE_TAG等必要变量 - Docker 权限:Jenkins 用户需要具有 Docker 执行权限
- 端口占用:确保 3000 和 81 端口未被占用
- 构建超时:流水线设置了 30 分钟超时
- 镜像清理:部署脚本保留最近 3 个版本的镜像
十、总结
通过 Jenkins + Docker + Nginx 的组合,我们实现了从代码提交到应用部署的全自动化流程:
核心优势:
- 一致性:容器化保证各环境一致
- 可追溯:每个构建都有唯一版本标签
- 自动化:减少人工干预,提高效率
- 可扩展:易于扩展到多节点部署
这套方案适合中小型前端项目的自动化部署需求,具有较高的实用性和可操作性。