Kamixitong/deploy.py

340 lines
10 KiB
Python
Raw Permalink Normal View History

2025-11-17 12:56:43 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
KaMiXiTong 部署脚本
用于生产环境的一键部署
"""
import os
import sys
import subprocess
import argparse
from pathlib import Path
def check_python_version():
"""检查Python版本"""
if sys.version_info < (3, 6):
print("❌ 错误: 需要Python 3.6或更高版本")
print(f"当前版本: {sys.version}")
sys.exit(1)
print(f"✅ Python版本检查通过: {sys.version}")
def create_directories():
"""创建必要的目录"""
directories = [
'logs',
'static/uploads',
'static/css',
'static/js'
]
for directory in directories:
Path(directory).mkdir(parents=True, exist_ok=True)
print("✅ 目录结构创建完成")
def check_env_file():
"""检查环境配置文件"""
env_file = Path('.env')
if not env_file.exists():
print("⚠️ 未找到 .env 文件,正在创建默认配置...")
try:
with open('.env.example', 'r', encoding='utf-8') as src:
with open('.env', 'w', encoding='utf-8') as dst:
dst.write(src.read())
print("✅ 已创建默认 .env 文件")
except FileNotFoundError:
print("❌ 未找到 .env.example 文件")
return False
else:
print("✅ 环境配置文件已存在")
return True
def install_dependencies():
"""安装依赖"""
print("📦 正在安装依赖包...")
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
# 检查并安装生产环境依赖
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'gunicorn'])
print("✅ Gunicorn安装完成")
except subprocess.CalledProcessError:
print("⚠️ Gunicorn安装失败")
print("✅ 依赖安装完成")
return True
except subprocess.CalledProcessError:
print("❌ 依赖安装失败")
return False
def init_database():
"""初始化数据库"""
print("🗄️ 正在初始化数据库...")
# 检查使用哪种数据库
env_file = Path('.env')
if env_file.exists():
with open(env_file, 'r', encoding='utf-8') as f:
content = f.read()
if 'DATABASE_URL=sqlite' in content:
init_script = 'init_db_sqlite.py'
else:
init_script = 'setup_mysql.py'
else:
# 默认使用SQLite
init_script = 'init_db_sqlite.py'
try:
if os.path.exists(init_script):
subprocess.check_call([sys.executable, init_script])
print("✅ 数据库初始化完成")
return True
else:
print(f"⚠️ 数据库初始化脚本 {init_script} 不存在")
return False
except subprocess.CalledProcessError as e:
print(f"❌ 数据库初始化失败: {e}")
return False
def setup_systemd_service():
"""配置Systemd服务"""
print("⚙️ 正在配置Systemd服务...")
# 获取当前路径
app_path = os.path.abspath('.')
venv_path = os.path.join(app_path, 'venv', 'bin', 'gunicorn')
# 如果没有虚拟环境使用系统gunicorn
if not os.path.exists(venv_path):
venv_path = 'gunicorn'
2025-11-22 16:48:45 +08:00
# 从环境变量获取域名
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or 'your-domain.com'
# 确保域名不包含协议部分
if frontend_domain.startswith(('http://', 'https://')):
frontend_domain = frontend_domain.split('://', 1)[1]
2025-11-17 12:56:43 +08:00
service_content = f"""[Unit]
Description=KaMiXiTong Service
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory={app_path}
Environment="FLASK_ENV=production"
2025-11-22 16:48:45 +08:00
Environment="DOMAIN_NAME={frontend_domain}"
2025-11-22 20:32:49 +08:00
ExecStart={venv_path} -w 4 -b 0.0.0.0:5088 run:app
2025-11-17 12:56:43 +08:00
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
try:
# 写入服务文件
with open('/tmp/kamaxitong.service', 'w') as f:
f.write(service_content)
print("✅ Systemd服务配置文件已生成")
print("💡 请使用以下命令安装服务:")
print(" sudo cp /tmp/kamaxitong.service /etc/systemd/system/")
print(" sudo systemctl daemon-reload")
print(" sudo systemctl enable kamaxitong.service")
print(" sudo systemctl start kamaxitong.service")
return True
except Exception as e:
print(f"❌ Systemd服务配置失败: {e}")
return False
def setup_nginx_config():
"""配置Nginx"""
print("⚙️ 正在生成Nginx配置...")
# 从环境变量获取域名
2025-11-22 16:48:45 +08:00
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or ''
2025-11-17 12:56:43 +08:00
if frontend_domain:
2025-11-22 16:48:45 +08:00
# 确保域名不包含协议部分
if frontend_domain.startswith(('http://', 'https://')):
2025-11-17 12:56:43 +08:00
server_name = frontend_domain.split('://', 1)[1]
else:
server_name = frontend_domain
else:
server_name = 'your-domain.com'
nginx_config = f"""server {{
listen 80;
server_name {server_name};
2025-11-22 16:48:45 +08:00
server_tokens off;
2025-11-17 12:56:43 +08:00
2025-11-22 16:48:45 +08:00
# ACME挑战目录用于Let's Encrypt证书
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
}}
# 重定向到HTTPS
location / {{
return 301 https://$server_name$request_uri;
}}
2025-11-17 12:56:43 +08:00
}}
server {{
listen 443 ssl http2;
server_name {server_name};
2025-11-22 16:48:45 +08:00
server_tokens off;
2025-11-17 12:56:43 +08:00
# SSL证书配置请替换为实际路径
2025-11-22 16:48:45 +08:00
# ssl_certificate /etc/letsencrypt/live/{server_name}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/{server_name}/privkey.pem;
# SSL安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
2025-11-17 12:56:43 +08:00
# 安全头配置
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
2025-11-22 16:48:45 +08:00
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
2025-11-17 12:56:43 +08:00
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
2025-11-22 20:32:49 +08:00
gzip_proxied expired no-cache no-store private;
2025-11-17 12:56:43 +08:00
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
# 代理到Flask应用
location / {{
2025-11-22 16:48:45 +08:00
proxy_pass http://127.0.0.1:5088;
2025-11-17 12:56:43 +08:00
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_set_header X-Forwarded-Host $server_name;
2025-11-22 16:48:45 +08:00
proxy_redirect off;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
2025-11-17 12:56:43 +08:00
}}
# 静态文件直接由Nginx处理
location /static/ {{
2025-11-22 16:48:45 +08:00
alias /var/www/kamaxitong/static/;
2025-11-17 12:56:43 +08:00
expires 1y;
add_header Cache-Control "public, immutable";
2025-11-22 16:48:45 +08:00
add_header Access-Control-Allow-Origin "*";
}}
# 上传文件处理
location /static/uploads/ {{
alias /var/www/kamaxitong/static/uploads/;
expires 1d;
add_header Cache-Control "public";
}}
# ACME挑战目录用于Let's Encrypt证书
location /.well-known/acme-challenge/ {{
root /var/www/certbot;
2025-11-17 12:56:43 +08:00
}}
# 日志配置
access_log /var/log/nginx/kamaxitong.access.log;
error_log /var/log/nginx/kamaxitong.error.log;
2025-11-22 16:48:45 +08:00
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {{
root /usr/share/nginx/html;
}}
}}"""
2025-11-17 12:56:43 +08:00
try:
# 写入配置文件
with open('/tmp/kamaxitong_nginx.conf', 'w') as f:
f.write(nginx_config)
print("✅ Nginx配置文件已生成")
print("💡 请使用以下命令安装配置:")
print(" sudo cp /tmp/kamaxitong_nginx.conf /etc/nginx/sites-available/kamaxitong")
print(" sudo ln -s /etc/nginx/sites-available/kamaxitong /etc/nginx/sites-enabled/")
print(" sudo nginx -t")
print(" sudo systemctl restart nginx")
return True
except Exception as e:
print(f"❌ Nginx配置生成失败: {e}")
return False
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='KaMiXiTong 部署脚本')
parser.add_argument('--skip-install', action='store_true', help='跳过依赖安装')
parser.add_argument('--skip-db', action='store_true', help='跳过数据库初始化')
parser.add_argument('--setup-service', action='store_true', help='配置Systemd服务')
parser.add_argument('--setup-nginx', action='store_true', help='生成Nginx配置')
args = parser.parse_args()
print("=" * 50)
print(" KaMiXiTong 生产环境部署脚本")
print("=" * 50)
# 检查Python版本
check_python_version()
# 创建目录
create_directories()
# 检查环境配置
if not check_env_file():
sys.exit(1)
# 安装依赖
if not args.skip_install:
if not install_dependencies():
sys.exit(1)
else:
print("⏭️ 跳过依赖安装")
# 初始化数据库
if not args.skip_db:
if not init_database():
sys.exit(1)
else:
print("⏭️ 跳过数据库初始化")
# 配置Systemd服务
if args.setup_service:
setup_systemd_service()
# 配置Nginx
if args.setup_nginx:
setup_nginx_config()
print("\n" + "=" * 50)
print("✅ 部署准备完成!")
print("=" * 50)
if args.setup_service:
print("💡 已生成Systemd服务配置文件请按提示安装")
if args.setup_nginx:
print("💡 已生成Nginx配置文件请按提示安装")
print("\n📌 下一步操作:")
print("1. 编辑 .env 文件,配置数据库和其他参数")
print("2. 配置SSL证书推荐")
print("3. 启动服务")
print("4. 访问你的域名")
if __name__ == '__main__':
main()