Files
张翔 96dddeb20b chore: 上线前测试修复与部署配置更新
- fix(test): 添加 useSearchParams mock,修正联系链接断言
- style(nav): 将"联系我们"改为"联系"
- chore(deploy): 更新 Nginx 配置和部署文档
- style(logo): 更新 Logo SVG 文件
- feat(scripts): 添加字体处理和站点配置脚本
2026-04-22 20:17:13 +08:00

524 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### 一、Nginx 配置文件(3 个)
#### 1. [nginx-static-production.conf](computer:///sessions/69e889f08f8c93c2514713f1/workspace/nginx-static-production.conf)
**用途**: 生产环境完整 Nginx 主配置文件(含反向代理),是 `docker-compose-nginx.yml` 挂载的配置。
**关键内容**:
- **全局配置**: `worker_processes auto`,使用 epoll,连接数 1024Docker 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.3HSTS 2年,`_next/static/` 和字体/图片缓存 1 年,`try_files $uri $uri.html $uri/ /404.html`
- `git.f.novalon.cn` -- 反向代理到 Gitea(端口 3000
- `ci.f.novalon.cn` -- 反向代理到 Jenkins(端口 8080),超时 60s
- **SSL**: Let's Encrypt ACME challenge 支持,独立子域名证书路径
#### 2. [nginx-static.conf](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/nginx-internal.conf)
**用途**: 内部/开发环境配置,监听端口 3000,无 SSL。
**关键内容**:
- 监听 `localhost:3000`,根目录 `/var/www/novalon`
- 无 SSL、无 HSTS、无 ACME challenge
- 包含基本的 Gzip、安全头、静态资源缓存
- 用于 `Dockerfile.static` 构建的内部容器
---
### 二、Docker 相关文件(5 个)
#### 4. [Dockerfile](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/Dockerfile.static)
**用途**: 纯静态文件 Nginx 容器(无构建阶段),用于内部部署。
**关键内容**:
- 基础镜像 `nginx:alpine`
-`html/` 目录复制到 `/var/www/novalon`
- 挂载 `nginx-internal.conf``/etc/nginx/conf.d/default.conf`
- 暴露端口 3000
#### 6. [docker-compose.yml](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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](computer:///sessions/69e889f08f8c93c2514713f1/workspace/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 证书 |
#### 添加新产品站点流程
```bash
# 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
```nginx
# 在 http {} 块末尾,现有 server 块之后添加:
include /etc/nginx/conf.d/*.conf;
```
#### 3. 产品站点配置模板
每个产品一个独立配置文件,例如 `conf.d/product-a.novalon.cn.conf`
```nginx
# 产品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 挂载:
```yaml
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 证书:
```bash
#!/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. 一键添加新产品脚本
```bash
#!/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 页面启动独立容器 |
### 注意事项
1. **DNS 解析必须先完成**:SSL 证书申请需要域名已指向服务器 IP
2. **证书续期**:建议在现有 cron 任务中加入产品子域名的自动续期
3. **如果未来产品数量超过 15 个**,建议升级为泛域名证书(`*.novalon.cn`),可大幅简化证书管理
4. **文件更新**:更新某个产品的 HTML 文件后,只需替换 `sites/product-x/` 下的文件,无需重启 Nginx(静态文件通过 volume 挂载)