diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..42b273a --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,90 @@ +name: Build and Deploy + +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 + run: | + set -o pipefail + docker buildx build \ + --push \ + --provenance=false \ + --tag ${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-backend:latest \ + ./backend 2>&1 | tee /tmp/build.log + + - name: Build and Push Web + run: | + set -o pipefail + docker buildx build \ + --push \ + --provenance=false \ + --tag ${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-web:latest \ + ./web 2>&1 | tee -a /tmp/build.log + + - name: Setup Kubectl + run: | + curl -LO "https://dl.k8s.io/release/v1.28.2/bin/linux/amd64/kubectl" || \ + curl -LO "https://cdn.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: Apply K8s Manifests + run: | + # Replace image placeholders + sed -i "s|\${CI_REGISTRY_IMAGE}/video-backend:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-backend:latest|g" k8s/backend-deployment.yaml + sed -i "s|\${CI_REGISTRY_IMAGE}/video-web:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-web:latest|g" k8s/web-deployment.yaml + + # Apply all manifests + set -o pipefail + { + kubectl apply -f k8s/backend-deployment.yaml + kubectl apply -f k8s/web-deployment.yaml + kubectl apply -f k8s/ingress.yaml + kubectl rollout restart deployment/video-backend + kubectl rollout restart deployment/video-web + } 2>&1 | tee /tmp/deploy.log + + - name: Report failure + if: failure() + run: | + BUILD_LOG="" + DEPLOY_LOG="" + if [ -f /tmp/build.log ]; then + BUILD_LOG=$(tail -50 /tmp/build.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi + if [ -f /tmp/deploy.log ]; then + DEPLOY_LOG=$(tail -50 /tmp/deploy.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi + echo "Build failed on branch ${{ github.ref_name }}" + echo "Build log: ${BUILD_LOG}" + echo "Deploy log: ${DEPLOY_LOG}" diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..5e3b6fc --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +*.pyc +db.sqlite3 +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3469c83 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# System dependencies (Aliyun mirror for China) +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 \ + default-libmysqlclient-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# 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 . /app/ + +# Collect static files +RUN python manage.py collectstatic --noinput 2>/dev/null || true + +EXPOSE 8000 + +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "config.wsgi:application"] diff --git a/backend/config/settings.py b/backend/config/settings.py index 9e3fa52..3f3165d 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -119,8 +119,13 @@ CORS_ALLOWED_ORIGINS = [ 'http://127.0.0.1:5173', 'http://localhost:3000', ] +_extra_cors = os.environ.get('CORS_ALLOWED_ORIGINS', '') +if _extra_cors: + CORS_ALLOWED_ORIGINS += [o.strip() for o in _extra_cors.split(',') if o.strip()] CORS_ALLOW_CREDENTIALS = True +CSRF_TRUSTED_ORIGINS = [o for o in CORS_ALLOWED_ORIGINS if o.startswith('https://')] + LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True diff --git a/backend/config/urls.py b/backend/config/urls.py index 914a151..ccddedc 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -1,7 +1,14 @@ from django.contrib import admin from django.urls import path, include +from django.http import JsonResponse + + +def healthz(request): + return JsonResponse({'status': 'ok'}) + urlpatterns = [ + path('healthz/', healthz), path('admin/', admin.site.urls), path('api/v1/auth/', include('apps.accounts.urls')), path('api/v1/', include('apps.generation.urls')), diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml new file mode 100644 index 0000000..b03e2e9 --- /dev/null +++ b/k8s/backend-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: video-backend + labels: + app: video-backend +spec: + replicas: 1 + selector: + matchLabels: + app: video-backend + template: + metadata: + labels: + app: video-backend + spec: + containers: + - name: video-backend + image: ${CI_REGISTRY_IMAGE}/video-backend:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + env: + - name: USE_MYSQL + value: "true" + - name: DJANGO_DEBUG + value: "False" + - name: DJANGO_ALLOWED_HOSTS + value: "*" + - name: DJANGO_SECRET_KEY + value: "video-huoshan-prod-secret-key-2026" + # Database (Aliyun RDS) + - name: DB_HOST + value: "rm-7xv1uaw910558p1788o.mysql.rds.aliyuncs.com" + - name: DB_NAME + value: "video_auto" + - name: DB_USER + value: "ai_video" + - name: DB_PASSWORD + value: "JogNQdtrd3WY8CBCAiYfYEGx" + - name: DB_PORT + value: "3306" + # CORS + - name: CORS_ALLOWED_ORIGINS + value: "https://video-huoshan-web.airlabs.art" + livenessProbe: + httpGet: + path: /healthz/ + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz/ + port: 8000 + initialDelaySeconds: 15 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "1024Mi" + cpu: "1000m" +--- +apiVersion: v1 +kind: Service +metadata: + name: video-backend +spec: + selector: + app: video-backend + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..3562df1 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,36 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: video-huoshan-ingress + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + tls: + - hosts: + - video-huoshan-api.airlabs.art + secretName: video-huoshan-api-tls + - hosts: + - video-huoshan-web.airlabs.art + secretName: video-huoshan-web-tls + rules: + - host: video-huoshan-api.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: video-backend + port: + number: 8000 + - host: video-huoshan-web.airlabs.art + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: video-web + port: + number: 80 diff --git a/k8s/web-deployment.yaml b/k8s/web-deployment.yaml new file mode 100644 index 0000000..121435b --- /dev/null +++ b/k8s/web-deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: video-web + labels: + app: video-web +spec: + replicas: 1 + selector: + matchLabels: + app: video-web + template: + metadata: + labels: + app: video-web + spec: + containers: + - name: video-web + image: ${CI_REGISTRY_IMAGE}/video-web:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "250m" +--- +apiVersion: v1 +kind: Service +metadata: + name: video-web +spec: + selector: + app: video-web + ports: + - protocol: TCP + port: 80 + targetPort: 80 diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..c17fba4 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +test/ +*.md +.env diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..6d54884 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,22 @@ +# ---- Build Stage ---- +FROM node:18-alpine AS builder + +RUN npm config set registry https://registry.npmmirror.com + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# ---- Runtime Stage ---- +FROM nginx:alpine + +RUN sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..29e97c3 --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # API requests proxy to backend service + location /api/ { + proxy_pass http://video-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; + proxy_read_timeout 120s; + client_max_body_size 50m; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } +}