Jenkins+Docker实现Nuxt2自动化部署

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
  1. 进入 Manage Jenkins > Global Tool Configuration
  2. 找到 NodeJS 配置区域
  3. 点击 Add NodeJS,设置名称和版本(推荐 Node.js 18.x)
  4. 点击 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)
  1. 进入 Manage JenkinsManage Credentials
  2. 点击 (global)Add Credentials
  3. 选择类型:
    • 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 "=========================================="

部署脚本核心流程

  1. 停止并清理旧容器和网络
  2. 清理历史镜像(保留最近3个版本)
  3. 创建专用网络
  4. 启动 Nuxt 应用容器
  5. 等待应用启动并进行健康检查
  6. 配置并启动 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 容器
  • 进行健康检查

九、注意事项

  1. 环境变量配置 :确保 Jenkins 环境中设置了 IMAGE_NAMEIMAGE_TAG 等必要变量
  2. Docker 权限:Jenkins 用户需要具有 Docker 执行权限
  3. 端口占用:确保 3000 和 81 端口未被占用
  4. 构建超时:流水线设置了 30 分钟超时
  5. 镜像清理:部署脚本保留最近 3 个版本的镜像

十、总结

通过 Jenkins + Docker + Nginx 的组合,我们实现了从代码提交到应用部署的全自动化流程:

核心优势

  • 一致性:容器化保证各环境一致
  • 可追溯:每个构建都有唯一版本标签
  • 自动化:减少人工干预,提高效率
  • 可扩展:易于扩展到多节点部署

这套方案适合中小型前端项目的自动化部署需求,具有较高的实用性和可操作性。

相关推荐
jsons11 小时前
linux 用户内存保障管理配置
linux·运维·服务器
北京智和信通1 小时前
智和信通助力某信息工程大学实现校园全域运维监控
运维·服务器·网络监控·网络管理软件·网管软件·网管运维·网络管理系统
Fanfanaas1 小时前
Linux 系统编程 文件篇 (一)
linux·运维·服务器·c++·学习
七牛云行业应用1 小时前
MCP 服务器本地部署实战【2026】:Python/Node.js 搭建 + Claude/Cursor/TRAE
服务器·python·node.js
j_xxx404_1 小时前
Linux信号机制:从键盘到内核、进阶实战硬核剖析
linux·运维·服务器·c++·人工智能·ai
Mr. zhihao1 小时前
从 `cat file.txt` 到屏幕:一次 Linux 文件读取的完整旅程
linux·运维·服务器
码完就睡2 小时前
Linux——进程间通信
linux·运维·服务器
j_xxx404_2 小时前
Linux进程信号:内核数据结构与捕捉递达全流程
linux·运维·服务器·人工智能·ai
Black蜡笔小新2 小时前
企业私有化AI训练推理一体工作站/自动化AI算法训练服务器DLTM让企业AI自主可控
服务器·人工智能·自动化