Add CI/CD pipeline and K8S deployment
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m13s

- Backend/Web Dockerfiles with multi-stage builds
- K8S deployments, services, and ingress for both domains
- Gitea Actions workflow: build → push to SWR → deploy to K3s
- Health check endpoint (/healthz/)
- CORS env var support for production domains
- Nginx reverse proxy for frontend → backend API

Domains:
  - video-huoshan-api.airlabs.art (backend)
  - video-huoshan-web.airlabs.art (frontend)
This commit is contained in:
zyc 2026-03-13 10:24:31 +08:00
parent 304b8e9746
commit f42eb64e25
11 changed files with 363 additions and 0 deletions

View File

@ -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}"

5
backend/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
venv/
__pycache__/
*.pyc
db.sqlite3
.env

28
backend/Dockerfile Normal file
View File

@ -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"]

View File

@ -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

View File

@ -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')),

View File

@ -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

36
k8s/ingress.yaml Normal file
View File

@ -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

57
k8s/web-deployment.yaml Normal file
View File

@ -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

5
web/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
test/
*.md
.env

22
web/Dockerfile Normal file
View File

@ -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;"]

28
web/nginx.conf Normal file
View File

@ -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";
}
}