Add CI/CD pipeline and K8S deployment
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m13s
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:
parent
304b8e9746
commit
f42eb64e25
90
.gitea/workflows/deploy.yaml
Normal file
90
.gitea/workflows/deploy.yaml
Normal 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
5
backend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
.env
|
||||
28
backend/Dockerfile
Normal file
28
backend/Dockerfile
Normal 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"]
|
||||
@ -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
|
||||
|
||||
@ -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')),
|
||||
|
||||
80
k8s/backend-deployment.yaml
Normal file
80
k8s/backend-deployment.yaml
Normal 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
36
k8s/ingress.yaml
Normal 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
57
k8s/web-deployment.yaml
Normal 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
5
web/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test/
|
||||
*.md
|
||||
.env
|
||||
22
web/Dockerfile
Normal file
22
web/Dockerfile
Normal 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
28
web/nginx.conf
Normal 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";
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user