本文档介绍 Hanphone’s Blog 后端的两种生产部署方式:
| 方式 | 适用场景 | 说明 |
|---|---|---|
| 方式一:Systemd 原生部署 | 已有主机 + JDK,轻量 | 当前生产使用,直接托管 jar |
| 方式二:Docker Compose | 需要一键拉起依赖 | 连同 PostgreSQL / Redis 一起容器化 |
.env(由 env.example 复制,完整字段见 环境变量)两种方式共用同一个 Spring Boot 可执行 fat jar:
cd server
mvn clean package
# 产物:target/blog-3.0.jar
mkdir -p /home/hanphone/server_blog/logs
# 将构建产物放到该目录
# /home/hanphone/server_blog/blog-3.0.jar
cp env.example .env
vim /home/hanphone/server_blog/.env
Systemd 原生支持加载 .env 文件,无需 export 前缀(与 环境变量 格式一致)。
sudo vim /etc/systemd/system/blog.service
[Unit]
Description=Blog Spring Boot Application
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/hanphone/server_blog
# 关键点:直接指定 .env 文件,Systemd 会自动解析并注入为环境变量
EnvironmentFile=/home/hanphone/server_blog/.env
ExecStart=/usr/bin/java -Xmx1024M -Xms256M -XX:+UseG1GC -jar /home/hanphone/server_blog/blog-3.0.jar
Restart=on-failure
RestartSec=10
StandardOutput=append:/home/hanphone/server_blog/logs/out.log
StandardError=append:/home/hanphone/server_blog/logs/error.log
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start blog
sudo systemctl enable blog
⚠️ 不要用
scp原地覆盖正在运行的 jar:scp是「截断 + 写入」,运行中的 JVM 之后再去加载尚未加载的类时会报NoClassDefFoundError(曾导致线上 API 卡死)。先传临时文件,再原子mv替换(mv是 rename,运行中的进程仍持有旧 inode)。
# 1) 上传到临时文件
scp target/blog-3.0.jar <user>@<server>:/home/hanphone/server_blog/blog-3.0.jar.new
# 2) 原子替换(不影响运行中的进程)
ssh <user>@<server> 'mv -f /home/hanphone/server_blog/blog-3.0.jar.new /home/hanphone/server_blog/blog-3.0.jar'
# 3) 重启服务
ssh <user>@<server> 'sudo systemctl restart blog'
回滚:升级前先备份 cp -a blog-3.0.jar blog-3.0.jar.bak,出问题用备份覆盖 + 重启即可。
sudo journalctl -u blog -f # 实时查看系统日志
tail -f /home/hanphone/server_blog/logs/out.log # 或看输出文件
后端使用 DB-IP City Lite 离线库把访客 IP 解析成国家/省份。该库每月更新一次,可用 scripts/update-dbip.sh 配 cron 自动拉取覆盖并重启服务。
默认无需配置 .env:后端未设置 GEO_DB_PATH 时,会自动加载当前工作目录(即 systemd WorkingDirectory,如 /home/hanphone/server_blog)下的 dbip-city-lite.mmdb。而 update-dbip.sh 不传 -p 时也默认下载到脚本所在目录——因此只需把脚本放在后端服务目录并运行一次,即完成配置。
# 1. 把脚本放到后端服务目录(即 systemd WorkingDirectory,如 /home/hanphone/server_blog)
sudo cp scripts/update-dbip.sh /home/hanphone/server_blog/update-dbip.sh
sudo chmod +x /home/hanphone/server_blog/update-dbip.sh
# 2. 立即手动执行一次验证(-s 为 systemd 服务名;mmdb 会下载到脚本同目录)
sudo /home/hanphone/server_blog/update-dbip.sh -s blog
# 3. 加入 crontab:每月 1 号凌晨 3 点自动更新
sudo crontab -e
# 添加一行(换成你的实际路径):
# 0 3 1 * * /home/hanphone/server_blog/update-dbip.sh -s blog >> /var/log/update-dbip.log 2>&1
脚本会:下载 db-ip 官方当月 mmdb → 校验为有效 MaxMind DB 格式 → 覆盖目标文件(保留 .bak 备份)→ 重启 -s 指定的服务。不传 -p 时,mmdb 默认下载到脚本所在目录(dbip-city-lite.mmdb)。若需自定义路径,可设 GEO_DB_PATH 并配 -p 指向同一文件。
参数说明见脚本头部注释(
-p可选默认脚本目录,-s可选,-n预演不落库)。所有路径/服务名由调用方传入,脚本本身不包含任何部署信息。排查:若执行报
sudo: update-dbip.sh: command not found,是脚本不在 PATH 且未加执行权限。用绝对路径执行sudo /home/hanphone/server_blog/update-dbip.sh -s blog,并先chmod +x。
生产服务器在国内,datasets-server.huggingface.co / lmarena.ai 直连不可达。方案:在服务器本机跑一个轻量代理客户端(mihomo / clash-meta),采集器通过 HOT_PROXY 走它;只有 LMArena 走代理,其它信源直连。
# 1) 下载 mihomo(GitHub 直连被墙,用镜像)
mkdir -p ~/mihomo && cd ~/mihomo
curl -sL -o mihomo.gz "https://gh-proxy.com/https://github.com/MetaCubeX/mihomo/releases/download/v1.19.32/mihomo-linux-amd64-compatible-v1.19.32.gz"
gunzip -f mihomo.gz && chmod +x mihomo
# 2) 用机场/订阅节点生成最小 config.yaml:
# mixed-port: 7890, allow-lan: false, bind-address: 127.0.0.1, rules: ['MATCH,自动选择']
# (仅本机可用;节点信息只放服务器,不要入库)
# 3) 后台运行 + 保活(无 sudo,用 crontab)
cat > ~/mihomo/start.sh <<'EOF'
#!/bin/bash
cd /home/hanphone/mihomo || exit 0
if ! pgrep -f "mihomo -d" >/dev/null 2>&1; then
nohup ./mihomo -d /home/hanphone/mihomo -f /home/hanphone/mihomo/config.yaml >> /home/hanphone/mihomo/mihomo.log 2>&1 &
fi
EOF
chmod +x ~/mihomo/start.sh
crontab -l 2>/dev/null | { cat; echo "@reboot /home/hanphone/mihomo/start.sh"; echo "*/5 * * * * /home/hanphone/mihomo/start.sh >/dev/null 2>&1"; } | crontab -
# 4) 后端 .env 打开(改完重启 blog)
# HOT_LMARENA_ENABLED=true
# HOT_PROXY=http://127.0.0.1:7890
curl -x http://127.0.0.1:7890 "https://datasets-server.huggingface.co/splits?dataset=lmarena-ai%2Fleaderboard-dataset" 应 200。HOT_PROXY 目前只被 LMArena 采集器使用(HttpJsonSupport.getJsonViaProxy)。Docker 方式会把 PostgreSQL、Redis、后端一起容器化,适合需要一键拉起完整依赖的场景。
cp env.example .env
# 编辑 .env(PG_PASSWORD / REDIS_PASSWORD / TOKEN_SECRET 等)
# 构建镜像并后台启动(PostgreSQL + Redis + 后端)
docker compose up -d
# 查看状态与日志
docker compose ps
docker compose logs -f blog
# 拉取代码后重建镜像并重启
docker compose up -d --build
docker compose restart blog # 仅重启后端
docker compose down # 停止(保留数据卷)
docker compose exec blog bash # 进入容器
docker stats # 查看资源占用
更完整的 Docker 说明(生产配置、备份恢复、监控)见 DOCKER_DEPLOYMENT.md。
.env 由 env.example 复制而来,关键字段:
| 变量 | 说明 | 必需 |
|---|---|---|
SPRING_PROFILES_ACTIVE |
运行环境(prod/dev) | ✓ |
TOKEN_SECRET |
JWT 签名密钥 | ✓ |
PG_HOST / PG_USERNAME / PG_PASSWORD |
数据库连接 | ✓ |
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD |
Redis 连接 | ✓ |
EMAIL_USERNAME / EMAIL_PASSWORD / EMAIL_HOST |
邮件服务 | ✓ |
SERVER_PORT |
应用端口(默认 8090) | |
CORS_ALLOWED_ORIGINS |
CORS 白名单(逗号分隔) | |
INTERNAL_API_KEY |
内部服务通信密钥 | |
GITHUB_* / GOOGLE_* |
OAuth 登录配置 |
后端启动时也会从
WorkingDirectory/.env兜底加载(见BlogApplication.loadDotEnv());方式一通过 systemdEnvironmentFile注入,两种途径最终效果一致。
后端使用 spring.jpa.hibernate.ddl-auto=update:启动时自动创建缺失的表、字段与 @Table(indexes=...) 声明的索引,无需手工执行迁移。
init.sql 初始化基础结构与数据。init.sql 中对应的 CREATE INDEX 语句(带 IF NOT EXISTS)。| 方式 | 升级 | 回滚 |
|---|---|---|
| Systemd | 覆盖 jar → systemctl restart blog |
换回旧 jar → systemctl restart blog |
| Docker | docker compose up -d --build |
git revert <commit> → 重建 |
回滚前建议先备份数据库:
pg_dump -U <user> blog > backup_$(date +%Y%m%d_%H%M%S).sql
AtomicLong)依赖单实例。若横向扩容为多实例,需改用 Redis 统一计数/缓存。X-Real-IP:公共写接口限流依赖 nginx 的 proxy_set_header X-Real-IP $remote_addr,否则退化为按 remoteAddr 限流。GET /actuator/health,可用于探活。8090,通过 SERVER_PORT 覆盖。