使用 Kubernetes 部署 DjangoBlog:从零到上线的完整配置指南

Python 2026-04-29 181
预计阅读时间:16 分钟

本文详细介绍如何使用 Kubernetes 部署 DjangoBlog,包括存储配置、服务编排、Nginx 反向代理、HTTPS 证书自动签发,以及 Whoosh 和 Elasticsearch 两种搜索引擎的配置方法。

项目 Docker 镜像已发布到 Docker Hub:liangliangyy/djangoblog

整体架构

部署完成后,整个系统由以下组件构成:

Internet
    │
    ▼
Ingress (ingress-nginx + cert-manager)
    │  HTTPS TLS 1.2/1.3
    ▼
Nginx Pod              ← 静态文件服务 + 反向代理
    │
    ▼
DjangoBlog Pod         ← Gunicorn,端口 8000
    ├── MySQL Pod      ← 数据库,端口 3306
    ├── Redis Pod      ← 缓存,端口 6379
    └── Elasticsearch  ← 搜索(可选),端口 9200

各组件通过 Kubernetes ClusterIP Service 互联,外部流量只通过 Ingress 进入,内部服务不对外暴露。

前置条件

K8s 集群中需要提前安装:

  • ingress-nginx:处理外部 HTTP/HTTPS 流量入口
  • cert-manager:自动申请和续期 Let's Encrypt 证书
# 安装 ingress-nginx
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml

# 安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# 创建 Let's Encrypt ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
EOF

环境变量说明

DjangoBlog 的 settings.py 已经将所有关键配置通过环境变量读取,用户无需编写任何额外的 settings 文件,只需在 ConfigMap 里配置对应的环境变量即可。

完整的环境变量列表:

环境变量 说明 示例值
DJANGO_SECRET_KEY Django 密钥(必填) 随机长字符串
DJANGO_DEBUG 调试模式 False
DJANGO_MYSQL_DATABASE 数据库名 djangoblog
DJANGO_MYSQL_USER 数据库用户 root
DJANGO_MYSQL_PASSWORD 数据库密码 your-password
DJANGO_MYSQL_HOST 数据库地址 db(K8s Service 名)
DJANGO_MYSQL_PORT 数据库端口 3306
DJANGO_REDIS_URL Redis 地址(设置后自动启用 Redis 缓存) redis:6379
DJANGO_ELASTICSEARCH_HOST ES 地址(设置后自动切换 ES 搜索) http://elasticsearch:9200
ELASTICSEARCH_USERNAME ES 用户名(可选) elastic
ELASTICSEARCH_PASSWORD ES 密码(可选) your-es-password
ELASTICSEARCH_API_KEY ES API Key(可选) your-api-key
DJANGO_EMAIL_USER 发件邮箱 [email protected]
DJANGO_EMAIL_PASSWORD 邮箱密码 your-email-password
DJANGO_EMAIL_HOST SMTP 服务器 smtp.exmail.qq.com
DJANGO_EMAIL_PORT SMTP 端口 587
MYSQL_ROOT_PASSWORD MySQL root 密码(MySQL 容器初始化用) DJANGO_MYSQL_PASSWORD 相同
MYSQL_DATABASE MySQL 初始化数据库名 djangoblog

第一步:创建命名空间

kubectl create namespace djangoblog

第二步:StorageClass 与持久化存储

StorageClass

使用本地存储,适合单节点或自建集群:

# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

PersistentVolume

在宿主机节点上提前创建目录:

mkdir -p /mnt/local-storage-db
mkdir -p /mnt/local-storage-djangoblog
mkdir -p /mnt/resource
# 如果使用 Elasticsearch,还需要:
mkdir -p /mnt/local-storage-elasticsearch

对应的 PV 配置:

# pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-db
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/local-storage-db
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - <your-node-name>   # 替换为实际节点名
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-djangoblog
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/local-storage-djangoblog
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - <your-node-name>
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-resource
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt/resource
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - <your-node-name>

各 PV 用途说明:

PV 名称 大小 宿主机路径 用途
local-pv-db 10Gi /mnt/local-storage-db MySQL 数据
local-pv-djangoblog 5Gi /mnt/local-storage-djangoblog Django collectstatic 静态文件
local-pv-resource 5Gi /mnt/resource 用户上传资源文件
local-pv-elasticsearch 5Gi /mnt/local-storage-elasticsearch ES 数据(使用 ES 时才需要)

PVC

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
  namespace: djangoblog
spec:
  storageClassName: local-storage
  volumeName: local-pv-db
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: djangoblog-pvc
  namespace: djangoblog
spec:
  storageClassName: local-storage
  volumeName: local-pv-djangoblog
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: resource-pvc
  namespace: djangoblog
spec:
  storageClassName: local-storage
  volumeName: local-pv-resource
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

第三步:ConfigMap 配置

环境变量 ConfigMap

这是最核心的配置,将所有必要参数通过环境变量注入到 Django 容器:

# configmap.yaml(djangoblog-env)
apiVersion: v1
kind: ConfigMap
metadata:
  name: djangoblog-env
  namespace: djangoblog
data:
  # 数据库
  DJANGO_MYSQL_DATABASE: djangoblog
  DJANGO_MYSQL_USER: root
  DJANGO_MYSQL_PASSWORD: <your-mysql-password>
  DJANGO_MYSQL_HOST: db          # K8s Service 名,Pod 间 DNS 解析
  DJANGO_MYSQL_PORT: "3306"
  MYSQL_ROOT_PASSWORD: <your-mysql-password>
  MYSQL_DATABASE: djangoblog

  # Redis 缓存(设置后自动启用 Redis 替代内存缓存)
  DJANGO_REDIS_URL: "redis:6379"

  # Django 基础配置
  DJANGO_DEBUG: "False"
  DJANGO_SECRET_KEY: <your-secret-key>   # 生产环境必须使用随机长字符串

  # 邮件通知(可选)
  DJANGO_EMAIL_USER: [email protected]
  DJANGO_EMAIL_PASSWORD: <your-email-password>
  DJANGO_EMAIL_HOST: smtp.exmail.qq.com
  DJANGO_EMAIL_PORT: "587"

  # Elasticsearch(可选,设置后自动切换搜索引擎)
  # DJANGO_ELASTICSEARCH_HOST: http://elasticsearch:9200

注意DJANGO_MYSQL_HOST: dbDJANGO_REDIS_URL: redis:6379 使用 K8s Service 名称。Pod 之间通过集群内 DNS 解析,无需配置 IP。

Nginx ConfigMap

# configmap.yaml(web-nginx-config)
apiVersion: v1
kind: ConfigMap
metadata:
  name: web-nginx-config
  namespace: djangoblog
data:
  nginx.conf: |
    user  nginx;
    worker_processes  auto;
    events {
        worker_connections  1024;
        use epoll;
    }
    http {
        include /etc/nginx/mime.types;
        default_type  application/octet-stream;
        sendfile on;
        keepalive_timeout 65;
        gzip on;
        gzip_comp_level 8;
        gzip_types text/plain application/javascript text/css application/xml;
        include /etc/nginx/conf.d/*.conf;
    }

  djangoblog.conf: |
    server {
        server_name yourdomain.com;
        listen 80;
        keepalive_timeout 70;

        # 静态文件直接由 Nginx 服务,不经过 Django
        location /static/ {
            expires max;
            alias /code/djangoblog/collectedstatic/;
        }

        # 动态请求转发给 DjangoBlog(gunicorn)
        location / {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            if (!-f $request_filename) {
                proxy_pass http://djangoblog:8000;
                break;
            }
        }
    }
    server {
        server_name www.yourdomain.com;
        listen 80;
        return 301 https://yourdomain.com$request_uri;
    }

Nginx 和 DjangoBlog 共享同一个 djangoblog-pvc,Nginx 直接读取 collectedstatic/ 里的静态文件,不经过 Python 进程。

第四步:Deployment 配置

DjangoBlog

# deployment.yaml(djangoblog 部分)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: djangoblog
  namespace: djangoblog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: djangoblog
  template:
    metadata:
      labels:
        app: djangoblog
    spec:
      # initContainer:每次 Pod 启动时重建 Whoosh 搜索索引
      # 使用 Elasticsearch 时不需要此步骤,可删除
      initContainers:
      - name: init-whoosh-index
        image: liangliangyy/djangoblog:latest
        command: ["python", "manage.py", "rebuild_index", "--noinput"]
        workingDir: /code/djangoblog
        envFrom:
        - configMapRef:
            name: djangoblog-env

      containers:
      - name: djangoblog
        image: liangliangyy/djangoblog:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
        envFrom:
        - configMapRef:
            name: djangoblog-env
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 30
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 30
        resources:
          requests:
            cpu: 10m
            memory: 100Mi
          limits:
            cpu: "2"
            memory: 2Gi
        volumeMounts:
        - name: djangoblog
          mountPath: /code/djangoblog/collectedstatic
        - name: resource
          mountPath: /code/djangoblog/static/

      volumes:
      - name: djangoblog
        persistentVolumeClaim:
          claimName: djangoblog-pvc
      - name: resource
        persistentVolumeClaim:
          claimName: resource-pvc

MySQL

apiVersion: apps/v1
kind: Deployment
metadata:
  name: db
  namespace: djangoblog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
      - name: db
        image: mysql:latest
        command:
        - mysqld
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        - --max_connections=50
        - --innodb_buffer_pool_size=128M
        - --performance_schema=OFF    # 关闭性能监控,节省内存
        envFrom:
        - configMapRef:
            name: djangoblog-env
        readinessProbe:
          exec:
            command: ["mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p$(MYSQL_ROOT_PASSWORD)"]
          initialDelaySeconds: 10
          periodSeconds: 30
        resources:
          requests:
            cpu: 50m
            memory: 200Mi
          limits:
            cpu: 500m
            memory: 512Mi
        volumeMounts:
        - name: db-data
          mountPath: /var/lib/mysql
      volumes:
      - name: db-data
        persistentVolumeClaim:
          claimName: db-pvc

Redis 与 Nginx

# Redis
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: djangoblog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:latest
        ports:
        - containerPort: 6379
        resources:
          limits:
            cpu: 200m
            memory: 256Mi
---
# Nginx
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: djangoblog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: djangoblog.conf
        - name: djangoblog-pvc
          mountPath: /code/djangoblog/collectedstatic   # 与 Django 共享静态文件
      volumes:
      - name: nginx-config
        configMap:
          name: web-nginx-config
      - name: djangoblog-pvc
        persistentVolumeClaim:
          claimName: djangoblog-pvc

Elasticsearch(可选)

项目内置了带 IK 中文分词插件的 ES 镜像,默认 replicas: 0 不启动,需要时改为 1:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: elasticsearch
  namespace: djangoblog
spec:
  replicas: 0    # 需要 ES 时改为 1
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: liangliangyy/elasticsearch-analysis-ik:8.6.1   # 内置 IK 分词
        env:
        - name: discovery.type
          value: single-node
        - name: ES_JAVA_OPTS
          value: "-Xms256m -Xmx256m"
        - name: xpack.security.enabled
          value: "false"
        ports:
        - containerPort: 9200
        resources:
          limits:
            cpu: "2"
            memory: 2Gi
        volumeMounts:
        - name: elasticsearch-data
          mountPath: /usr/share/elasticsearch/data/
      volumes:
      - name: elasticsearch-data
        persistentVolumeClaim:
          claimName: elasticsearch-pvc

第五步:Service 配置

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: djangoblog
  namespace: djangoblog
spec:
  selector:
    app: djangoblog
  ports:
    - port: 8000
      targetPort: 8000
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: djangoblog
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: db
  namespace: djangoblog
spec:
  selector:
    app: db
  ports:
    - port: 3306
      targetPort: 3306
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: djangoblog
spec:
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: djangoblog
spec:
  selector:
    app: elasticsearch
  ports:
    - port: 9200
      targetPort: 9200
  type: ClusterIP

第六步:Ingress 配置

# gateway.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
  namespace: djangoblog
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
spec:
  ingressClassName: nginx
  rules:
    - host: yourdomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx
                port:
                  number: 80
    - host: www.yourdomain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx
                port:
                  number: 80
  tls:
  - hosts:
    - yourdomain.com
    - www.yourdomain.com
    secretName: djangoblog-tls-secret   # cert-manager 自动创建

搜索引擎配置详解

DjangoBlog 支持两种搜索引擎,settings.py 会根据环境变量自动切换,无需修改代码:

方案一:Whoosh(默认,轻量推荐)

不需要任何额外配置,部署后开箱即用。Whoosh 是纯 Python 全文搜索引擎,使用 Jieba 做中文分词。

索引数据存储在容器内 BASE_DIR/whoosh_index 目录。由于 Deployment 中的 initContainer 在每次 Pod 启动时会自动执行 rebuild_index,索引会自动重建,无需持久化。

# settings.py 中的默认配置(无需手动设置)
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    }
}

优点:部署简单,无需额外 Pod,内存占用极低。 缺点:文章量大时重建索引较慢,搜索能力弱于 ES。

方案二:Elasticsearch(高性能可选)

ES 使用 IK 中文分词器(ik_max_word 分词、ik_smart 搜索),搜索质量和性能均优于 Whoosh。

第一步:将 Elasticsearch Deployment 的 replicas 改为 1,并创建 PVC:

# 在宿主机创建 ES 数据目录
mkdir -p /mnt/local-storage-elasticsearch
# 创建对应 PV 和 PVC(参考上方 PV 配置)
kubectl apply -f pv-elasticsearch.yaml
kubectl apply -f pvc-elasticsearch.yaml
# 启动 ES Pod
kubectl -n djangoblog scale deployment elasticsearch --replicas=1

第二步:在 djangoblog-env ConfigMap 中添加 ES 地址,settings.py 会自动切换搜索引擎:

data:
  DJANGO_ELASTICSEARCH_HOST: "http://elasticsearch:9200"
  # 若开启了认证(xpack.security.enabled=true),还需:
  # ELASTICSEARCH_USERNAME: elastic
  # ELASTICSEARCH_PASSWORD: <your-password>
  # 或使用 API Key:
  # ELASTICSEARCH_API_KEY: <your-api-key>

第三步:重启 DjangoBlog Pod,并手动创建 ES 索引:

kubectl -n djangoblog rollout restart deployment/djangoblog

# 等待 Pod 就绪后创建索引
kubectl -n djangoblog exec -it deploy/djangoblog -- \
  python manage.py search_index --rebuild -f

两种方案对比:

对比项 Whoosh Elasticsearch
中文分词 Jieba IK(ik_max_word/ik_smart)
额外 Pod 不需要 需要(约 256MB+ 内存)
部署复杂度
索引构建 initContainer 自动重建 手动执行一次后实时更新
搜索质量 一般 优秀
适用场景 小型博客 文章量大或搜索质量要求高

第七步:部署顺序

# 1. 创建命名空间
kubectl create namespace djangoblog

# 2. StorageClass
kubectl apply -f storageclass.yaml

# 3. 在宿主机创建目录
mkdir -p /mnt/local-storage-db /mnt/local-storage-djangoblog /mnt/resource

# 4. PV(注意修改 nodeAffinity 中的节点名)
kubectl apply -f pv.yaml

# 5. PVC
kubectl apply -f pvc.yaml

# 6. ConfigMap(先修改密码、域名等配置)
kubectl apply -f configmap.yaml

# 7. Service
kubectl apply -f service.yaml

# 8. Deployment
kubectl apply -f deployment.yaml

# 9. Ingress(先修改域名)
kubectl apply -f gateway.yaml

第八步:初始化

首次部署后执行数据库初始化:

# 等待 Pod 就绪
kubectl -n djangoblog wait --for=condition=ready pod -l app=djangoblog --timeout=120s

# 数据库迁移
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py migrate

# 收集静态文件(Nginx 直接服务这些文件)
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py collectstatic --noinput

# 创建管理员账号
kubectl -n djangoblog exec -it deploy/djangoblog -- python manage.py createsuperuser

验证与运维

# 查看所有 Pod 状态
kubectl -n djangoblog get pods

# 查看 DjangoBlog 日志
kubectl -n djangoblog logs -f deploy/djangoblog

# 查看证书状态
kubectl -n djangoblog get certificate

# 更新镜像(发布新版本)
kubectl -n djangoblog rollout restart deployment/djangoblog

# 数据库备份
POD=$(kubectl -n djangoblog get pod -l app=db -o jsonpath="{.items[0].metadata.name}")
kubectl -n djangoblog exec $POD -- \
  mysqldump -u root -p'<password>' djangoblog > backup_$(date +%Y-%m-%d).sql

总结

整个方案的核心思路是:通过环境变量驱动配置,不需要修改任何代码或编写额外的 settings 文件

几个关键点:

  • 所有密码、连接地址都通过 djangoblog-env ConfigMap 注入,切换环境只改 ConfigMap
  • 搜索引擎通过 DJANGO_ELASTICSEARCH_HOST 环境变量自动切换,未设置则默认使用 Whoosh
  • Redis 缓存通过 DJANGO_REDIS_URL 环境变量自动启用
  • Nginx 与 DjangoBlog 共享同一个 PVC,实现静态文件的高效服务
  • Ingress + cert-manager 自动处理 HTTPS 证书的申请与续期

完整配置文件可参考项目 GitHub 仓库


本文由 liangliangyy 原创,转载请注明出处。

相关推荐

评论

0
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多