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等目录,PowerShell执行:
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.conf
Start-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.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
if (-not (Get-NetFirewallRule -Name sshd -ErrorAction SilentlyContinue)) { New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 }
创建普通部署用户,用户名是jenkinsdeploy:
$Password = Read-Host '输入 jenkinsdeploy 密码' -AsSecureString
New-LocalUser -Name 'jenkinsdeploy' -Password $Password -Description 'Jenkins frontend deployment user'
Add-LocalGroupMember -Group 'Users' -Member 'jenkinsdeploy'
在 Windows宿主机生成密钥,管理员 PowerShell 执行:
ssh-keygen -t ed25519 -C 'jenkins-nginx-deploy' -f 'D:/work/ssh/jenkins-nginx-host'
将公钥加入宿主机授权文件,管理员 PowerShell 执行:
$SshDir = 'C:/Users/jenkinsdeploy/.ssh'
New-Item -ItemType Directory -Force -Path $SshDir | Out-Null
Copy-Item 'D:/work/ssh/jenkins-nginx-host.pub' "$SshDir/authorized_keys" -Force
测试,管理员 PowerShell 执行:
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(compose.jenkins.yml) 的 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 docker容器:
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/releases
Jenkins 容器能访问 /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.Target
if (-not $LinkTarget) { $LinkTarget = $Link.LinkTarget }
if ($LinkTarget) { $PreviousTag = Split-Path -Leaf $LinkTarget }
}
Set-Content -Path $PreviousTagFile -Value $PreviousTag -Encoding ascii
if (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 $NginxConf
if ($LASTEXITCODE -ne 0) { throw 'Nginx 配置检查失败' }
& $NginxExe -s reload -p $NginxPrefix -c $NginxConf
if ($LASTEXITCODE -ne 0) { throw 'Nginx reload 失败' }
Write-Output "切换成功: $Tag"
exit 0
}
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 $_
exit 1
}
五、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-Service sshd
Get-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.conf
Get-Process nginx
确认 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 凭据。
更多推荐
所有评论(0)