feat(deploy,ssl): automate Let’s Encrypt (webroot), preserve SSL, and auto-enable HTTPS
- generate-config.sh
- Add flags: --no-clean, --reset-ssl, --ssl-mode (letsencrypt|self-signed|provided)
- Stop deleting docker/ssl by default; only wipe on explicit --reset-ssl
- Inject ACME webroot route into HTTP (80) server; create docker/letsencrypt-www
- Default SSL_MODE: full=letsencrypt, private/public=self-signed
- Add enable_https_if_cert_present: append 443 server only when server-cert.pem/server-key.pem exist
- Keep self-signed path generating HTTPS immediately (non-basic)
- docker-compose.yml
- Mount ./docker/letsencrypt-www:/var/www/certbot:ro for Nginx ACME challenges
- deploy.sh
- Add --le-email for Let’s Encrypt account email
- Auto-install certbot once (apt-get) and enable systemd timer if available
- Install deploy hook at /etc/letsencrypt/renewal-hooks/deploy/privydrop-reload.sh to:
- Copy renewed certs into docker/ssl
- Hot-reload Nginx; HUP or restart coturn
- First-time issuance (webroot) for <domain> and turn.<domain> after Nginx:80 is up; copy certs
- Re-run generate-config with --no-clean --ssl-mode letsencrypt to enable 443, then reload Nginx
- Behavior changes
- Full mode prefers Let’s Encrypt by default; HTTPS gets enabled as soon as certs exist
- docker/ssl is no longer wiped by config generation
- Notes
- SNI-based turns:443 is not implemented yet (planned)
- Backward compatible with private/public (self-signed)
This commit is contained in:
@@ -45,6 +45,7 @@ PrivyDrop Docker 一键部署脚本
|
||||
full: 完整HTTPS部署 + TURN服务器
|
||||
--with-nginx 启用Nginx反向代理
|
||||
--with-turn 启用TURN服务器
|
||||
--le-email EMAIL 使用 Let's Encrypt 时的证书邮箱(full 模式推荐传入)
|
||||
--clean 清理现有容器和数据
|
||||
--help 显示帮助信息
|
||||
|
||||
@@ -67,6 +68,7 @@ parse_arguments() {
|
||||
WITH_NGINX=false
|
||||
WITH_TURN=false
|
||||
CLEAN_MODE=false
|
||||
LE_EMAIL=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
@@ -86,6 +88,10 @@ parse_arguments() {
|
||||
WITH_TURN=true
|
||||
shift
|
||||
;;
|
||||
--le-email)
|
||||
LE_EMAIL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
CLEAN_MODE=true
|
||||
shift
|
||||
@@ -157,6 +163,107 @@ check_dependencies() {
|
||||
log_success "依赖检查通过"
|
||||
}
|
||||
|
||||
# 安装并准备 Let's Encrypt(certbot)
|
||||
ensure_certbot() {
|
||||
if command -v certbot >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
log_info "安装 certbot (需要 sudo 权限)..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -y && sudo apt-get install -y certbot
|
||||
else
|
||||
log_error "未检测到 apt-get,请手动安装 certbot 或在支持的系统上运行"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 写入 certbot 部署钩子:续期后复制证书并热重载服务
|
||||
install_certbot_deploy_hook() {
|
||||
local repo_dir="$SCRIPT_DIR"
|
||||
local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
|
||||
local hook_file="$hook_dir/privydrop-reload.sh"
|
||||
local compose_file="$repo_dir/docker-compose.yml"
|
||||
|
||||
sudo mkdir -p "$hook_dir"
|
||||
sudo bash -c "cat > '$hook_file'" << EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
REPO_DIR="$repo_dir"
|
||||
COMPOSE_FILE="$compose_file"
|
||||
|
||||
# RENEWED_LINEAGE 由 certbot 传入,指向 live/<domain>
|
||||
if [[ -z "\$RENEWED_LINEAGE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cp "\$RENEWED_LINEAGE/fullchain.pem" "\$REPO_DIR/docker/ssl/server-cert.pem"
|
||||
cp "\$RENEWED_LINEAGE/privkey.pem" "\$REPO_DIR/docker/ssl/server-key.pem"
|
||||
chmod 600 "\$REPO_DIR/docker/ssl/server-key.pem" || true
|
||||
|
||||
# 热重载 nginx,如失败则重启
|
||||
docker compose -f "\$COMPOSE_FILE" exec -T nginx nginx -s reload 2>/dev/null || \
|
||||
docker compose -f "\$COMPOSE_FILE" restart nginx || true
|
||||
|
||||
# 优先向 coturn 发送 HUP,不行则重启(若未启用则忽略)
|
||||
docker compose -f "\$COMPOSE_FILE" exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || \
|
||||
docker compose -f "\$COMPOSE_FILE" restart coturn || true
|
||||
EOF
|
||||
sudo chmod +x "$hook_file"
|
||||
|
||||
# 尝试启用 systemd 定时器
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl enable --now certbot.timer 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# 使用 webroot 首次签发并启用 443 配置
|
||||
provision_letsencrypt_cert() {
|
||||
# 仅在 full 模式且启用 nginx 且存在域名时执行
|
||||
if [[ "$DEPLOYMENT_MODE" != "full" || "$WITH_NGINX" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$DOMAIN_NAME" ]]; then
|
||||
log_warning "full 模式未指定 --domain,跳过 Let’s Encrypt"
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$LE_EMAIL" ]]; then
|
||||
log_warning "未指定 --le-email,将使用 --register-unsafely-without-email"
|
||||
fi
|
||||
|
||||
ensure_certbot
|
||||
install_certbot_deploy_hook
|
||||
|
||||
mkdir -p docker/letsencrypt-www docker/ssl
|
||||
|
||||
# 如果证书已存在,跳过签发
|
||||
if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]]; then
|
||||
log_info "检测到已存在的证书,跳过首次签发"
|
||||
else
|
||||
log_info "通过 webroot 模式签发 Let's Encrypt 证书..."
|
||||
local email_args="--email $LE_EMAIL"
|
||||
if [[ -z "$LE_EMAIL" ]]; then
|
||||
email_args="--register-unsafely-without-email"
|
||||
fi
|
||||
# 需要 80 端口可达且 nginx 已启动
|
||||
sudo certbot certonly --webroot -w "$(pwd)/docker/letsencrypt-www" \
|
||||
-d "$DOMAIN_NAME" -d "turn.$DOMAIN_NAME" \
|
||||
$email_args --agree-tos --non-interactive || {
|
||||
log_error "证书签发失败,请查看 certbot 输出"
|
||||
return 1
|
||||
}
|
||||
# 首次签发后复制到 docker/ssl
|
||||
sudo cp "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" docker/ssl/server-cert.pem
|
||||
sudo cp "/etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem" docker/ssl/server-key.pem
|
||||
sudo chmod 600 docker/ssl/server-key.pem || true
|
||||
fi
|
||||
|
||||
# 启用 443 配置(证书已就绪):不清理,仅追加
|
||||
bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" --mode full --domain "$DOMAIN_NAME" --no-clean --ssl-mode letsencrypt || true
|
||||
|
||||
# 热重载 nginx 以启用 443
|
||||
docker compose exec -T nginx nginx -s reload || docker compose restart nginx
|
||||
}
|
||||
|
||||
# 清理现有部署
|
||||
clean_deployment() {
|
||||
if [[ "$CLEAN_MODE" == "true" ]]; then
|
||||
@@ -473,6 +580,9 @@ main() {
|
||||
# 部署服务
|
||||
deploy_services
|
||||
echo ""
|
||||
|
||||
# 若为 full + nginx,自动签发证书并启用 443
|
||||
provision_letsencrypt_cert || true
|
||||
|
||||
# 等待服务就绪
|
||||
if wait_for_services; then
|
||||
|
||||
@@ -94,6 +94,7 @@ services:
|
||||
volumes:
|
||||
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./docker/letsencrypt-www:/var/www/certbot:ro
|
||||
- ./docker/ssl:/etc/nginx/ssl:ro
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
depends_on:
|
||||
|
||||
@@ -55,13 +55,24 @@ parse_turn_port_range() {
|
||||
TURN_MAX_PORT="$max"
|
||||
}
|
||||
|
||||
NO_CLEAN=false
|
||||
RESET_SSL=false
|
||||
|
||||
cleanup_previous_artifacts() {
|
||||
log_warning "清理上一次生成的配置产物..."
|
||||
if [[ "$NO_CLEAN" == "true" ]]; then
|
||||
log_info "跳过清理历史生成物 (--no-clean)"
|
||||
return 0
|
||||
fi
|
||||
log_warning "清理上一次生成的配置产物 (保留 SSL 证书)..."
|
||||
rm -f .env 2>/dev/null || true
|
||||
rm -f docker/nginx/nginx.conf 2>/dev/null || true
|
||||
rm -f docker/nginx/conf.d/*.conf 2>/dev/null || true
|
||||
rm -f docker/coturn/turnserver.conf 2>/dev/null || true
|
||||
rm -f docker/ssl/* 2>/dev/null || true
|
||||
# 默认不清理 docker/ssl,除非显式 --reset-ssl
|
||||
if [[ "$RESET_SSL" == "true" ]]; then
|
||||
log_warning "按请求重置 SSL 证书目录: docker/ssl/*"
|
||||
rm -f docker/ssl/* 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
@@ -82,6 +93,10 @@ PrivyDrop 配置生成脚本(Docker 版)
|
||||
--domain DOMAIN 指定域名(用于 Nginx/证书/TURN realm,如 turn.DOMAIN)
|
||||
--local-ip IP 指定本机局域网IP(不传则自动探测)
|
||||
--help 显示本帮助
|
||||
--no-clean 跳过清理历史生成物(推荐用于二次生成避免清理 SSL)
|
||||
--reset-ssl 强制清理 docker/ssl/*(默认不清理)
|
||||
--ssl-mode MODE 证书模式:letsencrypt|self-signed|provided
|
||||
- full 模式默认 letsencrypt;private/public 默认 self-signed
|
||||
|
||||
环境变量(可选):
|
||||
PUBLIC_IP 显式指定公网IP;仅在 public/full 模式有效。
|
||||
@@ -362,6 +377,7 @@ http {
|
||||
EOF
|
||||
|
||||
# 生成站点配置
|
||||
mkdir -p docker/letsencrypt-www
|
||||
cat > docker/nginx/conf.d/default.conf << EOF
|
||||
# 上游服务定义
|
||||
upstream backend {
|
||||
@@ -384,6 +400,11 @@ server {
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
|
||||
# ACME 回源,用于 Let's Encrypt 签发/续期
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /nginx-health {
|
||||
access_log off;
|
||||
@@ -512,7 +533,7 @@ EOF
|
||||
log_success "SSL证书已生成: docker/ssl/"
|
||||
log_info "要信任证书,请导入CA证书: docker/ssl/ca-cert.pem"
|
||||
|
||||
# 生成HTTPS Nginx配置
|
||||
# 自签场景直接生成 443 配置
|
||||
if [[ "$DEPLOYMENT_MODE" != "basic" ]]; then
|
||||
generate_https_nginx_config
|
||||
fi
|
||||
@@ -608,6 +629,20 @@ EOF
|
||||
log_success "HTTPS配置已添加"
|
||||
}
|
||||
|
||||
# 当证书存在时再启用 443 配置(适用于 letsencrypt/provided)
|
||||
enable_https_if_cert_present() {
|
||||
if [[ -f "docker/ssl/server-cert.pem" && -f "docker/ssl/server-key.pem" ]]; then
|
||||
# 若 default.conf 中尚未存在 443 server,则追加
|
||||
if ! grep -q "listen 443 ssl" docker/nginx/conf.d/default.conf 2>/dev/null; then
|
||||
generate_https_nginx_config
|
||||
else
|
||||
log_info "检测到已存在 443 配置,跳过追加"
|
||||
fi
|
||||
else
|
||||
log_warning "未检测到证书 (docker/ssl/server-*.pem),暂不启用 443 配置"
|
||||
fi
|
||||
}
|
||||
|
||||
# 生成Coturn配置
|
||||
generate_coturn_config() {
|
||||
if [[ "$TURN_ENABLED" == "true" ]]; then
|
||||
@@ -779,6 +814,18 @@ main() {
|
||||
parse_turn_port_range "$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-clean)
|
||||
NO_CLEAN=true
|
||||
shift
|
||||
;;
|
||||
--reset-ssl)
|
||||
RESET_SSL=true
|
||||
shift
|
||||
;;
|
||||
--ssl-mode)
|
||||
SSL_MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
@@ -813,9 +860,25 @@ main() {
|
||||
generate_nginx_config
|
||||
echo ""
|
||||
|
||||
# 证书生成策略:
|
||||
# - private/public 默认自签;full 默认 letsencrypt(由部署脚本触发签发与复制)
|
||||
if [[ -z "$SSL_MODE" ]]; then
|
||||
if [[ "$DEPLOYMENT_MODE" == "full" ]]; then
|
||||
SSL_MODE="letsencrypt"
|
||||
else
|
||||
SSL_MODE="self-signed"
|
||||
fi
|
||||
fi
|
||||
|
||||
generate_ssl_certificates
|
||||
echo ""
|
||||
|
||||
|
||||
# full/provided/letsencrypt:仅在证书就绪时启用 443
|
||||
if [[ "$DEPLOYMENT_MODE" == "full" ]]; then
|
||||
enable_https_if_cert_present
|
||||
echo ""
|
||||
fi
|
||||
|
||||
generate_coturn_config
|
||||
echo ""
|
||||
|
||||
|
||||
+22
-14
@@ -4,21 +4,11 @@ FROM node:18-alpine AS builder
|
||||
ARG HTTP_PROXY
|
||||
ARG HTTPS_PROXY
|
||||
ARG NO_PROXY
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ARG NEXT_PUBLIC_TURN_HOST
|
||||
ARG NEXT_PUBLIC_TURN_USERNAME
|
||||
ARG NEXT_PUBLIC_TURN_PASSWORD
|
||||
|
||||
ENV http_proxy ${HTTP_PROXY} \
|
||||
https_proxy ${HTTPS_PROXY} \
|
||||
no_proxy ${NO_PROXY}
|
||||
|
||||
# 前端构建期注入可公开环境变量(用于客户端直连后端与 TURN)
|
||||
ENV NEXT_PUBLIC_API_URL ${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_TURN_HOST ${NEXT_PUBLIC_TURN_HOST}
|
||||
ENV NEXT_PUBLIC_TURN_USERNAME ${NEXT_PUBLIC_TURN_USERNAME}
|
||||
ENV NEXT_PUBLIC_TURN_PASSWORD ${NEXT_PUBLIC_TURN_PASSWORD}
|
||||
|
||||
WORKDIR /app
|
||||
# 复制package文件
|
||||
COPY package*.json ./
|
||||
@@ -33,6 +23,18 @@ RUN pnpm install --frozen-lockfile
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 在依赖安装之后再声明与使用构建期公开变量,避免仅 API/TURN 改动导致依赖层缓存失效
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ARG NEXT_PUBLIC_TURN_HOST
|
||||
ARG NEXT_PUBLIC_TURN_USERNAME
|
||||
ARG NEXT_PUBLIC_TURN_PASSWORD
|
||||
|
||||
# 前端构建期注入可公开环境变量(用于客户端直连后端与 TURN)
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
|
||||
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
|
||||
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
|
||||
|
||||
# 设置环境变量
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NODE_ENV production
|
||||
@@ -72,8 +74,14 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# 运行期保留公开变量(非必需,但便于服务端渲染读取)
|
||||
ENV NEXT_PUBLIC_API_URL ${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_TURN_HOST ${NEXT_PUBLIC_TURN_HOST}
|
||||
ENV NEXT_PUBLIC_TURN_USERNAME ${NEXT_PUBLIC_TURN_USERNAME}
|
||||
ENV NEXT_PUBLIC_TURN_PASSWORD ${NEXT_PUBLIC_TURN_PASSWORD}
|
||||
# 需在当前阶段重新声明 ARG,以便在此阶段展开到 ENV
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ARG NEXT_PUBLIC_TURN_HOST
|
||||
ARG NEXT_PUBLIC_TURN_USERNAME
|
||||
ARG NEXT_PUBLIC_TURN_PASSWORD
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
|
||||
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
|
||||
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
|
||||
|
||||
Reference in New Issue
Block a user