diff --git a/.gitea/workflows/deploy-backend.yaml b/.gitea/workflows/deploy-backend.yaml new file mode 100644 index 0000000..5422bcf --- /dev/null +++ b/.gitea/workflows/deploy-backend.yaml @@ -0,0 +1,59 @@ +name: Build and Deploy Backend + +on: + push: + branches: + - main + - master + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + config-inline: | + [registry."docker.io"] + mirrors = ["https://docker.m.daocloud.io", "https://docker.1panel.live", "https://hub.rat.dev"] + + - name: Login to Huawei Cloud SWR + uses: docker/login-action@v2 + with: + registry: ${{ secrets.SWR_SERVER }} + username: ${{ secrets.SWR_USERNAME }} + password: ${{ secrets.SWR_PASSWORD }} + + - name: Build and Push Backend + uses: docker/build-push-action@v4 + with: + context: ./backend + push: true + provenance: false + tags: ${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/airlabs-manage-backend:latest + + - name: Setup Kubectl + run: | + curl -LO "https://files.m.daocloud.io/dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Deploy to K3s + uses: Azure/k8s-set-context@v3 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBE_CONFIG }} + + - name: Update K8s Manifests + run: | + # 替换镜像地址 + sed -i "s|\${CI_REGISTRY_IMAGE}/backend:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/airlabs-manage-backend:latest|g" k8s/backend-deployment-prod.yaml + + # 应用配置 + kubectl apply -f k8s/backend-deployment-prod.yaml + kubectl apply -f k8s/backend-ingress.yaml + + kubectl rollout restart deployment/airlabs-manage-backend diff --git a/.gitea/workflows/deploy-web.yaml b/.gitea/workflows/deploy-web.yaml new file mode 100644 index 0000000..f8fd68e --- /dev/null +++ b/.gitea/workflows/deploy-web.yaml @@ -0,0 +1,59 @@ +name: Build and Deploy Web + +on: + push: + branches: + - main + - master + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + config-inline: | + [registry."docker.io"] + mirrors = ["https://docker.m.daocloud.io", "https://docker.1panel.live", "https://hub.rat.dev"] + + - name: Login to Huawei Cloud SWR + uses: docker/login-action@v2 + with: + registry: ${{ secrets.SWR_SERVER }} + username: ${{ secrets.SWR_USERNAME }} + password: ${{ secrets.SWR_PASSWORD }} + + - name: Build and Push Web + uses: docker/build-push-action@v4 + with: + context: ./frontend + push: true + provenance: false + tags: ${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/airlabs-manage-web:latest + + - name: Setup Kubectl + run: | + curl -LO "https://files.m.daocloud.io/dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Deploy to K3s + uses: Azure/k8s-set-context@v3 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBE_CONFIG }} + + - name: Update K8s Manifests + run: | + # 替换镜像地址 + sed -i "s|\${CI_REGISTRY_IMAGE}/web:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/airlabs-manage-web:latest|g" k8s/web-deployment-prod.yaml + + # 应用配置 + kubectl apply -f k8s/web-deployment-prod.yaml + kubectl apply -f k8s/web-ingress.yaml + + kubectl rollout restart deployment/airlabs-manage-web diff --git a/.gitignore b/.gitignore index 6fed677..fbd45e6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,15 @@ __pycache__/ # Node node_modules/ +# Database +*.db + +# Build output +frontend/dist/ + +# Environment +.env + # OS .DS_Store Thumbs.db diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..dd893a2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Install system dependencies +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \ + apt-get update && apt-get install -y gcc && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt /app/ +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \ + pip install --upgrade pip && pip install -r requirements.txt + +# Copy project +COPY . /app/ + +# Create data directory for SQLite persistence +RUN mkdir -p /app/data + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..967c922 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +# Build stage +FROM node:20-alpine AS build-stage + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +# Production stage +FROM nginx:stable-alpine AS production-stage + +COPY --from=build-stage /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..04c79ae --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name localhost; + + # API 代理到后端服务(K8s 内部通信) + location /api { + proxy_pass http://airlabs-manage-backend:8000; + 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; + } + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + # SPA 路由刷新 404 的关键配置 + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location /assets/ { + root /usr/share/nginx/html; + expires 7d; + add_header Cache-Control "public"; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/k8s/backend-deployment-prod.yaml b/k8s/backend-deployment-prod.yaml new file mode 100644 index 0000000..244dea2 --- /dev/null +++ b/k8s/backend-deployment-prod.yaml @@ -0,0 +1,67 @@ +# SQLite 持久化存储 +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: airlabs-manage-sqlite-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: airlabs-manage-backend + labels: + app: airlabs-manage-backend +spec: + replicas: 1 + selector: + matchLabels: + app: airlabs-manage-backend + template: + metadata: + labels: + app: airlabs-manage-backend + spec: + containers: + - name: airlabs-manage-backend + image: ${CI_REGISTRY_IMAGE}/backend:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + env: + # SQLite 存储在持久卷中 + - name: DATABASE_URL + value: "sqlite:////app/data/airlabs.db" + # 生产环境 JWT 密钥(部署前请修改) + - name: SECRET_KEY + value: "Ui5-xEvtAhKRDtlXKzDfd7TElsVZFUhakff0qcjn8jU" + volumeMounts: + - name: sqlite-data + mountPath: /app/data + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: sqlite-data + persistentVolumeClaim: + claimName: airlabs-manage-sqlite-data +--- +apiVersion: v1 +kind: Service +metadata: + name: airlabs-manage-backend +spec: + selector: + app: airlabs-manage-backend + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 diff --git a/k8s/backend-ingress.yaml b/k8s/backend-ingress.yaml new file mode 100644 index 0000000..0696705 --- /dev/null +++ b/k8s/backend-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: airlabs-manage-backend-ingress + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - airlabs-manage-api.airlabs.art + secretName: airlabs-manage-backend-tls + rules: + - host: airlabs-manage-api.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: airlabs-manage-backend + port: + number: 8000 diff --git a/k8s/web-deployment-prod.yaml b/k8s/web-deployment-prod.yaml new file mode 100644 index 0000000..06001f8 --- /dev/null +++ b/k8s/web-deployment-prod.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: airlabs-manage-web + labels: + app: airlabs-manage-web +spec: + replicas: 1 + selector: + matchLabels: + app: airlabs-manage-web + template: + metadata: + labels: + app: airlabs-manage-web + spec: + containers: + - name: airlabs-manage-web + image: ${CI_REGISTRY_IMAGE}/web:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: airlabs-manage-web +spec: + selector: + app: airlabs-manage-web + ports: + - protocol: TCP + port: 80 + targetPort: 80 diff --git a/k8s/web-ingress.yaml b/k8s/web-ingress.yaml new file mode 100644 index 0000000..082d9a5 --- /dev/null +++ b/k8s/web-ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: airlabs-manage-web-ingress + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - airlabs-manage-web.airlabs.art + secretName: airlabs-manage-web-tls + rules: + - host: airlabs-manage-web.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: airlabs-manage-web + port: + number: 80