基础配置

创建文件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;
    }
}

部署和验证

  1. 启动所有服务
docker compose up -d
  1. 手动触发证书申请(首次)
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/
  1. 重载 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 服务certbotcertbot-blog),因此需要分别续签。

✅ 改造目标

让脚本能:

  1. 同时续签两个域名
  2. 任一证书更新就重载 Nginx
  3. 保持原有邮件通知逻辑

🔧 修改后的 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 管理多个域名和服务
独立续签分别调用 certbotcertbot-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_hashfullchain.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

标签: none

添加新评论

🔝