- fix(test): 添加 useSearchParams mock,修正联系链接断言 - style(nav): 将"联系我们"改为"联系" - chore(deploy): 更新 Nginx 配置和部署文档 - style(logo): 更新 Logo SVG 文件 - feat(scripts): 添加字体处理和站点配置脚本
20 KiB
一、Nginx 配置文件(3 个)
1. nginx-static-production.conf
用途: 生产环境完整 Nginx 主配置文件(含反向代理),是 docker-compose-nginx.yml 挂载的配置。
关键内容:
- 全局配置:
worker_processes auto,使用 epoll,连接数 1024,Docker DNS 解析器127.0.0.11 - 安全头: X-Frame-Options、X-XSS-Protection、X-Content-Type-Options、Referrer-Policy
- 限流:
limit_req_zone全局限速 100r/s - Gzip 压缩: 级别 6,覆盖 text/css/json/javascript/xml 等类型
- Upstream 定义:
gitea_app(gitea:3000)、jenkins_app(jenkins:8080) - 虚拟主机:
novalon.cn / www.novalon.cn-- 主站静态文件服务,根目录/var/www/novalon,SSL/TLS 1.2+1.3,HSTS 2年,_next/static/和字体/图片缓存 1 年,try_files $uri $uri.html $uri/ /404.htmlgit.f.novalon.cn-- 反向代理到 Gitea(端口 3000)ci.f.novalon.cn-- 反向代理到 Jenkins(端口 8080),超时 60s
- SSL: Let's Encrypt ACME challenge 支持,独立子域名证书路径
2. nginx-static.conf
用途: 简化版 Nginx 配置,仅包含主站静态服务,用于 Dockerfile 构建镜像。
关键内容:
- 仅配置
novalon.cn / www.novalon.cn,无 Gitea/Jenkins 反向代理 - 静态根目录
/usr/share/nginx/html - SSL 配置较简化(
ssl_ciphers HIGH:!aNULL:!MD5) - 包含与 production 版相同的安全头、Gzip、静态资源缓存策略
- 额外包含
Permissions-Policy头(禁用摄像头/麦克风/地理位置)
3. nginx-internal.conf
用途: 内部/开发环境配置,监听端口 3000,无 SSL。
关键内容:
- 监听
localhost:3000,根目录/var/www/novalon - 无 SSL、无 HSTS、无 ACME challenge
- 包含基本的 Gzip、安全头、静态资源缓存
- 用于
Dockerfile.static构建的内部容器
二、Docker 相关文件(5 个)
4. Dockerfile
用途: 多阶段构建 -- Node.js 编译 + Nginx 静态服务。
关键内容:
- 构建阶段:
node:20-alpine,npm ci安装依赖,npm run build构建 - 运行阶段:
nginx:alpine,将dist/复制到/usr/share/nginx/html,挂载nginx-static.conf - 暴露端口 80
5. Dockerfile.static
用途: 纯静态文件 Nginx 容器(无构建阶段),用于内部部署。
关键内容:
- 基础镜像
nginx:alpine - 将
html/目录复制到/var/www/novalon - 挂载
nginx-internal.conf到/etc/nginx/conf.d/default.conf - 暴露端口 3000
6. docker-compose.yml
用途: 基础 Docker Compose 配置,使用 Dockerfile 构建。
关键内容:
- 服务名
novalon-website,镜像标签novalon-website:1.0.0 - 容器名
novalon-website - 端口映射
80:80和443:443 - 挂载
nginx-static.conf和ssl/目录(只读)
7. docker-compose.server.yml
用途: 服务器端 Docker Compose 配置,使用 Dockerfile.static 构建。
关键内容:
- 服务名
novalon-website,镜像标签novalon-website:latest - 容器名
novalon-website - 端口映射
3000:3000 - 使用外部网络
novalon-network(bridge 驱动)
8. docker-compose-nginx.yml
用途: 独立 Nginx 反向代理容器配置(生产环境网关)。
关键内容:
- 服务名
nginx,使用官方nginx:alpine镜像 - 容器名
novalon-nginx-secure - 端口映射
80:80和443:443 - 挂载:
nginx-static-production.conf->/etc/nginx/nginx.conf(只读)ssl/->/etc/nginx/ssl(只读)logs/->/var/log/nginx../certbot->/var/www/certbot(ACME 验证)../novalon-static->/var/www/novalon(只读,静态文件)
- 使用外部网络
novalon-network
三、部署脚本(2 个)
9. deploy.sh
用途: 完整部署脚本 -- SSH 到远程服务器执行 Docker 构建+部署。
关键内容:
- 默认服务器
139.155.109.62(root 用户),部署路径/home/novalon/docker-app/novalon-website - 容器名
novalon-website,Nginx 容器名novalon-nginx - 流程: 部署前检查 -> SSH 验证 -> SCP 上传文件 -> 服务器端 SSL 配置 ->
docker-compose build --no-cache->docker-compose up -d-> 健康检查 -> SSL 自动续期 cron 配置 - 支持命令行参数:
-i(IP)、-u(用户)、-p(项目名)、-c(容器名)、-v(版本号)
10. deploy-dist.sh
用途: 轻量部署脚本 -- 仅上传预构建的 dist/ 目录到服务器。
关键内容:
- 同一服务器
139.155.109.62,部署到/home/novalon/docker-app/novalon-website/dist - 流程: 检查 dist 目录 -> SSH 验证 -> 备份旧版本 -> rsync 上传 -> 设置文件权限 -> 验证 -> 清理旧备份(保留最近 3 个)
- 适用于已有 Nginx 容器运行、仅需更新静态文件的场景
四、引用 "novalon-nginx" / "novalon-website" 的其他文件(29 个)
除上述 10 个核心文件外,以下文件也引用了这两个关键词,按类别归纳:
| 类别 | 文件路径 |
|---|---|
| 文档 | README.md, DEPLOYMENT.md, docs/deployment.md, docs/deployment/DEPLOYMENT.md, docs/deployment/phase1-deployment-guide.md, docs/deployment/rollback-procedure.md, docs/development/getting-started.md, docs/STRUCTURE_PLAN.md, docs/PRODUCTION_RELEASE_REPORT.md, docs/PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md, docs/PRODUCTION_DEPLOYMENT.md, docs/PERFORMANCE_OPTIMIZATION.md, docs/OPTIMIZATION_REPORT.md, docs/MONITORING_SETUP.md, docs/MONITORING_QUICKSTART.md, docs/HMR-ERROR-SOLUTIONS.md, docs/CDN_CONFIGURATION.md |
| 脚本 | scripts/ssl-wildcard-dns.sh, scripts/ssl-individual-http.sh, scripts/ssl-individual-http-v2.sh, scripts/setup-wildcard-ssl.sh, scripts/monitoring/setup-monitoring.sh, scripts/deploy-wildcard-domain.sh, scripts/deploy-subdomain-ssl.sh |
五、架构总结(2026-04-22 更新)
该项目的部署架构已优化为单层容器模式:
当前架构
┌─────────────────────────────────────────────────────────┐
│ novalon-nginx-secure (唯一容器) - 端口 80/443 │
│ │
│ nginx-static-production.conf (主配置) │
│ └── include /etc/nginx/conf.d/*.conf; │
│ │
│ conf.d/ (模块化配置目录) │
│ ├── git.f.novalon.cn.conf (Gitea 反向代理) │
│ ├── ci.f.novalon.cn.conf (Jenkins 反向代理) │
│ └── *.novalon.cn.conf (产品站点配置) │
│ │
│ sites/ (产品静态文件目录) │
│ ├── product-a/ │
│ └── product-b/ │
│ │
│ 挂载卷: │
│ - ./conf.d → /etc/nginx/conf.d │
│ - ./sites → /var/www/sites │
│ - ../novalon-static → /var/www/novalon │
│ - ./ssl → /etc/nginx/ssl │
└─────────────────────────────────────────────────────────┘
架构优化说明
已移除的组件:
novalon-website容器(原端口 3000)-- 功能已合并到novalon-nginx-secure容器
优化收益:
- 资源节省:减少一个容器实例
- 架构简化:单一网关,职责明确
- 配置模块化:每个服务独立配置文件,互不影响
新增自动化脚本
| 脚本 | 用途 |
|---|---|
scripts/add-product-site.sh |
一键添加新产品站点(配置文件 + 静态文件) |
scripts/ssl-product-site.sh |
为产品站点申请 SSL 证书 |
添加新产品站点流程
# 1. 添加 DNS A 记录: product-a.novalon.cn → 服务器IP
# 2. 运行添加脚本
./scripts/add-product-site.sh product-a ./product-a-files
# 3. 申请 SSL 证书
./scripts/ssl-product-site.sh product-a.novalon.cn
# 4. 重载 Nginx
docker exec novalon-nginx-secure nginx -s reload
历史架构(已废弃)
该项目的部署架构为双层容器模式:
1. 内层:
novalon-website 容器(端口 3000)-- 由 Dockerfile.static 构建,仅提供静态文件服务2. 外层: novalon-nginx-secure 容器(端口 80/443)-- 由 docker-compose-nginx.yml 启动,使用 nginx-static-production.conf 作为完整网关,负责 SSL 终止、静态文件服务、以及到 Gitea (git.f.novalon.cn) 和 Jenkins (ci.f.novalon.cn) 的反向代理
两个容器通过外部 Docker 网络 novalon-network(bridge 驱动)互联。域名 novalon.cn / www.novalon.cn 指向主站静态资源,SSL 证书通过 Let's Encrypt 获取并配置了自动续期 cron 任务。
现在我对你的生产环境架构有了清晰的了解。让我再确认几个关键细节:
现在我对你的架构完全清楚了。在给出方案之前,我需要确认几个关键信息:
好的,需求明确了。让我整理完整的部署方案。
任务理解
- 目标:在现有
novalon-nginx网关上,为 6-15 个独立 HTML 产品官网配置子域名路由 + SSL - 约束:每个产品用独立子域名(如
product-a.novalon.cn),SSL 需要新申请 - 当前架构:
novalon-nginx-secure容器(nginx:alpine)作为网关,静态文件通过 volume 挂载
方案设计
推荐方案:Nginx 模板化 + 目录约定 + Let's Encrypt 自动化
核心思路是利用你现有的 novalon-nginx-secure 网关容器,通过目录约定 + 配置模板来管理多个产品站点,避免为每个产品创建独立容器。
1. 目录结构约定
在服务器上按以下结构组织产品页面:
/home/novalon/docker-app/novalon-nginx/
├── nginx-static-production.conf # 主配置(include 子配置)
├── conf.d/ # 产品站点配置目录(新增)
│ ├── product-a.novalon.cn.conf
│ ├── product-b.novalon.cn.conf
│ └── ...
├── ssl/ # SSL 证书目录
│ ├── novalon.cn/
│ ├── git.f.novalon.cn/
│ ├── ci.f.novalon.cn/
│ ├── product-a.novalon.cn/ # 每个产品独立子目录
│ └── product-b.novalon.cn/
├── sites/ # 产品静态文件目录(新增)
│ ├── product-a/
│ │ └── index.html
│ ├── product-b/
│ │ └── index.html
│ └── ...
└── docker-compose-nginx.yml
2. Nginx 主配置修改
在 nginx-static-production.conf 的 http {} 块末尾添加一行 include:
# 在 http {} 块末尾,现有 server 块之后添加:
include /etc/nginx/conf.d/*.conf;
3. 产品站点配置模板
每个产品一个独立配置文件,例如 conf.d/product-a.novalon.cn.conf:
# 产品A官网 - product-a.novalon.cn
server {
listen 80;
server_name product-a.novalon.cn;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name product-a.novalon.cn;
ssl_certificate /etc/nginx/ssl/product-a.novalon.cn/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/product-a.novalon.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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
root /var/www/sites/product-a;
index index.html;
# 静态资源缓存
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
limit_req zone=general burst=20 nodelay;
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
access_log /var/log/nginx/product-a-access.log;
error_log /var/log/nginx/product-a-error.log;
}
4. Docker Compose 修改
在 docker-compose-nginx.yml 中添加新的 volume 挂载:
services:
nginx:
image: nginx:alpine
container_name: novalon-nginx-secure
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx-static-production.conf:/etc/nginx/nginx.conf:ro
- ./conf.d:/etc/nginx/conf.d:ro # 新增:产品站点配置
- ./ssl:/etc/nginx/ssl:ro
- ./logs:/var/log/nginx
- ../certbot:/var/www/certbot
- ../novalon-static:/var/www/novalon:ro
- ./sites:/var/www/sites:ro # 新增:产品静态文件
networks:
- novalon-network
networks:
novalon-network:
driver: bridge
external: true
5. SSL 证书申请脚本
为每个产品子域名申请 Let's Encrypt 证书:
#!/bin/bash
# scripts/ssl-product-site.sh
# 用法: ./ssl-product-site.sh product-a.novalon.cn
DOMAIN=$1
if [ -z "$DOMAIN" ]; then
echo "用法: $0 <subdomain>.novalon.cn"
exit 1
fi
CERTBOT_DIR="/home/novalon/docker-app/certbot"
SSL_DIR="/home/novalon/docker-app/novalon-nginx/ssl"
# 创建证书目录
mkdir -p "${SSL_DIR}/${DOMAIN}"
# 申请证书(使用 standalone 模式,需临时停止 nginx 的 80 端口)
docker run --rm \
-p 80:80 \
-v "${CERTBOT_DIR}:/var/www/certbot" \
-v "/etc/letsencrypt:/etc/letsencrypt" \
certbot/certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
-d "${DOMAIN}" \
--email admin@novalon.cn \
--agree-tos \
--no-eff-email
# 复制证书到 nginx ssl 目录
cp "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "${SSL_DIR}/${DOMAIN}/"
cp "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "${SSL_DIR}/${DOMAIN}/"
echo "✅ ${DOMAIN} 证书申请完成"
6. 一键添加新产品脚本
#!/bin/bash
# scripts/add-product-site.sh
# 用法: ./add-product-site.sh product-a /path/to/product-a-files
PRODUCT_NAME=$1
FILES_SOURCE=$2
if [ -z "$PRODUCT_NAME" ] || [ -z "$FILES_SOURCE" ]; then
echo "用法: $0 <product-name> <html-files-directory>"
echo "示例: $0 product-a ./product-a-website"
exit 1
fi
DOMAIN="${PRODUCT_NAME}.novalon.cn"
NGINX_DIR="/home/novalon/docker-app/novalon-nginx"
CONF_DIR="${NGINX_DIR}/conf.d"
SITES_DIR="${NGINX_DIR}/sites"
SSL_DIR="${NGINX_DIR}/ssl"
# 1. 创建目录
mkdir -p "${SITES_DIR}/${PRODUCT_NAME}"
mkdir -p "${SSL_DIR}/${DOMAIN}"
mkdir -p "${CONF_DIR}"
# 2. 复制静态文件
cp -r "${FILES_SOURCE}/"* "${SITES_DIR}/${PRODUCT_NAME}/"
# 3. 生成 Nginx 配置
cat > "${CONF_DIR}/${DOMAIN}.conf" << 'CONF_TEMPLATE'
# 产品官网 - {{DOMAIN}}
server {
listen 80;
server_name {{DOMAIN}};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name {{DOMAIN}};
ssl_certificate /etc/nginx/ssl/{{DOMAIN}}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/{{DOMAIN}}/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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
root /var/www/sites/{{PRODUCT_NAME}};
index index.html;
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
limit_req zone=general burst=20 nodelay;
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
access_log /var/log/nginx/{{PRODUCT_NAME}}-access.log;
error_log /var/log/nginx/{{PRODUCT_NAME}}-error.log;
}
CONF_TEMPLATE
# 替换模板变量
sed -i "s/{{DOMAIN}}/${DOMAIN}/g" "${CONF_DIR}/${DOMAIN}.conf"
sed -i "s/{{PRODUCT_NAME}}/${PRODUCT_NAME}/g" "${CONF_DIR}/${DOMAIN}.conf"
echo "✅ 产品站点 ${PRODUCT_NAME} 配置完成"
echo ""
echo "后续步骤:"
echo " 1. 添加 DNS A 记录: ${DOMAIN} -> 服务器IP"
echo " 2. 申请 SSL 证书: ./ssl-product-site.sh ${DOMAIN}"
echo " 3. 重载 Nginx: docker exec novalon-nginx-secure nginx -s reload"
7. 完整部署流程
添加一个新产品站点的完整步骤:
步骤 1: DNS 解析
→ 在域名服务商添加 A 记录: product-a.novalon.cn → 139.155.109.62
步骤 2: 上传文件 + 生成配置
→ ./add-product-site.sh product-a ./product-a-html-files
步骤 3: 申请 SSL 证书
→ ./ssl-product-site.sh product-a.novalon.cn
步骤 4: 验证配置 + 重载 Nginx
→ docker exec novalon-nginx-secure nginx -t
→ docker exec novalon-nginx-secure nginx -s reload
步骤 5: 验证访问
→ curl -I https://product-a.novalon.cn
方案优势
| 特性 | 说明 |
|---|---|
| 零额外容器 | 所有产品站点复用现有 novalon-nginx-secure 网关 |
| 模板化 | 一键脚本生成配置,避免手动复制粘贴出错 |
| 隔离性 | 每个产品独立配置文件、独立日志、独立 SSL 证书目录 |
| 可扩展 | 新增产品只需 3 条命令 |
| 统一管理 | 所有配置集中在 conf.d/ 目录,便于审查和维护 |
| 资源高效 | 不需要为每个 HTML 页面启动独立容器 |
注意事项
- DNS 解析必须先完成:SSL 证书申请需要域名已指向服务器 IP
- 证书续期:建议在现有 cron 任务中加入产品子域名的自动续期
- 如果未来产品数量超过 15 个,建议升级为泛域名证书(
*.novalon.cn),可大幅简化证书管理 - 文件更新:更新某个产品的 HTML 文件后,只需替换
sites/product-x/下的文件,无需重启 Nginx(静态文件通过 volume 挂载)