195 lines
6.1 KiB
Bash
195 lines
6.1 KiB
Bash
#!/bin/bash
|
||
# ============================================
|
||
# SSL 证书初始化脚本(首次部署时运行)
|
||
#
|
||
# 使用 Let's Encrypt (certbot) 为 3 个域名申请证书
|
||
# 流程:
|
||
# 1. 启动 Nginx(仅 HTTP 80 端口,用于 ACME 验证)
|
||
# 2. 用 certbot 对每个域名申请证书
|
||
# 3. 重新加载 Nginx 以启用 HTTPS
|
||
#
|
||
# 用法:
|
||
# ./init-ssl.sh # 正式申请证书
|
||
# ./init-ssl.sh --staging # 使用 staging 环境测试(不受速率限制)
|
||
# ./init-ssl.sh --dry-run # 仅测试,不真正申请
|
||
# ============================================
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# 颜色输出
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
NC='\033[0m'
|
||
|
||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||
|
||
# 加载 .env
|
||
if [ ! -f .env ]; then
|
||
log_error ".env 文件不存在!请先执行: cp .env.example .env 并填写配置"
|
||
exit 1
|
||
fi
|
||
source .env
|
||
|
||
# 检查必要变量
|
||
if [ -z "$SSL_EMAIL" ]; then
|
||
log_error "请在 .env 中设置 SSL_EMAIL(用于 Let's Encrypt 证书申请通知)"
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "$API_DOMAIN" ] || [ -z "$DLWEB_DOMAIN" ] || [ -z "$WX_DOMAIN" ]; then
|
||
log_error "请在 .env 中设置 API_DOMAIN, DLWEB_DOMAIN, WX_DOMAIN"
|
||
exit 1
|
||
fi
|
||
|
||
DOMAINS=("$API_DOMAIN" "$DLWEB_DOMAIN" "$WX_DOMAIN")
|
||
|
||
# 判断使用 docker-compose 还是 docker compose
|
||
COMPOSE_CMD="docker compose"
|
||
if ! docker compose version &> /dev/null 2>&1; then
|
||
COMPOSE_CMD="docker-compose"
|
||
fi
|
||
|
||
# 解析参数
|
||
STAGING_ARG=""
|
||
DRY_RUN=""
|
||
for arg in "$@"; do
|
||
case $arg in
|
||
--staging)
|
||
STAGING_ARG="--staging"
|
||
log_warn "使用 Let's Encrypt Staging 环境(测试用,证书不受信任)"
|
||
;;
|
||
--dry-run)
|
||
DRY_RUN="--dry-run"
|
||
log_warn "Dry-run 模式,不会真正申请证书"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ============================================
|
||
# Step 1: 生成临时自签名证书(让 Nginx 能先启动)
|
||
# ============================================
|
||
log_info "Step 1: 生成临时自签名证书..."
|
||
|
||
for domain in "${DOMAINS[@]}"; do
|
||
CERT_DIR="./docker/nginx/dummy-certs/$domain"
|
||
mkdir -p "$CERT_DIR"
|
||
|
||
if [ ! -f "$CERT_DIR/fullchain.pem" ]; then
|
||
openssl req -x509 -nodes -newkey rsa:2048 -days 1 \
|
||
-keyout "$CERT_DIR/privkey.pem" \
|
||
-out "$CERT_DIR/fullchain.pem" \
|
||
-subj "/CN=$domain" 2>/dev/null
|
||
log_info " 已生成临时自签名证书: $domain"
|
||
fi
|
||
done
|
||
|
||
# ============================================
|
||
# Step 2: 将临时证书复制到 certbot volume
|
||
# ============================================
|
||
log_info "Step 2: 初始化证书 volume..."
|
||
|
||
# 确保容器和 volume 存在
|
||
$COMPOSE_CMD up -d nginx 2>/dev/null || true
|
||
|
||
# 将临时证书复制到 certbot-certs volume
|
||
for domain in "${DOMAINS[@]}"; do
|
||
CERT_DIR="./docker/nginx/dummy-certs/$domain"
|
||
LIVE_DIR="/etc/letsencrypt/live/$domain"
|
||
|
||
# 通过 nginx 容器操作 volume
|
||
docker exec youle-nginx sh -c "mkdir -p $LIVE_DIR" 2>/dev/null || true
|
||
docker cp "$CERT_DIR/fullchain.pem" "youle-nginx:$LIVE_DIR/fullchain.pem"
|
||
docker cp "$CERT_DIR/privkey.pem" "youle-nginx:$LIVE_DIR/privkey.pem"
|
||
done
|
||
|
||
# 重新加载 Nginx 以使用临时证书
|
||
docker exec youle-nginx nginx -s reload 2>/dev/null || true
|
||
log_info " Nginx 已使用临时证书启动"
|
||
|
||
# ============================================
|
||
# Step 3: 用 certbot 申请真实证书
|
||
# ============================================
|
||
log_info "Step 3: 申请 Let's Encrypt 证书..."
|
||
|
||
for domain in "${DOMAINS[@]}"; do
|
||
log_info " 正在为 $domain 申请证书..."
|
||
|
||
$COMPOSE_CMD run --rm certbot certonly \
|
||
--webroot \
|
||
-w /var/www/certbot \
|
||
--email "$SSL_EMAIL" \
|
||
--agree-tos \
|
||
--no-eff-email \
|
||
--force-renewal \
|
||
-d "$domain" \
|
||
$STAGING_ARG \
|
||
$DRY_RUN
|
||
|
||
if [ $? -eq 0 ]; then
|
||
log_info " ✓ $domain 证书申请成功"
|
||
else
|
||
log_error " ✗ $domain 证书申请失败!请检查:"
|
||
log_error " - 域名 DNS 是否已正确解析到本服务器"
|
||
log_error " - 服务器 80 端口是否对外开放"
|
||
log_error " - 是否超过 Let's Encrypt 速率限制(可用 --staging 测试)"
|
||
fi
|
||
done
|
||
|
||
# ============================================
|
||
# Step 4: 重新加载 Nginx 以使用真实证书
|
||
# ============================================
|
||
log_info "Step 4: 重新加载 Nginx..."
|
||
|
||
docker exec youle-nginx nginx -s reload
|
||
|
||
# ============================================
|
||
# Step 5: 清理临时文件并启动 certbot 定时续签
|
||
# ============================================
|
||
rm -rf ./docker/nginx/dummy-certs
|
||
|
||
log_info "Step 5: 启动 certbot 自动续签服务..."
|
||
$COMPOSE_CMD up -d certbot
|
||
|
||
# ============================================
|
||
# Step 6: 安装 crontab 定时重载 Nginx(使续签生效)
|
||
# ============================================
|
||
log_info "Step 6: 设置自动重载 Nginx 的 crontab..."
|
||
|
||
CRON_JOB="0 */12 * * * docker exec youle-nginx nginx -s reload >/dev/null 2>&1"
|
||
CRON_MARKER="# youle-nginx-ssl-reload"
|
||
|
||
# 检查是否已存在
|
||
if crontab -l 2>/dev/null | grep -q "$CRON_MARKER"; then
|
||
log_info " crontab 已存在,跳过"
|
||
else
|
||
# 追加到当前用户的 crontab
|
||
(crontab -l 2>/dev/null; echo "$CRON_JOB $CRON_MARKER") | crontab -
|
||
log_info " 已添加 crontab: 每 12 小时重载 Nginx(使续签的证书生效)"
|
||
fi
|
||
|
||
echo ""
|
||
log_info "============================================"
|
||
log_info "SSL 初始化完成!"
|
||
log_info "============================================"
|
||
echo ""
|
||
echo " 证书信息:"
|
||
for domain in "${DOMAINS[@]}"; do
|
||
echo " https://$domain"
|
||
done
|
||
echo ""
|
||
echo " 证书有效期: 90 天"
|
||
echo " 自动续签: certbot 容器每 12 小时检查一次"
|
||
echo " 自动重载: crontab 每 12 小时执行 nginx -s reload"
|
||
echo ""
|
||
echo " 查看证书状态:"
|
||
echo " $COMPOSE_CMD run --rm certbot certificates"
|
||
echo ""
|
||
echo " 手动续签:"
|
||
echo " ./deploy.sh ssl-renew"
|
||
echo ""
|