build(ci): 新增 CI/CD 配置 + 修复 3 个部署 bug
新增: - Jenkinsfile:master→PROD(172.17.16.14)、release/next→STAGING(172.17.16.7) 镜像推 172.17.16.7:5000/aiot-iot-ui,宿主机暴露 90 - docker-compose.frontend.yml:nginx 容器加入 1panel-network - 根 Dockerfile + apps/web-antd/nginx.conf:与 vben 前端结构对齐 nginx 反代 /admin-api → http://aiot-gateway:48080 修复: - .env.production VITE_GLOB_API_URL 硬编码 127.0.0.1 → /admin-api(走 nginx 反代) - scripts/deploy/Dockerfile 引用不存在的 playground/dist → apps/web-antd/dist 并改用 turbo 单 filter 构建(与根 Dockerfile 一致)
This commit is contained in:
60
Dockerfile
Normal file
60
Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# ==============================
|
||||
# 1️⃣ 构建阶段
|
||||
# ==============================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ------------------------------
|
||||
# npm / pnpm registry 配置
|
||||
# ------------------------------
|
||||
RUN npm config set registry https://registry.npmmirror.com && \
|
||||
echo "registry=https://registry.npmmirror.com" > ~/.npmrc
|
||||
|
||||
# pnpm registry 默认指向 1Panel 内部的 Verdaccio(与 vben 前端共用)
|
||||
# 如果 Jenkins 容器不在 1panel-network 网络里,可以通过 build-arg 覆盖:
|
||||
# docker build --build-arg PNPM_REGISTRY=https://registry.npmmirror.com ...
|
||||
ARG PNPM_REGISTRY=http://1Panel-verdaccio-Ynee:4873/
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && corepack prepare pnpm@10.22.0 --activate
|
||||
RUN pnpm config set registry $PNPM_REGISTRY
|
||||
RUN pnpm config set store-dir /pnpm/store
|
||||
|
||||
# ------------------------------
|
||||
# 复制源码
|
||||
# ------------------------------
|
||||
COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./
|
||||
COPY packages packages
|
||||
COPY apps apps
|
||||
COPY internal internal
|
||||
|
||||
# ------------------------------
|
||||
# 安装依赖 + 构建
|
||||
# network-concurrency 限制并发下载,降低 CI 带宽占用
|
||||
# ------------------------------
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile --network-concurrency 2
|
||||
|
||||
# 限制 turbo 并发与 Node 内存,避免在 8GB 服务器上 OOM
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024 --expose-gc"
|
||||
ENV UV_THREADPOOL_SIZE=2
|
||||
RUN pnpm exec turbo build --filter=@vben/web-antd --concurrency=1
|
||||
|
||||
# ==============================
|
||||
# 2️⃣ 运行阶段
|
||||
# ==============================
|
||||
FROM nginx:alpine
|
||||
|
||||
# 移除默认配置
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制自定义 Nginx 配置(含 /admin-api 反代到后端 gateway)
|
||||
COPY apps/web-antd/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
338
Jenkinsfile
vendored
Normal file
338
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
// ============================================
|
||||
// AIoT IoT UI - Jenkins Pipeline
|
||||
// ============================================
|
||||
// 双分支部署:
|
||||
// master -> PROD (172.17.16.14:90)
|
||||
// release/next -> STAGING (172.17.16.7:90)
|
||||
// 镜像仓库:172.17.16.7:5000
|
||||
// ============================================
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5'))
|
||||
disableConcurrentBuilds()
|
||||
timeout(time: 45, unit: 'MINUTES')
|
||||
timestamps()
|
||||
retry(1)
|
||||
}
|
||||
|
||||
environment {
|
||||
// ===== 应用标识 =====
|
||||
APP_NAME = 'aiot-iot-ui'
|
||||
CONTAINER_NAME = 'aiot-iot-ui'
|
||||
HOST_PORT = '90'
|
||||
|
||||
// ===== 镜像仓库 =====
|
||||
REGISTRY = '172.17.16.7:5000'
|
||||
REGISTRY_HOST = '172.17.16.7'
|
||||
IMAGE_NAME = "${REGISTRY}/${APP_NAME}"
|
||||
|
||||
// ===== 部署目标(默认 PROD,release/next 在 Initialize 阶段覆盖) =====
|
||||
DEPLOY_HOST = '172.17.16.14'
|
||||
DEPLOY_PATH = '/opt/aiot-iot-ui'
|
||||
STAGING_DEPLOY_HOST = '172.17.16.7'
|
||||
STAGING_DEPLOY_PATH = '/opt/aiot-iot-ui'
|
||||
SSH_KEY = '/var/jenkins_home/.ssh/id_rsa'
|
||||
|
||||
// ===== 健康检查 =====
|
||||
HEALTH_CHECK_TIMEOUT = '60'
|
||||
HEALTH_CHECK_INTERVAL = '5'
|
||||
|
||||
// ===== 镜像保留份数 =====
|
||||
IMAGE_KEEP_COUNT = '3'
|
||||
|
||||
// ===== 磁盘守护阈值(%) =====
|
||||
DISK_FREE_MIN_PCT = '5'
|
||||
DISK_FREE_WARN_PCT = '10'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Initialize') {
|
||||
steps {
|
||||
script {
|
||||
env.PIPELINE_START_TIME = System.currentTimeMillis()
|
||||
|
||||
echo "=========================================="
|
||||
echo " ${env.APP_NAME} - CI/CD Pipeline"
|
||||
echo "=========================================="
|
||||
echo "Branch: ${env.BRANCH_NAME}"
|
||||
echo "Build: #${env.BUILD_NUMBER}"
|
||||
echo "=========================================="
|
||||
|
||||
if (env.BRANCH_NAME == 'release/next') {
|
||||
env.DEPLOY_HOST = env.STAGING_DEPLOY_HOST
|
||||
env.DEPLOY_PATH = env.STAGING_DEPLOY_PATH
|
||||
echo "📦 Deploy target: STAGING (${env.DEPLOY_HOST}:${env.HOST_PORT})"
|
||||
} else {
|
||||
echo "📦 Deploy target: PRODUCTION (${env.DEPLOY_HOST}:${env.HOST_PORT})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
script {
|
||||
retry(3) { checkout scm }
|
||||
|
||||
def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
|
||||
def sanitized = env.BRANCH_NAME.replaceAll('/', '-')
|
||||
env.IMAGE_TAG = "${sanitized}-${env.BUILD_NUMBER}-${shortCommit}"
|
||||
env.PREVIOUS_IMAGE_TAG = getPreviousImageTag()
|
||||
|
||||
echo "🏷️ Tag: ${env.IMAGE_TAG}"
|
||||
echo "🔖 Previous Tag (rollback to): ${env.PREVIOUS_IMAGE_TAG}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Pre-build Check') {
|
||||
steps {
|
||||
script {
|
||||
sh "docker version >/dev/null 2>&1 || { echo '❌ Docker not available'; exit 1; }"
|
||||
|
||||
sh """
|
||||
curl -f http://${env.REGISTRY}/v2/ >/dev/null 2>&1 || \
|
||||
echo '⚠️ Registry not accessible, will continue (build still produces local image)'
|
||||
"""
|
||||
|
||||
def diskUsage = sh(
|
||||
script: "df ${env.WORKSPACE} | tail -1 | awk '{print \$5}' | sed 's/%//'",
|
||||
returnStdout: true
|
||||
).trim() as int
|
||||
if (diskUsage > 80) {
|
||||
echo "⚠️ Local disk usage ${diskUsage}%, pruning Docker..."
|
||||
sh "docker system prune -f || true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build & Push Image') {
|
||||
steps {
|
||||
script {
|
||||
def buildStart = System.currentTimeMillis()
|
||||
echo "🔨 Building ${env.IMAGE_NAME}:${env.IMAGE_TAG}"
|
||||
|
||||
sh """
|
||||
set -e
|
||||
docker build \\
|
||||
--network=1panel-network \\
|
||||
-t ${env.IMAGE_NAME}:${env.IMAGE_TAG} \\
|
||||
.
|
||||
|
||||
docker push ${env.IMAGE_NAME}:${env.IMAGE_TAG}
|
||||
|
||||
if [ "${env.BRANCH_NAME}" = "master" ]; then
|
||||
docker tag ${env.IMAGE_NAME}:${env.IMAGE_TAG} ${env.IMAGE_NAME}:latest
|
||||
docker push ${env.IMAGE_NAME}:latest
|
||||
fi
|
||||
"""
|
||||
|
||||
def imageSize = sh(
|
||||
script: "docker images ${env.IMAGE_NAME}:${env.IMAGE_TAG} --format '{{.Size}}'",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
def duration = ((System.currentTimeMillis() - buildStart) / 1000) as int
|
||||
echo "✅ Built and pushed in ${duration}s, size=${imageSize}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Pre-deploy Check') {
|
||||
when { anyOf { branch 'master'; branch 'release/next' } }
|
||||
steps {
|
||||
script {
|
||||
echo "🛡️ Pre-deploy: SSH + 磁盘可达性检查"
|
||||
checkRemoteDiskOrFail(env.DEPLOY_HOST, 'Deploy')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy') {
|
||||
when { anyOf { branch 'master'; branch 'release/next' } }
|
||||
steps {
|
||||
script {
|
||||
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
||||
|
||||
echo "📂 同步 docker-compose.frontend.yml 到 ${env.DEPLOY_HOST}:${env.DEPLOY_PATH}"
|
||||
sh """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} 'mkdir -p ${env.DEPLOY_PATH}'
|
||||
scp ${sshOpts} docker-compose.frontend.yml root@${env.DEPLOY_HOST}:${env.DEPLOY_PATH}/docker-compose.frontend.yml
|
||||
"""
|
||||
|
||||
try {
|
||||
echo "🚀 Deploy ${env.APP_NAME}:${env.IMAGE_TAG}"
|
||||
sh """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
||||
set -e
|
||||
cd ${env.DEPLOY_PATH}
|
||||
export IMAGE_TAG=${env.IMAGE_TAG}
|
||||
export REGISTRY_HOST=${env.REGISTRY}
|
||||
export HOST_PORT=${env.HOST_PORT}
|
||||
docker compose -f docker-compose.frontend.yml pull
|
||||
docker compose -f docker-compose.frontend.yml up -d --force-recreate
|
||||
sleep 3
|
||||
'
|
||||
"""
|
||||
|
||||
waitForHealthy(sshOpts)
|
||||
|
||||
echo "✅ ${env.APP_NAME} deployed to ${env.DEPLOY_HOST}:${env.HOST_PORT}"
|
||||
} catch (Exception e) {
|
||||
echo "❌ Deploy failed: ${e.message}"
|
||||
if (env.PREVIOUS_IMAGE_TAG && env.PREVIOUS_IMAGE_TAG != 'latest') {
|
||||
echo "🔄 Rolling back to ${env.PREVIOUS_IMAGE_TAG}"
|
||||
rollback(sshOpts)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Cleanup Old Images') {
|
||||
when { anyOf { branch 'master'; branch 'release/next' } }
|
||||
steps {
|
||||
script {
|
||||
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
||||
echo "🧹 清理 ${env.DEPLOY_HOST} 上 ${env.APP_NAME} 的旧镜像(保留 ${env.IMAGE_KEEP_COUNT} 份)"
|
||||
sh """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
||||
docker images ${env.IMAGE_NAME} \\
|
||||
--format "{{.CreatedAt}}|{{.ID}}|{{.Tag}}" \\
|
||||
| grep -v "|latest\$" \\
|
||||
| sort -r \\
|
||||
| awk -F"|" -v k=${env.IMAGE_KEEP_COUNT} "NR > k {print \\\$2}" \\
|
||||
| sort -u \\
|
||||
| xargs -r docker rmi -f 2>/dev/null || true
|
||||
|
||||
docker image prune -f --filter "dangling=true" || true
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
script {
|
||||
def total = ((System.currentTimeMillis() - env.PIPELINE_START_TIME.toLong()) / 1000) as int
|
||||
echo """
|
||||
==========================================
|
||||
✅ BUILD SUCCESS
|
||||
==========================================
|
||||
App: ${env.APP_NAME}
|
||||
Tag: ${env.IMAGE_TAG}
|
||||
Target: ${env.DEPLOY_HOST}:${env.HOST_PORT}
|
||||
Total: ${total}s
|
||||
==========================================
|
||||
"""
|
||||
}
|
||||
}
|
||||
failure {
|
||||
script {
|
||||
echo "❌ BUILD FAILED — tag=${env.IMAGE_TAG ?: 'unknown'}, rollback_tag=${env.PREVIOUS_IMAGE_TAG ?: 'N/A'}"
|
||||
}
|
||||
}
|
||||
always {
|
||||
sh "docker image prune -f --filter 'dangling=true' || true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// helpers
|
||||
// ============================================
|
||||
|
||||
def getPreviousImageTag() {
|
||||
try {
|
||||
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
||||
def tag = sh(
|
||||
script: """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
||||
docker inspect --format="{{.Config.Image}}" ${env.CONTAINER_NAME} 2>/dev/null \
|
||||
| sed "s|.*:||" \
|
||||
|| echo "latest"
|
||||
' 2>/dev/null || echo "latest"
|
||||
""",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
return tag ?: 'latest'
|
||||
} catch (Exception e) {
|
||||
return 'latest'
|
||||
}
|
||||
}
|
||||
|
||||
def waitForHealthy(String sshOpts) {
|
||||
def maxAttempts = (env.HEALTH_CHECK_TIMEOUT.toInteger() / env.HEALTH_CHECK_INTERVAL.toInteger()) as int
|
||||
echo "⏳ 等待 ${env.APP_NAME} 健康(最多 ${env.HEALTH_CHECK_TIMEOUT}s)..."
|
||||
|
||||
sh """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
||||
set -e
|
||||
for i in \$(seq 1 ${maxAttempts}); do
|
||||
STATUS=\$(docker inspect --format="{{.State.Health.Status}}" ${env.CONTAINER_NAME} 2>/dev/null || echo "no-healthcheck")
|
||||
case "\$STATUS" in
|
||||
healthy)
|
||||
echo "✅ ${env.APP_NAME} healthy"; exit 0;;
|
||||
unhealthy)
|
||||
echo "❌ ${env.APP_NAME} unhealthy"
|
||||
docker logs --tail 100 ${env.CONTAINER_NAME}
|
||||
exit 1;;
|
||||
no-healthcheck)
|
||||
if curl -fsS http://localhost:${env.HOST_PORT}/ -o /dev/null; then
|
||||
echo "✅ ${env.APP_NAME} responds 200"; exit 0
|
||||
fi;;
|
||||
esac
|
||||
ELAPSED=\$((i * ${env.HEALTH_CHECK_INTERVAL}))
|
||||
echo "⏳ \${ELAPSED}s/${env.HEALTH_CHECK_TIMEOUT}s ..."
|
||||
sleep ${env.HEALTH_CHECK_INTERVAL}
|
||||
done
|
||||
echo "❌ Health check timeout"
|
||||
docker logs --tail 100 ${env.CONTAINER_NAME} || true
|
||||
exit 1
|
||||
'
|
||||
"""
|
||||
}
|
||||
|
||||
def rollback(String sshOpts) {
|
||||
sh """
|
||||
ssh ${sshOpts} root@${env.DEPLOY_HOST} '
|
||||
set -e
|
||||
cd ${env.DEPLOY_PATH}
|
||||
export IMAGE_TAG=${env.PREVIOUS_IMAGE_TAG}
|
||||
export REGISTRY_HOST=${env.REGISTRY}
|
||||
export HOST_PORT=${env.HOST_PORT}
|
||||
docker compose -f docker-compose.frontend.yml pull
|
||||
docker compose -f docker-compose.frontend.yml up -d --force-recreate
|
||||
'
|
||||
"""
|
||||
echo "✅ Rolled back ${env.APP_NAME} to ${env.PREVIOUS_IMAGE_TAG}"
|
||||
}
|
||||
|
||||
def checkRemoteDiskOrFail(String host, String role) {
|
||||
def sshOpts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${env.SSH_KEY}"
|
||||
def minPct = Integer.parseInt(env.DISK_FREE_MIN_PCT)
|
||||
def warnPct = Integer.parseInt(env.DISK_FREE_WARN_PCT)
|
||||
|
||||
def freePct
|
||||
try {
|
||||
freePct = sh(
|
||||
script: "ssh ${sshOpts} root@${host} \"df -P / | awk 'NR==2 { print 100 - \\\$5+0 }'\"",
|
||||
returnStdout: true
|
||||
).trim() as int
|
||||
} catch (Exception e) {
|
||||
error("❌ [${role}] SSH 到 ${host} 失败:${e.message}")
|
||||
}
|
||||
|
||||
echo " ${role}@${host}: 根分区空闲 ${freePct}%"
|
||||
if (freePct < minPct) {
|
||||
error("❌ [${role}] ${host} 空闲仅 ${freePct}% < ${minPct}%,终止部署")
|
||||
} else if (freePct < warnPct) {
|
||||
echo "⚠️ [${role}] ${host} 空闲 ${freePct}% < ${warnPct}%,部署后建议清理"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL=http://127.0.0.1:48080
|
||||
# 请求路径(容器部署时走 nginx 反代到后端 gateway,写相对路径即可)
|
||||
VITE_BASE_URL=
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:48080/admin-api
|
||||
VITE_GLOB_API_URL=/admin-api
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
|
||||
64
apps/web-antd/nginx.conf
Normal file
64
apps/web-antd/nginx.conf
Normal file
@@ -0,0 +1,64 @@
|
||||
# 配置 DNS 解析器(使用 Docker 内置 DNS 解析容器名)
|
||||
resolver 127.0.0.11 valid=30s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 开启 gzip 压缩
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# API 反向代理到后端服务
|
||||
# 后端容器名为 aiot-gateway,与本前端容器需在同一 Docker 网络(1panel-network)
|
||||
location /admin-api {
|
||||
client_max_body_size 100M;
|
||||
|
||||
proxy_pass http://aiot-gateway:48080/admin-api;
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 后端短暂不可达时尝试一次重试
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
||||
proxy_next_upstream_tries 2;
|
||||
|
||||
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;
|
||||
|
||||
# 跨域响应头(仅作兜底,正常同域不会触发)
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# SPA 路由刷新兜底
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
35
docker-compose.frontend.yml
Normal file
35
docker-compose.frontend.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
# ============================================
|
||||
# AIoT IoT UI - 部署 Compose
|
||||
# 由 Jenkins 在部署阶段同步到 ${DEPLOY_PATH}
|
||||
# 通过环境变量注入:
|
||||
# IMAGE_TAG 镜像版本号
|
||||
# REGISTRY_HOST 镜像仓库地址(含端口)
|
||||
# HOST_PORT 宿主机暴露端口
|
||||
# ============================================
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: 1panel-network
|
||||
external: true
|
||||
|
||||
services:
|
||||
aiot-iot-ui:
|
||||
image: ${REGISTRY_HOST:-172.17.16.7:5000}/aiot-iot-ui:${IMAGE_TAG:-latest}
|
||||
container_name: aiot-iot-ui
|
||||
restart: on-failure:5
|
||||
ports:
|
||||
- "${HOST_PORT:-90}:80"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O- http://localhost/ >/dev/null 2>&1 || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
start_period: 15s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256m
|
||||
cpus: '0.5'
|
||||
@@ -15,7 +15,7 @@ COPY . /app
|
||||
|
||||
# 安装依赖
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build --filter=\!./docs
|
||||
RUN pnpm exec turbo build --filter=@vben/web-antd --concurrency=1
|
||||
|
||||
RUN echo "Builder Success 🎉"
|
||||
|
||||
@@ -25,8 +25,8 @@ FROM nginx:stable-alpine AS production
|
||||
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf \
|
||||
&& rm -rf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/playground/dist /usr/share/nginx/html
|
||||
# 复制构建产物(vben monorepo 主入口为 apps/web-antd)
|
||||
COPY --from=builder /app/apps/web-antd/dist /usr/share/nginx/html
|
||||
|
||||
# 复制 nginx 配置
|
||||
COPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
Reference in New Issue
Block a user