部署方案二 使用 Docker 方案部署项目
方案概述
通过 Jenkins 构建 Docker 镜像,推送到镜像仓库,然后在远程服务器上拉取并运行。相比 SSH 直传方案,Docker 方案提供了更好的环境隔离和版本管理。
Jenkins 构建 -> npm build -> Docker build -> 镜像仓库推送 -> SSH 触发远程 docker run
text
Docker 部署 vs SSH 直传方案对比
| 维度 | SSH 直传方案 | Docker 部署方案 |
|---|---|---|
| 环境隔离 | 依赖宿主机 Nginx 配置 | 容器化隔离,环境一致 |
| 版本管理 | 直接覆盖 dist 文件 | 镜像标签版本化,支持回滚 |
| 部署粒度 | 单纯文件传输 | 完整运行环境打包 |
| 回滚能力 | 需要备份旧文件 | docker run <旧版本> 即可回滚 |
| 依赖要求 | 远端只需 Nginx | 远端需要 Docker 环境 |
| 构建速度 | 无需构建镜像,更快 | 需要额外 docker build 步骤 |
| 适用场景 | 简单前端项目 | 微服务架构、多环境部署 |
SSH 方案适合简单场景快速部署,Docker 方案适合需要环境隔离和版本管理的生产环境。
两种构建策略对比
| 策略 | 流程 | 优点 | 缺点 |
|---|---|---|---|
| Jenkins 内构建 | Jenkins Node.js 环境 install + build -> Docker 仅打包 dist | 缓存依赖,构建快 | 需要配置 Node.js 环境 |
| Docker 内构建 | Dockerfile 包含 install + build -> 一键构建 | 环境完全隔离 | 每次都要安装依赖,构建慢 |
推荐使用 Jenkins 内构建,因为:
- npm 依赖有缓存,后续构建更快
- Docker build 只负责打包 dist 产物,速度极快
- 最终镜像仅包含 Nginx + 静态文件,体积约 25-30MB
镜像仓库选择
| 仓库类型 | 示例 | 适用场景 |
|---|---|---|
| Docker Hub | hub.docker.com | 公开项目,国际网络 |
| 阿里云 ACR | registry.cn-hangzhou.aliyuncs.com | 国内网络,推荐 |
| 腾讯云 TCR | ccr.ccs.tencentyun.com | 国内网络备选 |
| 私有 Registry | 自建 Harbor | 企业内部,高安全 |
国内网络访问 Docker Hub 可能不稳定,建议使用阿里云容器镜像服务(ACR)。
Docker Hub 登录配置
方式一:使用密钥文件(推荐)
- 进入任务 Configure -> Build Environment
- 勾选 Use secret text(s) or file(s)
- 点击 Add -> Secret file
- 上传密码文件,设置变量名(如
PASS) - 在构建步骤中使用:
cat $PASS | docker login -u <username> --password-stdin
bash
方式二:直接使用密码(不推荐)
echo "<password>" | docker login -u <username> --password-stdin
bash
Dockerfile 配置
精简版 Dockerfile(Jenkins 已构建)
当 Jenkins 已完成 npm install 和 npm build,Dockerfile 只需打包 dist 目录:
FROM nginx:stable-alpine
# 拷贝构建产物到 Nginx 目录
COPY dist/ /usr/share/nginx/html/
# Nginx 配置(可选)
# COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
dockerfile
完整版 Dockerfile(多阶段构建,Docker 内完成全部构建)
如果不使用 Jenkins 的 Node.js 环境,可在 Docker 内完成全部构建。使用多阶段构建(multi-stage build)可以将 1.2GB 的开发镜像压缩至 25-30MB 的生产镜像:
# ---- 构建阶段 ----
FROM node:18-alpine AS builder
WORKDIR /app
# 先拷贝依赖文件,利用 Docker 层缓存加速后续构建
COPY package*.json ./
RUN npm install --registry=https://registry.npmmirror.com/
# 再拷贝源码(变更频率更高)
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM nginx:stable-alpine
# 仅拷贝构建产物
COPY --from=builder /app/dist /usr/share/nginx/html/
# 可选:自定义 Nginx 配置
# COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
dockerfile
多阶段构建的关键:构建阶段产生的
node_modules、源码等不会进入最终镜像,只有dist目录被拷贝到 Nginx 运行阶段。这使得最终镜像极其精简。
带 pnpm 支持的 Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm@latest
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
FROM nginx:stable-alpine
COPY --from=builder /app/dist /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
dockerfile
生产级 Nginx 配置文件
将以下文件保存为 nginx.conf 并在 Dockerfile 中拷贝:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 开启 gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# 静态资源缓存(Vite 打包后的文件名含 hash,可长期缓存)
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# SPA 路由回退
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# 隐藏 Nginx 版本号
server_tokens off;
}
nginx
缓存策略:Vite 等构建工具会对静态资源文件名添加 hash(如
index-B7id5rbw.js),因此/assets/路径可设置immutable长期缓存;而index.html设置no-cache确保每次部署后用户立即获取最新版本。
Jenkins Docker 构建任务配置
构建步骤配置
步骤 1:Node.js 构建
pnpm install
pnpm build
bash
步骤 2:Docker 镜像构建
# 镜像命名格式:用户名/镜像名:标签
# 使用 BUILD_NUMBER 作为标签实现版本递增
docker build -t <username>/frontend:$BUILD_NUMBER .
bash
镜像名称格式:
username/imagename:tag
- 如果推送到 Docker Hub:
lw96/frontend:v1- 如果推送到阿里云 ACR:
registry.cn-hangzhou.aliyuncs.com/lw96/frontend:v1
步骤 3:Docker 登录
cat $PASS | docker login -u <username> --password-stdin
bash
步骤 4:推送镜像
docker push <username>/frontend:$BUILD_NUMBER
bash
步骤 5:SSH 触发远程部署
添加构建步骤 Send files or execute commands over SSH:
| 配置项 | 值 |
|---|---|
| SSH Server | 目标部署服务器 |
| Source files | 任意不存在的文件(必填项占位) |
| Exec command | docker run 命令 |
# 停止并删除旧容器
docker rm -f test 2>/dev/null || true
# 运行新容器
docker run -itd \
--name=test \
--restart=always \
-p 10000:80 \
<username>/frontend:$BUILD_NUMBER
bash
阿里云容器镜像服务配置
创建镜像仓库
- 登录 阿里云控制台
- 搜索 容器镜像服务
- 选择 个人实例(免费)
- 点击 镜像仓库 -> 创建镜像仓库
- 配置:
| 配置项 | 值 |
|---|---|
| 命名空间 | 自定义(如 lw96) |
| 仓库名称 | public(或自定义) |
| 仓库类型 | 公开或私有 |
| 代码源 | 本地仓库 |
修改 Jenkins 配置适配阿里云
需要修改以下配置项:
# 镜像构建(添加阿里云前缀)
docker build -t registry.cn-hangzhou.aliyuncs.com/<namespace>/<repo>:$BUILD_NUMBER .
# 登录(使用阿里云凭证)
cat $PASS | docker login -u <aliyun-username> registry.cn-hangzhou.aliyuncs.com --password-stdin
# 推送
docker push registry.cn-hangzhou.aliyuncs.com/<namespace>/<repo>:$BUILD_NUMBER
# 远程运行
docker run -itd \
--name=test \
--restart=always \
-p 10000:80 \
registry.cn-hangzhou.aliyuncs.com/<namespace>/<repo>:$BUILD_NUMBER
bash
docker-compose 部署方案
对于更复杂的生产环境,可以使用 docker-compose.yml 管理多个服务:
基础版 docker-compose.yml
version: "3.8"
services:
frontend:
image: registry.cn-hangzhou.aliyuncs.com/<namespace>/frontend:latest
container_name: frontend
restart: always
ports:
- "10000:80"
yaml
生产级 docker-compose.yml(含 Nginx 反向代理)
version: "3.8"
services:
frontend:
image: registry.cn-hangzhou.aliyuncs.com/<namespace>/frontend:${TAG}
container_name: frontend
restart: always
expose:
- "80"
networks:
- app-network
nginx-proxy:
image: nginx:stable-alpine
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- frontend
networks:
- app-network
networks:
app-network:
driver: bridge
yaml
使用 docker-compose 的远程部署脚本
将部署脚本 deploy.sh 预先传输到远程服务器:
#!/bin/bash
set -e
TAG=$1
cd /opt/frontend
# 拉取最新镜像
docker compose pull frontend
# 停止旧服务并启动新服务
docker compose up -d --remove-orphans
# 清理旧镜像
docker image prune -f
bash
在 Jenkins 中通过 SSH 执行:
# 先传输 docker-compose.yml 和 deploy.sh
# 然后执行
chmod +x deploy.sh && ./deploy.sh $BUILD_NUMBER
bash
Docker in Docker vs 宿主机 Docker
两种方案对比
| 方案 | docker-compose.yml 配置 | 优劣 |
|---|---|---|
| Docker in Docker (DinD) | 使用 docker:dind 镜像 | 独立隔离,但 ARM 架构有坑 |
| 挂载宿主机 Docker Socket | 挂载 /var/run/docker.sock | 推荐方案,稳定可靠 |
推荐配置:挂载宿主机 Docker Socket
# docker-compose.yml
services:
jenkins:
image: jenkinsci/blueocean
container_name: jenkins-blueocean
volumes:
- jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock # 挂载 Docker Socket
# 注意:注释掉 environment 中的 DOCKER_HOST 等变量
yaml
ARM 架构下的坑点
在 ARM 架构(如 Apple Silicon M1/M2)下使用 Docker in Docker:
- 构建出的镜像可能缺少
Entrypoint和Cmd - 镜像无法正常启动运行
- 解决方案:改用宿主机 Docker Socket 挂载方式
# 检查镜像是否正常
docker inspect <image-id>
# 正常镜像输出:
# "Entrypoint": ["nginx", "-g", "daemon off;"]
# "Cmd": null 或有值
# 异常镜像输出(ARM DinD 坑点):
# "Entrypoint": null
# "Cmd": null
bash
Jenkins Pipeline 方式(进阶)
除了自由风格任务,还可以使用 Jenkinsfile 声明式流水线实现更规范的 Docker 部署:
pipeline {
agent any
environment {
DOCKER_IMAGE = 'frontend'
REGISTRY = 'registry.cn-hangzhou.aliyuncs.com/<namespace>'
DEPLOY_HOST = 'deploy@your-server'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'pnpm install && pnpm build'
}
}
stage('Docker Build') {
steps {
sh "docker build -t ${REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER} ."
}
}
stage('Docker Push') {
steps {
withCredentials([usernamePassword(
credentialsId: 'aliyun-registry-creds',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh """
echo \$REG_PASS | docker login ${REGISTRY} \
-u \$REG_USER --password-stdin
docker push ${REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
"""
}
}
}
stage('Deploy') {
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh """
ssh -o StrictHostKeyChecking=no ${DEPLOY_HOST} << 'EOF'
docker pull ${REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
docker rm -f frontend 2>/dev/null || true
docker run -itd --name frontend \
--restart=always -p 10000:80 \
${REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
EOF
"""
}
}
}
post {
always {
sh "docker rmi ${REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER} || true"
}
}
}
groovy
Pipeline 方式的优势:代码化配置、支持分支策略、可复现、便于团队协作。
镜像验证与调试
检查镜像内容
# 查看镜像详情
docker inspect <image-name>:<tag>
# 运行镜像测试
docker run --rm -p 8080:80 <image-name>:<tag>
# 查看运行中的容器
docker ps
# 查看已下载的镜像
docker images
bash
排错流程
镜像无法运行
|
docker inspect 检查 Entrypoint/Cmd
|
+-- Entrypoint/Cmd 为空 --> 检查 Dockerfile / Docker Socket 挂载
|
+-- Entrypoint/Cmd 正常 --> 检查端口映射 / 网络配置
text
常见部署问题排查
| 症状 | 原因 | 解决方案 |
|---|---|---|
| 页面空白 | base 路径配置错误 | vite.config.ts 设置 base: './' |
| 刷新 404 | 缺少 SPA 路由回退 | Nginx 添加 try_files $uri $uri/ /index.html |
| 静态资源 404 | Dockerfile COPY 路径不匹配 | 确认 dist 目录路径与 Nginx root 一致 |
| 镜像体积过大 | 包含 node_modules | 使用多阶段构建,仅拷贝 dist |
| 502 Bad Gateway | 后端服务未就绪 | 添加 health check 和启动依赖 |
远程命令执行配置
启用 Exec 命令
如果 SSH 插件无法执行远程命令,检查:
- 进入 Manage Jenkins -> System Configuration
- 搜索 SSH
- 展开 Advanced
- 取消勾选 Disable Exec
替代方案:传输 Shell 脚本
如果 docker run 命令过长,可以:
- 在 Jenkins 中先将部署脚本传输到远程服务器
- 再通过 Exec command 执行该脚本
# 构建前传输部署脚本
# Send files: deploy.sh
# 然后执行:
chmod +x deploy.sh && ./deploy.sh
bash
完整流程图
+----------------------------------------------------------+
| Jenkins CI/CD |
| |
| +----------+ +----------+ +------------------+ |
| | Git 拉取 | -> | npm build | -> | docker build | |
| | 源码 | | dist 产物 | | 构建镜像 | |
| +----------+ +----------+ +--------+---------+ |
| | |
| +---------------------------------------v----------+ |
| | docker login -> docker push -> 推送到镜像仓库 | |
| | (Docker Hub / 阿里云 ACR) | |
| +---------------------------------------+----------+ |
+------------------------------------------+---------------+
|
SSH Remote Command |
v
+----------------------------------------------------------+
| 远程部署服务器 |
| |
| docker pull <image>:<tag> -> docker run -> 服务上线 |
| |
+----------------------------------------------------------+
text
踩坑总结
| 坑点 | 原因 | 解决方案 |
|---|---|---|
| 远程命令不执行 | Disable Exec 被勾选 | 系统配置中取消勾选 |
| Docker Hub 推送超时 | 国内网络不稳定 | 切换到阿里云 ACR |
| 凭证安全 | 密码明文泄露 | 使用 Secret file + 环境变量 |
| ARM 架构镜像异常 | DinD 方案在 ARM 下有 bug | 挂载宿主机 Docker Socket |
| 镜像无 Entrypoint | Docker build 环境问题 | 使用宿主机 Docker 重新构建 |
| 空仓库 Webhook 500 | GitLab 无法计算 diff | 推送代码后再测试 |
| 部署后页面空白 | Vite base 路径配置 | 设置 base: './' 或正确路径 |
| 刷新路由 404 | Nginx 未配置 SPA 回退 | 添加 try_files 规则 |
↑