feat: 添加 Let's Encrypt SSL 证书管理

- 新增独立 certbot 服务项目
- nginx 配置添加 HTTPS 8443 端口监听
- frpc.toml HTTPS 代理指向 nginx:8443
- 支持自动续签并重载 nginx
- 共享 Docker volume 传递证书
This commit is contained in:
awei
2026-09-08 11:43:34 +00:00
parent b028e6dd73
commit 58d65188a1
13 changed files with 341 additions and 69 deletions

20
certbot/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Certbot 镜像 - Let's Encrypt 证书管理
FROM certbot/certbot:latest
LABEL maintainer="awei"
LABEL description="Let's Encrypt certificate management service"
# 安装额外工具
RUN apk add --no-cache curl bash
# 创建必要目录
RUN mkdir -p /var/www/certbot /etc/letsencrypt /var/log/letsencrypt
# 设置工作目录
WORKDIR /var/www/certbot
# 暴露端口(用于 ACME 验证)
EXPOSE 80
# 默认命令:启动 web 服务器用于 ACME 验证
CMD ["sh", "-c", "while true; do echo 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nOK' | nc -l -p 80; done"]

117
certbot/README.md Normal file
View File

@@ -0,0 +1,117 @@
# Let's Encrypt 证书管理服务
独立的 certbot 服务,用于管理 Let's Encrypt SSL证书的申请和自动续签。
## 架构说明
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用户请求 │───▶│ frps (远程) │───▶│ frpc (本地) │
│ *.stonelan.cn │ │ 80/443端口 │ │ 80/8443端口 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ nginx-proxy 容器 │
│ - 80端口: 处理 ACME 验证请求,转发到 certbot 容器 │
│ - 8443端口: HTTPS 反向代理,使用 Let's Encrypt 证书 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ certbot 容器 │
│ - 处理 ACME HTTP-01 验证 │
│ - 定期续签证书 │
│ - 续签后自动重载 nginx (通过 docker.sock) │
└─────────────────────────────────────────────────────────────────┘
```
## 文件说明
- `Dockerfile` - certbot 镜像定义
- `docker-compose.yml` - certbot 容器配置
- `init-cert.sh` - 初始化申请证书脚本
- `renew-cert.sh` - 自动续签脚本
- `crontab.example` - cron 配置示例
## 使用方法
### 1. 首次申请证书
```bash
cd /home/awei/workspace/common-server-public/certbot
# 为每个域名申请证书
./init-cert.sh fastgpt.stonelan.cn admin@example.com
./init-cert.sh gitea.stonelan.cn admin@example.com
./init-cert.sh image.stonelan.cn admin@example.com
./init-cert.sh registry.stonelan.cn admin@example.com
./init-cert.sh www.stonelan.cn admin@example.com
```
### 2. 配置自动续签
```bash
# 编辑 crontab
crontab -e
# 添加以下行(每天凌晨 2 点检查续签)
0 2 * * * cd /home/awei/workspace/common-server-public/certbot && ./renew-cert.sh >> /var/log/certbot-renew.log 2>&1
```
### 3. 手动续签测试
```bash
cd /home/awei/workspace/common-server-public/certbot
./renew-cert.sh
```
## 证书路径
证书存储在 Docker volume `certbot-certs`nginx 容器通过共享 volume 访问:
- 证书: `/etc/letsencrypt/live/<域名>/fullchain.pem`
- 私钥: `/etc/letsencrypt/live/<域名>/privkey.pem`
## 端口映射
| 服务 | 容器端口 | 宿主机端口 | 说明 |
|------|----------|------------|------|
| nginx | 80 | 8081 | HTTP (ACME验证 + 重定向) |
| nginx | 8443 | 8443 | HTTPS (SSL反向代理) |
| frpc | 80 | 80 (远程) | HTTP 映射 |
| frpc | 8443 | 443 (远程) | HTTPS 映射 |
## 重启 nginx
续签后会自动通过 `docker kill -s HUP nginx-proxy` 重载 nginx。如需手动重启
```bash
docker kill -s HUP nginx-proxy
# 或
docker restart nginx-proxy
```
## 故障排查
### 查看证书状态
```bash
docker compose exec certbot certbot certificates
```
### 查看续签日志
```bash
tail -f /var/log/certbot-renew.log
```
### 检查 ACME 验证是否可达
```bash
curl http://fastgpt.stonelan.cn/.well-known/acme-challenge/test
```
## 注意事项
1. 确保域名 DNS 已正确解析到 frps 服务器 IP
2. 确保 frpc 和 nginx 容器在同一个 Docker 网络 (`trim-default`)
3. 证书有效期 90 天,建议每 60 天续签一次cron 已配置每天检查)
4. 首次申请证书前,确保 nginx 容器已启动并能处理 ACME 验证请求

7
certbot/crontab.example Normal file
View File

@@ -0,0 +1,7 @@
# Let's Encrypt 自动续签 cron 配置
# 每天凌晨 2 点执行续签检查
# 编辑 crontab: crontab -e
# 添加以下行:
0 2 * * * cd /home/awei/workspace/common-server-public/certbot && ./renew-cert.sh >> /var/log/certbot-renew.log 2>&1

View File

@@ -0,0 +1,24 @@
services:
certbot:
build: .
container_name: certbot
restart: unless-stopped
volumes:
- certbot-webroot:/var/www/certbot
- certbot-certs:/etc/letsencrypt
- certbot-logs:/var/log/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- trim-default
volumes:
certbot-webroot:
driver: local
certbot-certs:
driver: local
certbot-logs:
driver: local
networks:
trim-default:
external: true

35
certbot/init-cert.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# Let's Encrypt 证书初始化脚本
# 用法: ./init-cert.sh <域名> [邮箱]
set -e
DOMAIN=$1
EMAIL=${2:-"admin@stonelan.cn"}
if [ -z "$DOMAIN" ]; then
echo "用法: $0 <域名> [邮箱]"
echo "示例: $0 fastgpt.stonelan.cn admin@example.com"
exit 1
fi
echo "开始为域名 $DOMAIN 申请证书..."
# 确保 certbot 容器运行
docker compose up -d
# 等待容器启动
sleep 5
# 申请证书(使用 webroot 验证方式)
docker compose exec certbot certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email "$EMAIL" \
--agree-tos \
--no-eff-email \
-d "$DOMAIN" \
--force-renewal
echo "证书申请完成!"
echo "证书路径: /etc/letsencrypt/live/$DOMAIN/"

27
certbot/renew-cert.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Let's Encrypt 证书自动续签脚本
# 续签后自动重载 nginx
set -e
echo "开始检查证书续签..."
# 执行续签
docker compose exec certbot certbot renew \
--webroot \
--webroot-path=/var/www/certbot \
--quiet
# 检查是否有证书被续签
if [ $? -eq 0 ]; then
echo "证书续签检查完成"
# 重载 nginx发送 HUP 信号)
echo "重载 nginx 配置..."
docker kill -s HUP nginx-proxy 2>/dev/null || echo "nginx-proxy 容器未运行或不存在"
echo "续签流程完成"
else
echo "证书续签失败"
exit 1
fi

View File

@@ -1,47 +1,19 @@
# frpc 基础配置 # frpc 基础配置
serverAddr = "47.113.230.48" serverAddr = "47.113.230.48"
serverPort = 7000 serverPort = 7000
auth.method = "token" auth.method = "token"
auth.token = "8f3c9b7d5e2a4f6h1i0j" auth.token = "8f3c9b7d5e2a4f6h1i0j"
[[proxies]] [[proxies]]
name = "http" name = "http"
type = "tcp" type = "tcp"
localIP = "nginx-proxy" localIP = "nginx-proxy"
localPort = 80 localPort = 80
remotePort = 80 remotePort = 80
[[proxies]] [[proxies]]
name = "https" name = "https"
type = "tcp" type = "tcp"
localIP = "10.0.3.173" localIP = "nginx-proxy"
localPort = 4430 localPort = 8443
remotePort = 443 remotePort = 443

View File

@@ -1,24 +1,47 @@
# HTTP -> HTTPS 重定向ACME 验证由独立 certbot 服务处理)
server { server {
listen 80; listen 80;
server_name fastgpt.stonelan.cn gitea.stonelan.cn image.stonelan.cn registry.stonelan.cn www.stonelan.cn;
location /.well-known/acme-challenge/ {
proxy_pass http://certbot:80;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS 配置
server {
listen 8443 ssl;
server_name fastgpt.stonelan.cn; server_name fastgpt.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/fastgpt.stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fastgpt.stonelan.cn/privkey.pem;
# SSL 优化配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / { location / {
proxy_pass http://10.0.3.173:3000; proxy_pass http://10.0.3.173:3000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# 添加代理超时配置
proxy_connect_timeout 60s; proxy_connect_timeout 60s;
proxy_read_timeout 600s; proxy_read_timeout 600s;
proxy_send_timeout 600s; proxy_send_timeout 600s;
# 支持大文件上传和API请求
client_max_body_size 100M; client_max_body_size 100M;
proxy_buffering off; proxy_buffering off;
# 启用gzip压缩
gzip on; gzip on;
gzip_vary on; gzip_vary on;
gzip_min_length 1024; gzip_min_length 1024;
@@ -35,4 +58,4 @@ server {
application/atom+xml application/atom+xml
image/svg+xml; image/svg+xml;
} }
} }

View File

@@ -1,7 +1,19 @@
# HTTPS 配置
server { server {
listen 80; listen 8443 ssl;
server_name gitea.stonelan.cn; server_name gitea.stonelan.cn;
client_max_body_size 200m; client_max_body_size 200m;
ssl_certificate /etc/letsencrypt/live/gitea.stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gitea.stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / { location / {
proxy_pass http://gitea:3000; proxy_pass http://gitea:3000;
proxy_connect_timeout 60s; proxy_connect_timeout 60s;
@@ -13,4 +25,4 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
} }

View File

@@ -1,17 +1,27 @@
# HTTPS 配置
server { server {
listen 80; listen 8443 ssl;
server_name image.stonelan.cn; server_name image.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/image.stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/image.stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / { location / {
proxy_pass http://10.0.3.173:3004; proxy_pass http://10.0.3.173:3004;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# 添加代理超时配置
proxy_connect_timeout 60s; proxy_connect_timeout 60s;
proxy_read_timeout 600s; proxy_read_timeout 600s;
proxy_send_timeout 600s; proxy_send_timeout 600s;
} }
} }

View File

@@ -1,8 +1,19 @@
# HTTPS 配置
server { server {
listen 80; listen 8443 ssl;
server_name registry.stonelan.cn; server_name registry.stonelan.cn;
client_max_body_size 500m; client_max_body_size 500m;
ssl_certificate /etc/letsencrypt/live/registry.stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / { location / {
proxy_pass http://127.0.0.1:3005; proxy_pass http://127.0.0.1:3005;
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -14,4 +25,4 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
proxy_send_timeout 600s; proxy_send_timeout 600s;
} }
} }

View File

@@ -1,7 +1,18 @@
# HTTPS 配置
server { server {
listen 80; listen 8443 ssl;
server_name www.stonelan.cn; server_name www.stonelan.cn;
ssl_certificate /etc/letsencrypt/live/www.stonelan.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.stonelan.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / { location / {
proxy_pass http://10.0.3.173:8080; proxy_pass http://10.0.3.173:8080;
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -9,7 +20,6 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# 添加代理超时配置
proxy_connect_timeout 60s; proxy_connect_timeout 60s;
proxy_read_timeout 600s; proxy_read_timeout 600s;
proxy_send_timeout 600s; proxy_send_timeout 600s;

View File

@@ -4,13 +4,17 @@ services:
container_name: nginx-proxy container_name: nginx-proxy
ports: ports:
- "8081:80" - "8081:80"
- "4430:443" - "8443:8443"
# 配置已内嵌于镜像(构建时 COPY 进 /etc/nginx故不挂载 conf.d volumes:
- certbot-certs:/etc/letsencrypt:ro
restart: unless-stopped restart: unless-stopped
networks: networks:
- trim-default - trim-default
volumes:
certbot-certs:
external: true
networks: networks:
trim-default: trim-default:
external: true external: true