Windows 使用 Docker Jenkins 自动部署 Vue 前端项目
Windows 使用 Docker Jenkins 自动部署 Vue 前端项目
本文以 joy-admin-frontend 为例,将 Mac/Linux 流程迁移到 Windows 10/11。前端使用 Vue 2、Vue CLI 4、webpack 4,构建环境固定为 Node.js 16.20.2、npm 8.19.4。
发布链路:
Codeup -> Jenkins Linux 容器构建 dist -> 共享挂载写入 Windows D:/work 发布目录 -> Windows OpenSSH 执行 PowerShell 切换 current -> Windows Nginx 检查并 reload -> HTTP 验证失败时恢复旧版本Jenkins 在 Docker Desktop 的 Linux 容器内运行,所以 Pipeline 使用 sh;Windows 主机命令使用 PowerShell。
一、目录和 Nginx
先手动创建 D:/work:
New-Item-ItemType Directory-Force-Path'D:/work/joy-admin-frontend-deploy/releases','D:/work/joy-admin-frontend-deploy/scripts','D:/work/joy-admin-deploy/jenkins','D:/work/nginx','D:/work/ssh'New-Item-ItemType File-Force-Path'D:/work/joy-admin-deploy/jenkins/known_hosts'|Out-Null目录结构:
D:/work/ ├── joy-admin-frontend-deploy/ │ ├── releases/ │ └── scripts/ ├── nginx/ │ ├── nginx.exe │ └── conf/ └── ssh/将 Windows 版 Nginx 解压到 D:/work/nginx,检查:
Test-Path'D:/work/nginx/nginx.exe'Test-Path'D:/work/nginx/conf/nginx.conf'在 nginx.conf 的 http 块中加入:
include D:/work/nginx/conf/nginx-joy-admin.conf;创建 nginx-joy-admin.conf:
server { listen 80; server_name dev.joy.admin.com; root D:/work/joy-admin-frontend-deploy/current; index index.html; location /prod-api/ { proxy_pass http://127.0.0.1:9080/; 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_connect_timeout 10s; proxy_read_timeout 60s; } location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } location /static/ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; } location / { try_files $uri $uri/ /index.html; } gzip on; gzip_min_length 1k; gzip_comp_level 6; gzip_types text/plain text/css application/javascript application/json image/svg+xml; }检查和启动 Nginx:
D:/work/nginx/nginx.exe-t-p D:/work/nginx-c D:/work/nginx/conf/nginx.confStart-Process-FilePath'D:/work/nginx/nginx.exe'-WorkingDirectory'D:/work/nginx'已运行时只 reload:
D:/work/nginx/nginx.exe-s reload-p D:/work/nginx-c D:/work/nginx/conf/nginx.conf本机测试域名时,管理员 PowerShell 执行:
Add-Content-Path'C:/Windows/System32/drivers/etc/hosts'-Value'127.0.0.1 dev.joy.admin.com'ipconfig/flushdns二、Windows OpenSSH
Jenkins 通过 SSH 调用 Windows PowerShell 和 Nginx,不使用 rsync 传输文件;文件由 Docker Desktop 共享挂载提供。
管理员 PowerShell 安装并启动 OpenSSH Server:
Add-WindowsCapability-Online-Name OpenSSH.Server~~~~0.0.1.0Start-ServicesshdSet-Service-Name sshd-StartupType Automaticif(-not(Get-NetFirewallRule-Name sshd-ErrorAction SilentlyContinue)){New-NetFirewallRule-Name sshd-DisplayName'OpenSSH Server'-Enabled True-Direction Inbound-Protocol TCP-Action Allow-LocalPort 22}创建普通部署用户:
$Password=Read-Host'输入 jenkinsdeploy 密码'-AsSecureStringNew-LocalUser-Name'jenkinsdeploy'-Password$Password-Description'Jenkins frontend deployment user'Add-LocalGroupMember-Group'Users'-Member'jenkinsdeploy'在 Windows 主机生成密钥:
ssh-keygen-t ed25519-C'jenkins-nginx-deploy'-f'D:/work/ssh/jenkins-nginx-host'写入授权文件:
$SshDir='C:/Users/jenkinsdeploy/.ssh'New-Item-ItemType Directory-Force-Path$SshDir|Out-NullCopy-Item'D:/work/ssh/jenkins-nginx-host.pub'"$SshDir/authorized_keys"-Force测试:
ssh-i D:/work/ssh/jenkins-nginx-host-o IdentitiesOnly=yes jenkinsdeploy@127.0.0.1'whoami'输出 jenkinsdeploy 即可。Nginx 最好由该部署用户启动,确保该用户可以执行 reload。
三、修改 Jenkins Compose 挂载
在 Windows Jenkins Compose 的 jenkins 服务 volumes 增加:
-"D:/work/joy-admin-frontend-deploy:/frontend-deploy"相关片段:
volumes:-jenkins_home:/var/jenkins_home-jenkins_npm_cache:/root/.npm-/var/run/docker.sock:/var/run/docker.sock-./jenkins/known_hosts:/root/.ssh/known_hosts:ro-"D:/work/joy-admin-frontend-deploy:/frontend-deploy"重新创建 Jenkins:
Set-Location'D:/work/joy-admin-deploy'docker compose--env-file.env-f compose.jenkins.yml up-d--force-recreate docker exec joy-jenkins test-d/frontend-deploy/releasesJenkins 容器能访问 /frontend-deploy/releases 才能继续。
从 Jenkins 容器扫描 Windows OpenSSH 主机密钥,写入宿主机挂载文件:
docker exec joy-jenkins ssh-keyscan-T 10-H-t ed25519,rsa host.docker.internal|Set-Content-Encoding ascii'D:/work/joy-admin-deploy/jenkins/known_hosts'docker exec joy-jenkins ssh-keygen-lf/root/.ssh/known_hosts确认指纹和 Windows 主机一致后再继续。known_hosts 文件通过挂载实时提供给 Jenkins。
四、创建 current 切换脚本
创建 D:/work/joy-admin-frontend-deploy/scripts/switch-current.ps1:
param([Parameter(Mandatory = $true)][ValidatePattern('^[A-Za-z0-9._-]+$')][string]$Tag)$ErrorActionPreference='Stop'$Root='D:/work/joy-admin-frontend-deploy'$NginxExe='D:/work/nginx/nginx.exe'$NginxPrefix='D:/work/nginx'$NginxConf='D:/work/nginx/conf/nginx.conf'$Release=Join-Path$Root"releases/$Tag"$Current=Join-Path$Root'current'$Previous=Join-Path$Root'current.previous'$PreviousTagFile=Join-Path$Root'previous-tag.txt'if(-not(Test-Path(Join-Path$Release'index.html'))){throw"发布目录不存在:$Release"}$PreviousTag=''if(Test-Path$Current){$Link=Get-Item-LiteralPath$Current$LinkTarget=$Link.Targetif(-not$LinkTarget){$LinkTarget=$Link.LinkTarget}if($LinkTarget){$PreviousTag=Split-Path-Leaf$LinkTarget}}Set-Content-Path$PreviousTagFile-Value$PreviousTag-Encoding asciiif(Test-Path$Previous){Remove-Item-LiteralPath$Previous-Force}if(Test-Path$Current){Move-Item-LiteralPath$Current-Destination$Previous-Force}try{New-Item-ItemType Junction-Path$Current-Target$Release|Out-Null&$NginxExe-t-p$NginxPrefix-c$NginxConfif($LASTEXITCODE-ne0){throw'Nginx 配置检查失败'}&$NginxExe-s reload-p$NginxPrefix-c$NginxConfif($LASTEXITCODE-ne0){throw'Nginx reload 失败'}Write-Output"切换成功:$Tag"exit0}catch{if(Test-Path$Current){Remove-Item-LiteralPath$Current-Force}if($PreviousTag){$OldRelease=Join-Path$Root"releases/$PreviousTag"New-Item-ItemType Junction-Path$Current-Target$OldRelease|Out-Null&$NginxExe-s reload-p$NginxPrefix-c$NginxConf|Out-Null}Write-Error$_exit1}五、Jenkins 凭据和 Pipeline
Jenkins 中配置:
codeup-ssh Codeup 代码拉取私钥 nginx-host-ssh jenkinsdeploy 用户的 Windows SSH 私钥nginx-host-ssh 凭据填写:
Kind:SSH Username with private key Username:jenkinsdeploy Private Key:D:/work/ssh/jenkins-nginx-host 的私钥内容 ID:nginx-host-ssh创建 New Item -> joy-admin-frontend -> Pipeline。完整脚本如下,替换 Codeup 仓库地址:
pipeline{agent any parameters{choice(name:'ACTION',choices:['DEPLOY','ROLLBACK'],description:'发布或回滚')string(name:'GIT_BRANCH',defaultValue:'main',description:'前端分支')string(name:'FRONTEND_URL',defaultValue:'http://dev.joy.admin.com',description:'前端访问地址')string(name:'ROLLBACK_TAG',defaultValue:'',description:'回滚版本')}options{timestamps()timeout(time:45,unit:'MINUTES')disableConcurrentBuilds()skipDefaultCheckout(true)buildDiscarder(logRotator(numToKeepStr:'30'))}environment{DEPLOY_HOST='host.docker.internal'DEPLOY_USER='jenkinsdeploy'DEPLOY_ROOT='/frontend-deploy'HOST_DEPLOY_ROOT='D:/work/joy-admin-frontend-deploy'FRONTEND_HOST='dev.joy.admin.com'NGINX_PORT='80'SWITCH_SCRIPT='D:/work/joy-admin-frontend-deploy/scripts/switch-current.ps1'NODE_OPTIONS='--max-old-space-size=4096'}stages{stage('拉取代码'){when{expression{params.ACTION=='DEPLOY'}}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:组织/仓库.git frontend '''}script{env.GIT_SHORT=sh(script:'git -C frontend rev-parse --short=8 HEAD',returnStdout:true).trim()env.RELEASE_TAG="${env.BUILD_NUMBER}-${env.GIT_SHORT}"}}}stage('构建前端'){when{expression{params.ACTION=='DEPLOY'}}steps{dir('frontend'){sh''' set -eu printf '%s\n' "VUE_APP_BASE_API=/prod-api" "VUE_APP_FRONT_URL=$FRONTEND_URL" > .env.production.local node -v npm -v npm ci --no-audit --no-fund npm run build:prod test -f dist/index.html test -d dist/static '''}}}stage('发布或回滚'){steps{sshagent(credentials:['nginx-host-ssh']){script{if(params.ACTION=='ROLLBACK'){if(!params.ROLLBACK_TAG.trim()){error('回滚必须填写 ROLLBACK_TAG')}env.TARGET_TAG=params.ROLLBACK_TAG.trim()}else{env.TARGET_TAG=env.RELEASE_TAG}sh''' set -eu case "$TARGET_TAG" in *[!A-Za-z0-9._-]*) echo "非法版本标签"; exit 1 ;; esac RELEASE_DIR="/frontend-deploy/releases/$TARGET_TAG" REMOTE="$DEPLOY_USER@$DEPLOY_HOST" SSH_OPTS="-o PreferredAuthentications=publickey -o PasswordAuthentication=no -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" if [ "$ACTION" = "DEPLOY" ]; then rm -rf "$RELEASE_DIR" mkdir -p "$RELEASE_DIR" cp -a frontend/dist/. "$RELEASE_DIR/" else ssh $SSH_OPTS "$REMOTE" "powershell.exe -NoProfile -NonInteractive -Command \"if (-not (Test-Path '$HOST_DEPLOY_ROOT/releases/$TARGET_TAG/index.html')) { exit 1 }\"" fi ssh $SSH_OPTS "$REMOTE" "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $SWITCH_SCRIPT -Tag $TARGET_TAG" if curl --fail --silent --show-error -H "Host: $FRONTEND_HOST" "http://$DEPLOY_HOST:$NGINX_PORT/" >/dev/null; then echo "前端发布成功: $TARGET_TAG" exit 0 fi PREVIOUS_TAG="$(ssh $SSH_OPTS "$REMOTE" "powershell.exe -NoProfile -NonInteractive -Command \"(Get-Content '$HOST_DEPLOY_ROOT/previous-tag.txt')\"" | tr -d '\r')" if [ -n "$PREVIOUS_TAG" ]; then ssh $SSH_OPTS "$REMOTE" "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $SWITCH_SCRIPT -Tag $PREVIOUS_TAG" fi exit 1 '''}}}}}post{always{deleteDir()}}}Pipeline 中继续使用 sh,因为 Jenkins 运行在 Linux 容器内。
六、发布、回滚和验证
首次发布参数:
ACTION=DEPLOY GIT_BRANCH=main FRONTEND_URL=http://dev.joy.admin.com ROLLBACK_TAG=留空版本目录格式:
D:/work/joy-admin-frontend-deploy/releases/构建号-Git短Commit回滚参数:
ACTION=ROLLBACK ROLLBACK_TAG=需要恢复的版本目录名Windows 验证:
Get-Item'D:/work/joy-admin-frontend-deploy/current'|Format-List*curl.exe-I-H'Host: dev.joy.admin.com'http://127.0.0.1/ curl.exe-I-H'Host: dev.joy.admin.com'http://127.0.0.1/dashboard curl.exe-i-H'Host: dev.joy.admin.com'http://127.0.0.1/prod-api/七、常见问题
Jenkins 不能访问发布目录
docker exec joy-jenkins test-d/frontend-deploy/releases检查 Jenkins Compose 是否挂载 D:/work/joy-admin-frontend-deploy 到 /frontend-deploy。
npm ci 失败
确认仓库包含 package-lock.json,并且 Jenkins 中的 Node.js 版本为 16.20.2、npm 版本为 8.19.4。不要在发布流水线中自动生成新的 lock 文件;如果 lockfile 版本与 npm 8 不兼容,应统一项目锁文件和构建镜像中的 npm 版本后再发布。
Windows OpenSSH 连接失败
Get-ServicesshdGet-NetFirewallRule-Name sshd ssh-i D:/work/ssh/jenkins-nginx-host jenkinsdeploy@127.0.0.1'whoami'Junction 创建失败
以管理员 PowerShell 执行,或开启 Windows Developer Mode,并确保部署用户对 D:/work 有读写权限。不要预先创建普通 current 目录。
Nginx reload 失败
D:/work/nginx/nginx.exe-t-p D:/work/nginx-c D:/work/nginx/conf/nginx.confGet-Processnginx确认 Nginx 由 jenkinsdeploy 用户启动,或者该用户拥有控制 Nginx 进程的权限。
/prod-api 返回 403 或 404
proxy_pass 末尾必须保留 /,这样 /prod-api/login 会转发为后端 /login。跨域部署时,后端 CORS_ALLOWED_ORIGINS 必须包含实际前端来源。
八、安全和迁移
- Jenkins 使用普通 Windows 用户,不要使用管理员账号。
- Jenkins 只绑定 127.0.0.1,不直接暴露公网。
- Jenkins Docker Socket 等同于宿主机 Docker 管理权限。
- SSH 私钥、环境文件不得提交 Git。
- releases 目录保留最近 5 至 10 个版本。
- Nginx 配置变更必须先 nginx -t,再 reload。
- 迁移到新 Windows 机器时,重新配置 D:/work、Nginx、OpenSSH、known_hosts 和 Jenkins 凭据。