从零搭建”一物一码”合格证查询系统——云服务器部署实战

系列文章 5/5 | 适合读者:技术开发者 | 风格:手把手教程,命令可直接复制执行

前四篇文章我们完成了数据模型设计、后端 API 开发、前端双端构建以及二维码签名与防伪机制。现在,是时候把整套系统搬上云端,让它真正跑起来了。本篇将以一台全新的 Ubuntu 云服务器为起点,带你走完从环境初始化到 HTTPS 上线的完整流程。


1. 服务器环境准备

以 Ubuntu 22.04 / 24.04 LTS 为例,阿里云、腾讯云、AWS 均适用。

1.1 系统更新与基础软件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 更新系统包
sudo apt update && sudo apt upgrade -y

# 安装 Python 3、venv、pip、Nginx、Git
sudo apt install python3 python3-venv python3-pip nginx git -y

# 安装 Node.js 18+(用于前端构建)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install nodejs -y

# 验证版本
python3 --version # >= 3.11
node --version # >= 18.x
nginx -v

1.2 创建部署用户

不要用 root 跑应用。创建一个专用的 deploy 用户:

1
2
3
4
5
6
7
8
9
10
# 创建用户并加入 sudo 组
sudo adduser deploy
sudo usermod -aG sudo deploy

# 切换到 deploy 用户,配置 SSH 密钥登录
su - deploy
mkdir -p ~/.ssh && chmod 700 ~/.ssh
# 将你的公钥写入 authorized_keys
echo "ssh-ed25519 AAAA...你的公钥..." >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

1.3 防火墙配置

只开放必要端口,最小化攻击面:

1
2
3
4
5
6
7
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH(建议后续改为非标准端口)
sudo ufw allow 80/tcp # HTTP(用于 certbot 验证)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
sudo ufw status

提示:如果你使用云厂商的安全组,记得在控制台同步放行这三个端口。


2. 后端部署

2.1 拉取代码与虚拟环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建项目目录
sudo mkdir -p /var/www/backend
sudo chown deploy:deploy /var/www/backend

# 克隆仓库
cd /var/www/backend
git clone https://github.com/yourorg/cert-query-system.git .

# 创建 Python 虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装依赖
pip install --upgrade pip
pip install -r requirements.txt
pip install gunicorn # 生产级 WSGI 服务器

2.2 生产环境配置

创建 .env.production 文件,绝对不要提交到 Git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > .env.production << 'EOF'
APP_ENV=production
DEBUG=False
SECRET_KEY=your-super-secret-random-string-at-least-50-chars
ALLOWED_HOSTS=yourdomain.com
BASE_URL=https://yourdomain.com

# Ed25519 签名密钥(用于二维码防伪)
SIGNING_ACTIVE_KID=prod-key-001
SIGNING_ACTIVE_PRIVATE_KEY_PEM=-----BEGIN PRIVATE KEY-----\nMC4CAQ...替换为真实密钥...\n-----END PRIVATE KEY-----

# 数据库(初始可用 SQLite,后续升级见第 6 节)
# DATABASE_URL=mysql://user:pass@localhost:3306/cert_db
EOF

安全提示SECRET_KEY 可用 python3 -c "import secrets; print(secrets.token_urlsafe(64))" 生成。

2.3 数据库初始化与静态文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 激活虚拟环境(如未激活)
source /var/www/backend/venv/bin/activate

# 数据库迁移
python manage.py migrate

# 生成签名密钥对(自定义管理命令,写入数据库)
python manage.py generate_signing_key

# 收集静态文件到 STATIC_ROOT
python manage.py collectstatic --noinput

# 创建管理员账号
python manage.py createsuperuser

2.4 Gunicorn 配置

先手动测试一下 Gunicorn 是否能正常启动:

1
2
3
4
5
6
gunicorn config.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 3 \
--timeout 120 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log

workers 数量公式:CPU 核心数 x 2 + 1。2 核服务器设 5 个 worker 即可。

确认无误后,用 systemd 托管进程,实现开机自启和崩溃自动重启。

2.5 Systemd 服务文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sudo tee /etc/systemd/system/gunicorn.service << 'EOF'
[Unit]
Description=Gunicorn - Cert Query System
After=network.target

[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/backend
Environment="PATH=/var/www/backend/venv/bin"
EnvironmentFile=/var/www/backend/.env.production
ExecStart=/var/www/backend/venv/bin/gunicorn config.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 3 \
--timeout 120 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
1
2
3
4
5
6
7
8
9
10
11
# 创建日志目录
sudo mkdir -p /var/log/gunicorn
sudo chown deploy:deploy /var/log/gunicorn

# 启动并设为开机自启
sudo systemctl daemon-reload
sudo systemctl start gunicorn
sudo systemctl enable gunicorn

# 检查状态
sudo systemctl status gunicorn

3. 前端构建与部署

项目包含两个前端应用,分别构建后部署到 Nginx 的静态文件目录。

3.1 管理后台(Admin Panel)

1
2
3
4
5
6
7
8
9
10
11
cd /var/www/backend/frontend/admin

# 安装依赖并构建
npm install
npm run build
# 产出目录: dist/

# 复制到 Nginx 服务目录
sudo mkdir -p /var/www/admin
sudo cp -r dist/* /var/www/admin/
sudo chown -R deploy:deploy /var/www/admin

3.2 公众查询端(Cert View)

1
2
3
4
5
6
7
8
9
10
11
cd /var/www/backend/frontend/cert-view

# 安装依赖并构建
npm install
npm run build
# 产出目录: dist/

# 复制到 Nginx 服务目录
sudo mkdir -p /var/www/cert-view
sudo cp -r dist/* /var/www/cert-view/
sudo chown -R deploy:deploy /var/www/cert-view

构建建议:如果服务器内存不足 2GB,可以在本地构建后 scp 上传 dist/ 目录,避免 OOM。


4. Nginx 配置

这是整套系统的核心路由层。一个 Nginx 同时服务两个 SPA、代理 API、托管静态资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
sudo tee /etc/nginx/sites-available/cert-query << 'NGINX'
# ============================================================
# HTTP → HTTPS 强制跳转
# ============================================================
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}

# ============================================================
# 主站 HTTPS 配置
# ============================================================
server {
listen 443 ssl http2;
server_name yourdomain.com;

# ---------- SSL 证书(Let's Encrypt) ----------
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

# ---------- Gzip 压缩 ----------
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
gzip_min_length 1024;
gzip_vary on;

# ---------- 安全头 ----------
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# ---------- 公众查询端(默认根路径) ----------
root /var/www/cert-view;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

# ---------- 管理后台 SPA ----------
location /manage/ {
alias /var/www/admin/;
try_files $uri $uri/ /manage/index.html;
}

# ---------- API 反向代理 → Gunicorn ----------
location /api/ {
proxy_pass http://127.0.0.1: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;

# 上传合格证图片可能较大
client_max_body_size 10M;
}

# ---------- Django 静态文件 ----------
location /static/ {
alias /var/www/backend/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}

# ---------- 用户上传的媒体文件 ----------
location /media/ {
alias /var/www/backend/media/;
expires 7d;
}
}
NGINX
1
2
3
4
5
6
7
8
9
# 启用站点配置
sudo ln -sf /etc/nginx/sites-available/cert-query /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default # 移除默认站点

# 测试配置语法
sudo nginx -t

# 重载 Nginx
sudo systemctl reload nginx

5. SSL 证书配置

使用 Let’s Encrypt 免费证书,certbot 会自动修改 Nginx 配置:

1
2
3
4
5
6
7
8
# 安装 certbot
sudo apt install certbot python3-certbot-nginx -y

# 申请证书(自动配置 Nginx)
sudo certbot --nginx -d yourdomain.com

# 验证自动续期
sudo certbot renew --dry-run

注意:申请前请确保域名 DNS 已解析到服务器 IP,且 80 端口可访问。国内服务器需完成 ICP 备案。

Certbot 会自动在 crontab 或 systemd timer 中添加续期任务,证书到期前 30 天会自动续期,无需手动操心。


6. 数据库升级(SQLite –> MySQL / PostgreSQL)

开发阶段用 SQLite 快速迭代没问题,但生产环境强烈建议升级:

维度 SQLite MySQL / PostgreSQL
并发写入 单写锁,多用户时卡顿 行级锁,高并发无压力
备份恢复 复制文件,简单但粗暴 mysqldump / pg_dump,支持增量备份
监控 无内置工具 慢查询日志、性能监控完善
部分唯一索引 原生支持 MySQL 需要变通方案(见下文)

6.1 迁移步骤

1
2
3
4
5
6
7
# 第一步:导出现有数据
python manage.py dumpdata --natural-foreign --natural-primary -o backup.json

# 第二步:安装 MySQL 客户端
pip install mysqlclient

# 第三步:修改 settings.py 中的 DATABASES 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# config/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'cert_db',
'USER': 'cert_user',
'PASSWORD': 'strong-password-here',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
1
2
3
4
5
# 第四步:在新数据库上执行迁移
python manage.py migrate

# 第五步:导入数据
python manage.py loaddata backup.json

6.2 MySQL 部分唯一索引注意事项

我们的模型中使用了 UniqueConstraintcondition 参数(部分唯一索引),这在 SQLite 和 PostgreSQL 中原生支持,但 MySQL 不支持部分唯一索引。解决方案有两种:

  1. 应用层校验:在 Model.clean() 或 Serializer 中手动检查唯一性约束
  2. 改用 PostgreSQL:如果业务依赖此特性较多,PostgreSQL 是更好的选择

7. 运维与监控

系统上线只是开始,稳定运行才是目标。

7.1 日志管理

项目的 settings.py 已配置了 RotatingFileHandler,日志文件按大小自动轮转,无需额外配置 logrotate。关注以下日志路径:

1
2
3
4
5
/var/log/gunicorn/access.log    # Gunicorn 访问日志
/var/log/gunicorn/error.log # Gunicorn 错误日志
/var/www/backend/logs/app.log # Django 应用日志
/var/log/nginx/access.log # Nginx 访问日志
/var/log/nginx/error.log # Nginx 错误日志

7.2 数据库定时备份

1
2
# 添加 crontab 任务:每天凌晨 3 点备份
crontab -e
1
2
# MySQL 每日备份,保留最近 30 天
0 3 * * * mysqldump -u cert_user -p'password' cert_db | gzip > /var/backups/db/cert_db_$(date +\%Y\%m\%d).sql.gz && find /var/backups/db/ -name "*.sql.gz" -mtime +30 -delete
1
2
3
# 别忘了创建备份目录
sudo mkdir -p /var/backups/db
sudo chown deploy:deploy /var/backups/db

7.3 证书到期监控

系统内置的 Notification 模块可以用来监控 SSL 证书和签名密钥的到期时间。建议添加一个 health check 端点:

1
2
3
4
5
6
7
8
9
10
# views.py - 健康检查接口
from django.http import JsonResponse
from datetime import datetime

def health_check(request):
return JsonResponse({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0',
})
1
2
3
4
# urls.py
urlpatterns += [
path('api/health/', health_check, name='health-check'),
]

配合外部监控服务(如 UptimeRobot)定期请求此端点,服务异常时自动告警。

7.4 Gunicorn Worker 调优

1
2
3
4
5
6
# 查看 CPU 核心数
nproc

# Worker 数量 = CPU 核心数 x 2 + 1
# 2 核 → 5 workers
# 4 核 → 9 workers

修改 systemd 服务文件中的 --workers 参数后:

1
2
sudo systemctl daemon-reload
sudo systemctl restart gunicorn

8. 常见问题与排错

静态文件返回 404

1
2
3
4
5
6
7
# 检查 STATIC_ROOT 是否配置正确
python manage.py shell -c "from django.conf import settings; print(settings.STATIC_ROOT)"

# 重新收集静态文件
python manage.py collectstatic --noinput

# 检查 Nginx 配置中的 alias 路径是否与 STATIC_ROOT 一致

CORS 跨域错误

生产环境要明确指定允许的域名,不能用通配符 *

1
2
3
4
5
# settings.py
CORS_ALLOWED_ORIGINS = [
"https://yourdomain.com",
]
# 不要设置 CORS_ALLOW_ALL_ORIGINS = True

微信 OAuth 登录不可用

微信网页授权需要满足以下条件,缺一不可:

  1. 域名已完成 ICP 备案
  2. 微信公众平台已完成 域名验证(JS 安全域名、授权回调域名)
  3. 服务器必须支持 HTTPS
  4. 公众号需为 已认证的服务号(订阅号没有网页授权接口)

Gunicorn 请求超时

PDF 合格证生成接口可能耗时较长,需要适当增加超时:

1
2
3
4
5
6
# 在 systemd 服务文件中调整
--timeout 120 # 默认 30 秒,建议设为 120 秒

# 同时调整 Nginx 代理超时
proxy_read_timeout 120s;
proxy_connect_timeout 10s;

部署后服务无法启动

1
2
3
4
# 排查三板斧
sudo systemctl status gunicorn # 查看服务状态
sudo journalctl -u gunicorn --no-pager -n 50 # 查看系统日志
tail -100 /var/log/gunicorn/error.log # 查看应用错误日志

系列文章回顾

至此,”一物一码”合格证查询系统的全部技术栈已经讲完。让我们回顾一下五篇文章的脉络:

篇目 主题 核心内容
第一篇 数据模型设计 产品、批次、合格证、二维码的多层数据关系;Django Model 设计与数据库约束
第二篇 后端 API 开发 Django REST Framework 接口设计;批量生成合格证;防伪签名与验签流程
第三篇 前端双端开发 管理后台(Admin Panel)+ 公众查询端(Cert View);Vue/React SPA 架构
第四篇 二维码与防伪机制 Ed25519 数字签名;JWS 格式编码;密钥轮换策略;签名验证全流程
第五篇 云服务器部署实战 Ubuntu + Nginx + Gunicorn 部署;SSL 配置;数据库升级;运维监控

从数据模型到线上运行,这套系统覆盖了一个典型 Web 应用的完整生命周期。其中最有价值的设计决策包括:

  • Ed25519 签名防伪:让每张合格证都具有密码学级别的不可伪造性
  • 双前端架构:管理端和公众端分离,各自独立部署和迭代
  • 密钥轮换机制:保证系统长期运行的安全性

希望这个系列能帮助你理解”一物一码”场景的技术全貌。现在,去把你的合格证系统部署上线吧!