VPS在camelzNAV现有项目下新增typecho
基础配置
创建文件blog.ini
在文件夹/home/camelznav-project/certbot/创建cloudflare_api_token文件blog.ini
dns_cloudflare_api_token = 你的_blog_token_这里或直接用命令创建
echo "dns_cloudflare_api_token = 你的_blog_token_这里" > /home/camelznav-project/certbot/blog.ini把你的_blog_token_这里改成你的cloudflare_api_token赋相应权限
chmod 600 /home/camelznav-project/certbot/blog.ini创建 Typecho 数据目录
# 创建目录
mkdir -p /home/camelznav-project/typecho/html /home/camelznav-project/typecho/mysql
或
sudo mkdir -p /home/camelznav-project/typecho/{mysql,html}
# 设置 mysql 数据目录权限(关键!)
sudo chown -R 999:999 /home/camelznav-project/typecho/mysql
# html 目录可设为宽松权限(Typecho 安装时需写 config/ 和 usr/)
sudo chmod -R 777 /home/camelznav-project/typecho/html/
或
sudo chmod -R 775 /home/camelznav-project/typecho/html
sudo chown -R www-data:www-data /home/camelznav-project/typecho/html # 可选假如你不想如果你不想用
777,可以在docker-compose.yml中 强制容器使用宿主机当前用户的 UID,这样更安全:services: typecho-app: image: your-typecho-image user: "${UID:-1000}:${GID:-1000}" # ← 关键! volumes: - ./typecho/html:/var/www/html # ...然后启动前导出环境变量:
export UID GID docker compose up -d✅ 优点:文件所有权与宿主机一致,无需
777
⚠️ 注意:需确保镜像支持该 UID(一般没问题)
下载并解压 Typecho
cd /home/camelznav-project/typecho/html
wget https://github.com/camel52zhang/Typecho-lelez/releases/download/v1.0.1-beta.1/typecho.zip
unzip typecho.zip
# 1. 进入 html 目录(你已经在了)
cd /home/camelznav-project/typecho/html
# 2. 把 typecho/ 里的所有文件移到当前目录
mv typecho/* ./
# 3. (可选)如果还有隐藏文件(如 .htaccess),也一并移动
mv typecho/.[!.]* ./ 2>/dev/null || true
# 4. 删除空的 typecho 目录
rmdir typecho
# 5. 验证
ls -la
# 应该看到 index.php, admin/, usr/, var/ 等直接在当前目录mv typecho/* ./的意思是把
typecho里的所有文件移到当前目录rmdir typecho的意思是删除已变空的
typecho文件夹
Typecho 安装时需要往这些目录写入插件、主题、缓存等
chmod -R 777 usr/
chmod -R 777 var/
#然后回到项目目录
cd ../..cd ../..意思是回到项目目录更新 docker-compose.yml
version: "3.9"
services:
# ========== 导航证书:dh.camelz.us.ci ==========
certbot:
image: camel52zhang/certbot-dns-cloudflare:latest
container_name: camelznav-certbot
volumes:
- ./certbot/letsencrypt:/etc/letsencrypt
- ./certbot/credentials.ini:/credentials.ini:ro
command: >
certonly
--dns-cloudflare
--dns-cloudflare-credentials /credentials.ini
--dns-cloudflare-propagation-seconds 30
-d dh.camelz.us.ci
--email eric.zhng@gmail.com
--agree-tos
--non-interactive
--preferred-challenges dns-01
networks:
- camelznav-net
# ========== 博客证书:blog.lelez.us.kg ==========
certbot-blog:
image: camel52zhang/certbot-dns-cloudflare:latest
container_name: camelznav-certbot-blog
volumes:
- ./certbot/letsencrypt:/etc/letsencrypt
- ./certbot/blog.ini:/credentials.ini:ro
command: >
certonly
--dns-cloudflare
--dns-cloudflare-credentials /credentials.ini
--dns-cloudflare-propagation-seconds 30
-d blog.lelez.us.kg
--email eric.zhng@gmail.com
--agree-tos
--non-interactive
--preferred-challenges dns-01
networks:
- camelznav-net
# ========== Nginx 反向代理 ==========
nginx:
image: nginx:alpine
container_name: camelznav-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/letsencrypt:/etc/letsencrypt:ro
depends_on:
- camelznav-backend
- camelznav-frontend
- typecho-app
networks:
- camelznav-net
# ========== 导航 backend ==========
camelznav-backend:
build: ./camelznav-backend
container_name: camelznav-backend
restart: always
environment:
- PORT=8080
- GIN_MODE=release
volumes:
- ./data/nav.db:/app/nav.db
- ./data/uploads:/app/uploads
- /etc/localtime:/etc/localtime:ro
expose:
- "8080"
networks:
- camelznav-net
# ========== 导航 frontend ==========
camelznav-frontend:
build:
context: ./camelznav-frontend
args:
- NEXT_PUBLIC_API_URL=http://camelznav-backend:8080
container_name: camelznav-frontend
restart: always
environment:
- NEXT_PUBLIC_API_URL=http://camelznav-backend:8080
expose:
- "3000"
depends_on:
- camelznav-backend
networks:
- camelznav-net
# ========== Typecho 博客 ==========
typecho-db:
image: mysql:8.0
container_name: typecho-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: "typecho_secure_password_123"
MYSQL_DATABASE: typecho_db
MYSQL_USER: typecho_zhang
MYSQL_PASSWORD: "typecho_secure_password_456"
volumes:
- /home/camelznav-project/typecho/mysql:/var/lib/mysql
networks:
- camelznav-net
typecho-app:
image: php:8.2-apache
container_name: typecho-app
restart: always
volumes:
- /home/camelznav-project/typecho/html:/var/www/html
environment:
APACHE_DOCUMENT_ROOT: /var/www/html
depends_on:
- typecho-db
networks:
- camelznav-net
networks:
camelznav-net:
driver: bridge
数据库信息
- 数据库地址:
typecho-db(Docker 服务名)- 端口:
3306- 用户名:
typecho_zhang- 密码:
typecho_secure_password_123- 数据库名:
typecho_db- 表前缀:
typecho_(可选)
更新 Nginx 配置
编辑 /home/camelznav-project/nginx/conf.d/default.conf:
# --- dh.camelz.us.ci ---
server {
listen 80;
server_name dh.camelz.us.ci;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name dh.camelz.us.ci;
ssl_certificate /etc/letsencrypt/live/dh.camelz.us.ci/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dh.camelz.us.ci/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# API
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://camelznav-backend:8080;
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;
}
# Frontend
location / {
proxy_pass http://camelznav-frontend:3000;
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;
}
}
# --- blog.lelez.us.kg ---
server {
listen 80;
server_name blog.lelez.us.kg;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name blog.lelez.us.kg;
ssl_certificate /etc/letsencrypt/live/blog.lelez.us.kg/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.lelez.us.kg/privkey.pem;
location / {
proxy_pass http://typecho-app;
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;
}
}
部署和验证
- 启动所有服务
docker compose up -d- 手动触发证书申请(首次)
docker compose run --rm certbot
docker compose run --rm certbot-blog成功后,你会看到:
./certbot/letsencrypt/live/dh.camelz.us.ci/./certbot/letsencrypt/live/blog.lelez.us.kg/
- 重载 Nginx(如果证书是后生成的)
docker exec camelznav-nginx nginx -s reload访问博客安装页
打开浏览器:
👉 https://blog.lelez.us.kg
填写数据库信息:
- 地址:
typecho-db - 端口:
3306 - 用户名:
typecho_zhang - 密码:
typecho_secure_password_123 - 数据库名:
typecho_db - 前缀:
typecho_
✅ 安装完成后,按提示 删除 install.php。
收紧权限(可选)
chmod 644 /home/camelznav-project/typecho/html/config/config.inc.php
chmod -R 755 /home/camelznav-project/typecho/html/usr/如果安装成功,记得:
# 删除 install.php(安全加固)
rm /home/camelznav-project/typecho/html/install.php回收权限
chmod 644 /home/camelznav-project/typecho/html/config.inc.php修改证书续签
现在有 两个独立域名:
dh.camelz.us.ci(主站)blog.lelez.us.kg(博客)
它们使用 不同的 Certbot 服务(certbot 和 certbot-blog),因此需要分别续签。
✅ 改造目标
让脚本能:
- 同时续签两个域名
- 任一证书更新就重载 Nginx
- 保持原有邮件通知逻辑
🔧 修改后的 renew-cert.sh
#!/usr/bin/env bash
set -e
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
COMPOSE="docker compose"
NGINX_CONTAINER="camelznav-nginx"
MAIL_TO="eric_zhng@163.com"
# 定义两个站点
declare -A SITES=(
["dh.camelz.us.ci"]="certbot"
["blog.lelez.us.kg"]="certbot-blog"
)
send_mail() {
local subject="$1"
local body="$2"
echo -e "$body" | mail -s "$subject" "$MAIL_TO"
}
echo "========== Certbot Renew Start =========="
date
cd "$PROJECT_DIR"
ANY_CERT_UPDATED=false
for DOMAIN in "${!SITES[@]}"; do
SERVICE="${SITES[$DOMAIN]}"
CERT_FILE="$PROJECT_DIR/certbot/letsencrypt/live/$DOMAIN/fullchain.pem"
HASH_FILE="$PROJECT_DIR/certbot/letsencrypt/live/$DOMAIN/.last_cert_hash"
echo "[*] Processing domain: $DOMAIN (service: $SERVICE)"
# 读取上一次 hash
LAST_HASH=""
if [[ -f "$HASH_FILE" ]]; then
LAST_HASH=$(cat "$HASH_FILE")
fi
echo " [1/3] Running certbot renew for $DOMAIN..."
if ! $COMPOSE run --rm "$SERVICE"; then
send_mail \
"❌ Certbot 续签失败 [$DOMAIN @ $(hostname)]" \
"时间: $(date)\n主机: $(hostname)\n域名: $DOMAIN\n服务: $SERVICE\n路径: $PROJECT_DIR"
exit 1
fi
# 计算当前证书 hash
if [[ -f "$CERT_FILE" ]]; then
NEW_HASH=$(sha256sum "$CERT_FILE" | awk '{print $1}')
else
send_mail \
"⚠️ 证书文件缺失 [$DOMAIN @ $(hostname)]" \
"时间: $(date)\n主机: $(hostname)\n域名: $DOMAIN\n证书文件不存在: $CERT_FILE"
exit 1
fi
# 判断是否更新
if [[ "$NEW_HASH" != "$LAST_HASH" ]]; then
echo " [2/3] Certificate for $DOMAIN updated."
ANY_CERT_UPDATED=true
echo "$NEW_HASH" > "$HASH_FILE"
else
echo " [2/3] Certificate for $DOMAIN not changed."
fi
done
# 如果任意证书更新,重载 Nginx
if [[ "$ANY_CERT_UPDATED" == true ]]; then
echo "[3/3] Reloading Nginx due to certificate update..."
if ! docker exec "$NGINX_CONTAINER" nginx -s reload; then
send_mail \
"⚠️ Nginx reload 失败 [$(hostname)]" \
"时间: $(date)\n主机: $(hostname)\n证书已更新,但 nginx reload 失败,请检查容器状态: $NGINX_CONTAINER"
exit 1
fi
echo "[3/3] Nginx reload done."
else
echo "[3/3] No certificates changed, nginx reload skipped."
fi
echo "========== Certbot Renew Finished =========="✅ 主要改进点
| 功能 | 说明 |
|---|---|
| 多域名支持 | 用 declare -A SITES 管理多个域名和服务 |
| 独立续签 | 分别调用 certbot 和 certbot-blog |
| 智能重载 | 只要任一证书更新,就 reload Nginx |
| 错误隔离 | 一个域名失败会发邮件并退出(避免部分成功) |
| 日志清晰 | 每个域名步骤带缩进,易于排查 |
首次运行前可手动创建 .last_cert_hash(可选):
sha256sum certbot/letsencrypt/live/dh.camelz.us.ci/fullchain.pem | awk '{print $1}' > certbot/letsencrypt/live/dh.camelz.us.ci/.last_cert_hash
sha256sum certbot/letsencrypt/live/blog.lelez.us.kg/fullchain.pem | awk '{print $1}' > certbot/letsencrypt/live/blog.lelez.us.kg/.last_cert_hash位置在*与证书文件同目录*,即和
fullchain.pem在同一个$DOMAIN子目录下总结
文件 推荐路径 证书文件 certbot/letsencrypt/live/example.com/fullchain.pem哈希缓存 certbot/letsencrypt/live/example.com/.last_cert_hash📌 原则:谁的数据,谁保管。.last_cert_hash是fullchain.pem的“元数据”,就该和它住在一起。你现在的做法完全正确,不需要移动!👍
定时任务建议
将脚本加入 crontab(每周执行):
# 编辑 crontab
crontab -e
# 添加(每周日凌晨 4 点)
0 4 * * 0 /home/camelznav-project/renew-cert.sh >> /var/log/certbot-renew.log 2>&1💡 日志便于追踪:tail -f /var/log/certbot-renew.log