diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100755 index 4ace229..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,66 +0,0 @@ -pipeline { - agent any - - environment { - REGISTRY = 'git.virtheart.com' - REGISTRY_OWNER = 'virtheart' - IMAGE_NAME = "${env.JOB_NAME.replaceAll(/[^a-zA-Z0-9.-]/, '-').toLowerCase()}" - IMAGE_TAG = 'latest' - CONTAINER_PORT = '3000' - HOST_PORT = '3010' - } - - stages { - stage('Checkout') { - steps { - checkout scm - } - } - - stage('Build Image') { - steps { - script { - def imageFullName = "${REGISTRY}/${REGISTRY_OWNER}/${IMAGE_NAME}:${IMAGE_TAG}" - sh "docker build -t ${imageFullName} ." - } - } - } - - stage('Push Image') { - steps { - withCredentials([usernamePassword(credentialsId: 'docker-registry-credentials', usernameVariable: 'REGISTRY_USERNAME', passwordVariable: 'REGISTRY_PASSWORD')]) { - script { - def imageFullName = "${REGISTRY}/${REGISTRY_OWNER}/${IMAGE_NAME}:${IMAGE_TAG}" - sh ''' - set +x - echo $REGISTRY_PASSWORD | docker login $REGISTRY -u $REGISTRY_USERNAME --password-stdin - docker push ''' + imageFullName - } - } - } - } - - stage('Deploy Container') { - steps { - script { - def imageFullName = "${REGISTRY}/${REGISTRY_OWNER}/${IMAGE_NAME}:${IMAGE_TAG}" - sh """ - docker stop $IMAGE_NAME || true - docker rm $IMAGE_NAME || true - docker pull $imageFullName - docker run -d --name $IMAGE_NAME \ - -p $HOST_PORT:$CONTAINER_PORT \ - --restart unless-stopped \ - $imageFullName - """ - } - } - } - } - - post { - always { - cleanWs() - } - } -} \ No newline at end of file diff --git a/README.md b/README.md index 5f371c5..eacdd0b 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ pnpm dev │ └── locales/ # 国际化文案(zh.json / en.json) ├── public/ # 公开静态文件(favicon、robots.txt 等) ├── Dockerfile # 容器镜像构建 -├── Jenkinsfile # CI/CD 流水线(构建、推送镜像、部署容器) -├── docker-compose.yml # 本地容器编排 +├── deploy/ # 生产部署配置(Kubernetes Deployment/Service/Ingress) +├── docker-compose.yml # 本地容器编排(使用 SWR 镜像) ├── nuxt.config.ts # Nuxt 配置(模块、i18n、Tailwind 等) ├── tailwind.config.js # Tailwind 配置 └── package.json # 项目依赖与脚本 @@ -69,19 +69,21 @@ pnpm dev ## 构建与部署 -生产构建: +本地构建: ```bash -pnpm build +docker build -t swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest . ``` -构建产物输出到 `.output/`,可直接通过 `pnpm start` 或容器运行。 +本地启动(使用 SWR 镜像,与生产一致): -当前部署相关文件位于仓库根目录(`deploy/` 目录暂未创建,后续按团队规范存放生产部署 k8s 配置): +```bash +docker compose up -d +``` -- `Dockerfile` — 多阶段构建:安装依赖 → 构建 → 仅保留 `.output/` 运行产物 -- `Jenkinsfile` — Jenkins 流水线:构建镜像 → 推送到镜像仓库 → 部署容器 -- `docker-compose.yml` — 本地容器编排,宿主机 3100 端口映射容器 3000 端口 +生产部署使用 `deploy/` 目录下的 Kubernetes 配置(Deployment / Service / Ingress), +镜像统一推送到华为云 SWR(`swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest`), +部署与回滚步骤见 [deploy/README.md](deploy/README.md)。 ## 国际化(i18n) diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..cd790d8 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,57 @@ +# Arcticnewone 生产部署配置 + +本目录存放 Arcticnewone 个人网站的 Kubernetes 生产部署配置(华为云 SWR 镜像 + Traefik Ingress)。 + +## 文件说明 + +| 文件 | 说明 | +| --- | --- | +| `deployment.yaml` | Deployment:SWR 镜像、副本数 1、私有镜像拉取 secrets | +| `service.yaml` | Service:ClusterIP,80 端口转发到容器 3000 端口 | +| `ingress.yaml` | Ingress:域名 `personal.virtheart.com`,Traefik + cert-manager 自动签发证书 | + +## 部署前置条件 + +1. 镜像已构建并推送到华为云 SWR: + `swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest`(多架构 `linux/amd64,linux/arm64`,tag 统一为 `latest`) +2. 集群已安装 Traefik Ingress 与 cert-manager(ClusterIssuer 名称为 `lets-encrypt`) +3. 域名 `personal.virtheart.com` 的 DNS 已解析到集群入口 +4. 创建私有镜像拉取 Secret(凭据从环境变量读取,绝不写入仓库): + + ```bash + kubectl create secret docker-registry huawei-swr-secret \ + --docker-server="$SWR_REGISTRY" \ + --docker-username="$SWR_USERNAME" \ + --docker-password="$SWR_PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - + ``` + +## 部署步骤 + +```bash +kubectl apply -f deploy/deployment.yaml +kubectl apply -f deploy/service.yaml +kubectl apply -f deploy/ingress.yaml +``` + +部署后验证: + +```bash +kubectl get deploy,svc,ingress +kubectl rollout status deployment/arcticnewone +curl -I https://personal.virtheart.com +``` + +## 回滚方案 + +镜像 tag 固定为 `latest`,回滚前先确认 SWR 上 `latest` 对应可回滚的版本(或先推送目标版本镜像),再执行: + +```bash +# 回滚到上一版本镜像 +kubectl rollout undo deployment/arcticnewone + +# 或整体移除本次部署的资源 +kubectl delete -f deploy/ingress.yaml +kubectl delete -f deploy/service.yaml +kubectl delete -f deploy/deployment.yaml +``` diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml new file mode 100644 index 0000000..9e59830 --- /dev/null +++ b/deploy/deployment.yaml @@ -0,0 +1,70 @@ +# ========================================================= +# Arcticnewone 生产部署 - Deployment +# +# 镜像: 华为云 SWR swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest +# 副本数: 1(团队规范) +# +# 私有镜像拉取说明: +# 集群中需预先创建 docker-registry 类型的 Secret(凭据从环境变量读取,不写入仓库): +# kubectl create secret docker-registry huawei-swr-secret \ +# --docker-server="$SWR_REGISTRY" \ +# --docker-username="$SWR_USERNAME" \ +# --docker-password="$SWR_PASSWORD" \ +# --dry-run=client -o yaml | kubectl apply -f - +# 详见 deploy/README.md +# ========================================================= +apiVersion: apps/v1 +kind: Deployment +metadata: + name: arcticnewone + labels: + app: arcticnewone +spec: + replicas: 1 + selector: + matchLabels: + app: arcticnewone + template: + metadata: + labels: + app: arcticnewone + spec: + imagePullSecrets: + - name: huawei-swr-secret + containers: + - name: arcticnewone + image: swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "3000" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 3 + failureThreshold: 3 diff --git a/deploy/ingress.yaml b/deploy/ingress.yaml new file mode 100644 index 0000000..e81e591 --- /dev/null +++ b/deploy/ingress.yaml @@ -0,0 +1,29 @@ +# ========================================================= +# Arcticnewone 生产部署 - Ingress +# +# 域名: personal.virtheart.com(已在 issue 评论中确认) +# 按团队统一 Ingress 模板配置,结构不可随意改动。 +# ========================================================= +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: arcticnewone-ingress + annotations: + cert-manager.io/cluster-issuer: "lets-encrypt" +spec: + ingressClassName: traefik + tls: + - hosts: + - personal.virtheart.com + secretName: arcticnewone-tls + rules: + - host: personal.virtheart.com + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: arcticnewone-svc # 注意:必须与 Service 名称一致 + port: + number: 80 diff --git a/deploy/service.yaml b/deploy/service.yaml new file mode 100644 index 0000000..15e2d52 --- /dev/null +++ b/deploy/service.yaml @@ -0,0 +1,20 @@ +# ========================================================= +# Arcticnewone 生产部署 - Service +# +# 容器监听 3000 端口(Dockerfile 中 EXPOSE 3000), +# Service 暴露 80 端口,与团队 Ingress 模板的 backend port 80 保持一致。 +# ========================================================= +apiVersion: v1 +kind: Service +metadata: + name: arcticnewone-svc + labels: + app: arcticnewone +spec: + type: ClusterIP + selector: + app: arcticnewone + ports: + - name: http + port: 80 + targetPort: 3000 diff --git a/docker-compose.yml b/docker-compose.yml index cd19994..a077e1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: app: - build: . + # 生产与本地使用同一 SWR 镜像(团队规范) + image: swr.cn-north-4.myhuaweicloud.com/virtheart/arcticnewone:latest container_name: arcticnewone ports: - "3100:3000"