开发、架构、部署与配置,一份手册讲清楚
面向维护者的单文件项目手册:从本地启动、共享 MySQL、静态预渲染,到管理端发布、 GitHub Actions 与 Docker/Nginx 生产落地。
核心发布闭环已经可用,但网站与管理端还不能称为生产完备。 jy-site 的静态生成、搜索、RSS 与主题链路基本打通;jy-admin 可完成登录、文章编辑、分类标签和发布触发。容器化、管理端自动部署、TLS、备份恢复、 回滚、完整 CI、细粒度权限与若干业务能力仍需补齐。
01项目总览
JY 是两个独立 Node 项目共处一个代码目录的博客系统。它们共享 MySQL
blog_site,但承担完全不同的生产职责:
SvelteKit 5 在开发时实时查库,生产构建时把内容烘焙成纯静态文件,由 Nginx 托管。
Next.js 15 常驻 Node 服务,负责认证、内容写入、站点设置和触发公开站重新构建。
当前能力分级
已实现
写作、发布、预渲染与线上更新主路径存在。
部分实现
阅读体验完整,404、sitemap、PWA 缓存等仍有缺口。
生产阻塞
容器、TLS、备份、健康检查和自动回滚均未入库。
生产环境的 jy-site 没有 Node 服务,也不会实时查询 MySQL。数据库内容变更只有经过
build:static 和部署后才会上线;/api/search
仅用于开发态,生产搜索读取构建生成的 JSON。
02快速开始
前置环境
| 组件 | 仓库约定 | 说明 |
|---|---|---|
| Node.js | 22.22.1 | 根目录 .nvmrc 与 jy-admin engines 一致。 |
| pnpm | 10.15.0 | 两个项目各自维护 lockfile,没有根 workspace。 |
| MySQL | Schema 源自 5.7.44 | 升级到 MySQL 8.x 前应在预发布环境完成导入与查询验证。 |
1. 初始化数据库
mysql -u root -p < jy-site/blog_site.sql
mysql -u root -p blog_site < jy-site/migrations/001_site_deploy_settings.sql
mysql -u root -p blog_site < jy-site/migrations/002_auto_publish_setting.sql
SQL dump 中的用户密码哈希是占位值,不是有效 bcrypt 凭证。首次启动前必须生成真实哈希并更新
users.password_hash;不要把明文密码写入 SQL 或 Git。
2. 配置两个应用
cp jy-site/.env.example jy-site/.env
cp jy-admin/.env.example jy-admin/.env
# 编辑两个 .env:
# - DATABASE_* 指向同一个 blog_site
# - jy-admin 的 SESSION_SECRET 至少 32 字符
# - 需要发布按钮时再配置 GITHUB_REPOSITORY / GITHUB_TOKEN
3. 安装并启动
终端 A · 公开站
cd jy-site
pnpm install
pnpm dev
默认 http://localhost:5173;端口占用时 Vite 会自动顺延。
终端 B · 管理端
cd jy-admin
pnpm install
pnpm dev
固定 http://localhost:5174。
常用质量命令
| 项目 | 命令 | 用途 |
|---|---|---|
| jy-site | pnpm check | SvelteKit sync + svelte-check。 |
| jy-site | pnpm lint | Prettier 检查 + ESLint。 |
| jy-site | pnpm build:static | 完整静态构建;不仅是 pnpm build。 |
| jy-admin | pnpm check | TypeScript --noEmit。 |
| jy-admin | pnpm verify | 类型检查 + Next.js 生产构建。 |
| jy-admin | pnpm dev:reset | 清理 .next 后重启,处理 HMR/Server Action 缓存异常。 |
03仓库与技术栈
这里是双应用同仓结构,但根目录没有 package.json 或
pnpm-workspace.yaml。依赖安装、命令和 lockfile 都在两个子项目内独立管理。
jy_prj/
├── .github/workflows/
│ ├── ci-jy-admin.yml # 管理端 TypeScript CI
│ └── deploy-jy-site.yml # 静态站构建与发布
├── jy-site/
│ ├── src/routes/ # SvelteKit 页面与端点
│ ├── src/lib/ # 组件、Markdown、主题、数据库读取
│ ├── scripts/ # 搜索索引、sitemap、构建规范化
│ ├── migrations/ # 手工 SQL 增量
│ ├── static/ # PWA、图片、搜索索引等
│ ├── blog_site.sql # 全量 schema + seed
│ └── nginx.conf # 当前 HTTP 静态站参考配置
├── jy-admin/
│ ├── app/ # Next.js App Router
│ ├── components/ # Ant Design 管理组件
│ └── lib/ # Server Actions、DB、会话、GitHub API
└── README.md # 当前高层说明
核心栈
| 层 | 技术 | 职责 |
|---|---|---|
| 公开站 | Svelte 5 · SvelteKit 2 · Vite 7 | 开发态 SSR 与生产静态预渲染。 |
| 管理端 | Next.js 15.5.14 · React 19.2.4 · Ant Design 5 | 动态管理后台与发布控制面。 |
| 内容渲染 | markdown-it · Shiki | Markdown、VitePress 容器、双主题代码高亮。 |
| 数据层 | MySQL · mysql2/promise | 无 ORM;两端均使用手写 SQL。 |
| 认证 | iron-session · bcryptjs | 加密 Cookie 会话与密码校验。 |
| 发布 | GitHub Actions · rsync · Nginx | 当前静态站构建与同步。 |
工作区注意
本次核对时,当前目录未被 git status 识别为 Git worktree。GitHub Actions、
repository_dispatch 与推荐的自动化流程都以“代码已经位于真实 GitHub 仓库”为前提。
04公开站 jy-site
双运行模式
开发模式
- Vite dev server 默认监听
0.0.0.0:5173。 - 页面 Server Load 实时读取 MySQL。
- 搜索调用
GET /api/search?q=...。 - 更新数据库后刷新页面即可看到内容。
生产模式
adapter-static输出到jy-site/build/。- 所有文章、分类、标签和 RSS 在构建时生成。
- 搜索读取
/search-index.json。 - Nginx 只提供静态文件,不连接数据库。
路由清单
| 路由 | 职责 | 数据与预渲染 |
|---|---|---|
/ | 首页与最新文章 | 构建时读取最近文章。 |
/about | 建设初衷 | 静态内容,参与预渲染。 |
/creed | 生活信条 | 静态页面。 |
/life | 生命刻度 | 静态页面 + 客户端计时。 |
/blog | 文章列表与分类胶囊 | 文章、分类和分页设置。 |
/blog/page/[n] | 列表分页 | entries() 枚举可用页。 |
/blog/[slug] | 文章详情、目录、上下篇 | 构建时 Markdown → HTML。 |
/blog/category/[slug] | 分类文章 | 构建时枚举分类。 |
/blog/tag/[slug] | 标签文章 | 构建时枚举标签。 |
/rss.xml | 最近 30 篇 RSS 2.0 | 静态 XML。 |
/api/search | 开发态数据库搜索 | 纯静态生产环境不可用,属于设计边界。 |
完整静态构建
-
生成搜索索引
generate-search-index.ts查询已发布文章并写入 static JSON。 -
清理并运行 SvelteKit build
读取 MySQL,预渲染页面、数据依赖与 RSS。
-
生成 sitemap 与 robots
优先使用数据库
site_url,其次使用SITE_URL。 -
规范化目录入口
fix-build.ts把route.html补成route/index.html。
cd jy-site
pnpm build:static
pnpm preview
功能状态
| 能力 | 状态 | 说明 |
|---|---|---|
| Markdown 与代码高亮 | 已实现 | markdown-it + Shiki,支持 tip/warning/danger/details。 |
| 分类与标签页面 | 已实现 | 详情页可跳转;标签首页导航与分类/标签分页未实现。 |
| 搜索 | 部分实现 | 开发 API + 生产 JSON;索引失败当前只警告并继续构建。 |
| RSS | 已实现 | 需要有效站点 URL 才能生成正确绝对链接。 |
| Sitemap | 部分实现 | 缺 /creed、/life、分页和 RSS。 |
| PWA | 部分实现 | 有 manifest/SW;固定 cache-first 可能长期返回旧页面。 |
| 404 | 缺失 | 没有自定义错误页,现有 Nginx 配置引用的 404.html 不存在。 |
| 评论、点赞、阅读数写入 | 缺失 | 字段或表存在,但公开站没有写入链路。 |
fonts.css 引用了多个 /fonts/* 文件,但当前
static/ 下未发现对应字体。浏览器会回退到系统字体,应在发布前补齐或移除无效引用。
05管理端 jy-admin
模块与路由
| 路由 | 功能 | 保护 |
|---|---|---|
/login | 用户名/密码登录 | 已登录用户跳转文章列表。 |
/articles | 文章列表 | segment layout 会话守卫。 |
/articles/new | 新建文章与 Markdown 分屏 | 同上。 |
/articles/[id]/edit | 编辑文章 | 同上。 |
/categories | 分类 CRUD | 同上。 |
/tags | 标签 CRUD | 同上。 |
/settings | 站点与部署设置 | 同上,但没有 admin-only RBAC。 |
/api/publish | 触发静态站构建 | API 内检查 session。 |
/api/publish/status | 查询最近 workflow | API 内检查 session。 |
认证模型
- 登录从
users读取 bcrypt 哈希并校验is_active。 - 会话由 iron-session 存放在加密的
jy_admin_sessionCookie。 - 生产 Cookie 使用
secure=true,因此管理端生产环境必须启用 HTTPS。 - 会话有效期为 7 天,
SESSION_SECRET必须至少 32 字符。 - 没有全局 middleware;四个业务 segment 各自通过 layout 守卫。
系统只在登录时允许 admin、author、editor,
登录后的 Server Actions 只检查“是否登录”。当前任意有效角色都能修改分类、标签、站点设置并触发部署。
数据写入结构
React Client Component
└─ Server Action ('use server')
├─ getSession() / requireUser()
├─ getPool() → mysql2/promise
├─ 事务 / 参数化 SQL
├─ revalidatePath()
└─ status=published 时可选 triggerSitePublish()
CRUD 覆盖
| 实体 | 覆盖 | 缺口 |
|---|---|---|
| 文章 | 创建 / 读取 / 更新 | 没有删除;可用 archived 代替软下线。 |
| 分类 | 完整 CRUD | 被文章引用时禁止删除。 |
| 标签 | 完整 CRUD | 删除时清理文章关联。 |
| 站点设置 | 部分 KV | enable_comments、theme 未暴露。 |
| 媒体 | 仅 URL | 没有上传、对象存储或媒体库。 |
| 用户 / 评论 | 未实现 | 只有登录读取;没有用户管理和评论审核。 |
生产运行
cd jy-admin
pnpm install --frozen-lockfile
pnpm verify
NODE_ENV=production pnpm start # :5174
当前仓库没有 PM2、systemd、Dockerfile 或 jy-admin 部署 workflow。仅执行
pnpm start 不能提供进程自愈、TLS、日志轮转和发布回滚。
06数据库与数据流
表职责
| 表 | 主要职责 | 应用使用 |
|---|---|---|
articles | Markdown、状态、分类、作者、计数与发布时间。 | admin 写;site 只读 published。 |
categories | 分类、slug、排序和启用状态。 | 双端使用;site 当前未过滤 is_active。 |
tags | 标签、slug、颜色和启用状态。 | 双端使用;site 当前未过滤 is_active。 |
article_tags | 文章与标签多对多关联。 | admin 事务替换;site 联表读取。 |
users | 管理登录与文章作者资料。 | admin 登录;site 显示作者。 |
site_settings | 站点与展示型部署配置。 | 双端读取不同键。 |
comments | 评论与回复结构。 | 未被应用代码使用。 |
Schema 管理现状
blog_site.sql是 DROP/CREATE/INSERT 的全量 dump,不是可重复的日常 migration。001与002是手工 SQL,没有版本表和自动 runner。- 数据库中存在三个 VIEW,但应用继续使用手写 JOIN,没有调用它们。
articles.author_id没有数据库外键,可能出现孤儿作者。- 没有备份、恢复、迁移校验或 schema drift 自动化。
07配置契约
应用环境变量
| 变量 | jy-site | jy-admin | 说明 |
|---|---|---|---|
DATABASE_HOST | 必需 | 必需 | 生产建议使用私网 DNS,例如 db。 |
DATABASE_PORT | 可选 | 可选 | 默认 3306。 |
DATABASE_USER | 必需 | 必需 | 不要在生产使用 root。 |
DATABASE_PASSWORD | 按环境 | 按环境 | 只进入 secret store,不写入仓库。 |
DATABASE_NAME | 可选 | 可选 | 默认 blog_site。 |
SITE_URL | 推荐 | — | sitemap、robots、RSS 的环境兜底。 |
SESSION_SECRET | — | 必需 | 至少 32 字符,需长期稳定保存。 |
GITHUB_REPOSITORY | — | 发布功能 | owner/repo。 |
GITHUB_TOKEN | — | 发布功能 | 最小化授权,可 dispatch 且可读 Actions。 |
DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_USER=jy_app
DATABASE_PASSWORD=${JY_DATABASE_PASSWORD}
DATABASE_NAME=blog_site
SITE_URL=https://blog.example.com
# jy-admin only
SESSION_SECRET=${JY_SESSION_SECRET}
GITHUB_REPOSITORY=example-org/jy-prj
GITHUB_TOKEN=${JY_GITHUB_TOKEN}
站点 URL 的优先级
公开站生成绝对链接时优先读取数据库 site_settings.site_url,其次读取
SITE_URL,最终会回退到占位域名。上线前必须至少配置前两者之一并保证一致。
GitHub Actions Secrets
| 当前 workflow 使用 | 用途 |
|---|---|
DATABASE_* | GitHub-hosted runner 在构建时直连数据库。 |
SITE_URL | 静态绝对 URL 兜底。 |
DEPLOY_SSH_HOST | rsync 目标主机。 |
DEPLOY_SSH_USER | SSH 用户。 |
DEPLOY_SSH_KEY | 部署私钥。 |
DEPLOY_SSH_PATH | Nginx 静态目录。 |
两个 .env.example 当前包含看起来像真实数据库密码的示例值。应替换为明显占位符;
如果该值曾用于任何环境,先轮换凭证,再检查 Git 历史。本文档不会复制该值。
数据库部署设置不是控制面
管理端可保存 deploy_method、Nginx 路径和 SSH 展示字段,但当前 GitHub Actions
不读取这些键。实际部署分支和目标完全由 Actions Secrets 决定,因此两处配置可能漂移。
08内容发布
文章状态语义
| 状态 | 管理端 | 公开站 |
|---|---|---|
draft | 可保存和继续编辑。 | 所有公开查询都不展示。 |
published | 首次进入状态时写入 published_at。 | 下一次构建后进入列表、详情、搜索和 RSS。 |
archived | 用于下线而不物理删除。 | 下一次构建后不再生成或展示。 |
线上更新路径
- 编辑并保存文章
jy-admin 在事务中写入 articles 与 article_tags。
- 决定是否触发构建
published 且
auto_publish_on_save=1时自动 dispatch;也可点击顶栏按钮。 - GitHub Actions 预渲染
runner 连接 MySQL,生成页面、搜索索引、RSS、sitemap 和 robots。
- 同步静态产物
当前使用 rsync
--delete到 Nginx 目录,或回退 gh-pages。 - 管理端轮询状态
构建中每 5 秒、空闲时每 30 秒查询最近 workflow run。
部署 workflow 设置 cancel-in-progress: true。短时间连续发布会取消前一轮构建,
只保留最新运行,适合单人博客,但构建与部署处于同一 job 时仍应避免在 rsync 阶段强制取消。
09当前部署链
已存在的自动化
deploy-jy-site.yml支持手动触发和jy-site-publishrepository dispatch。- 工作流固定 Node 版本文件与 pnpm 10.15.0,执行完整
build-static.ts。 - 配置 SSH 主机时 rsync 到 Nginx;否则发布到
gh-pages分支。 ci-jy-admin.yml只运行 TypeScript 检查,不构建也不部署管理端。
pnpm install --frozen-lockfile
node --import tsx scripts/build-static.ts
rsync -avz --delete \
jy-site/build/ "${DEPLOY_SSH_USER}@${DEPLOY_SSH_HOST}:${DEPLOY_SSH_PATH}/"
当前链路的生产风险
| 风险 | 影响 | 建议 |
|---|---|---|
| runner 必须直连 MySQL | 可能迫使数据库暴露公网。 | 改为自托管 runner、VPN/隧道,或推荐的服务器内构建。 |
Secrets 直接用于 step if | GitHub 对 secret 条件表达式支持受限,分支可能不按预期。 | 使用非敏感 variable/input 选择部署目标。 |
rsync --delete 原地覆盖 | 坏构建会立即替换线上且没有版本。 | 版本目录 + 原子软链 + 保留最近发布。 |
| 搜索索引失败只警告 | 可能部署旧索引或没有搜索。 | 生产构建应 fail closed,并验证 JSON。 |
| 没有部署后 smoke test | workflow 成功不等于站点可用。 | 检查首页、sitemap、搜索索引和代表文章。 |
| Nginx 仅 HTTP | 管理端 secure Cookie 无法正常工作,流量未加密。 | 部署 TLS 并强制 HTTP → HTTPS。 |
旧脚本不要作为生产入口
deploy.sh、deploy-elegant.sh 和旧部署指南仍执行
pnpm build,并描述已经不存在的 /jysite base path 与缺失的
nginx-fix-403.conf。生产文档和脚本应统一到 pnpm build:static。
10推荐 Docker 生产方案
下面的 Dockerfile、Compose 与 Nginx 是可落地参考,但当前仓库中不存在这些文件。
实施时应单独评审、写入 deploy/ 并在预发布环境验证。
建议目录
deploy/
├── compose.production.yml
├── admin.Dockerfile
├── site-builder.Dockerfile
├── nginx.conf
├── deploy-site.sh
├── backup-db.sh
└── env/
├── admin.env.example
├── database.env.example
└── site-build.env.example
/srv/jy/
├── app/ # 真实 Git checkout
├── site-releases/
│ ├── 20260814-abcdef/
│ └── current -> 20260814-abcdef
└── backups/
Compose 参考
name: jy-production
services:
db:
image: mysql:8.4
restart: unless-stopped
env_file: ./env/database.env
volumes:
- mysql-data:/var/lib/mysql
- ../jy-site/blog_site.sql:/docker-entrypoint-initdb.d/00-schema.sql:ro
- ../jy-site/migrations/001_site_deploy_settings.sql:/docker-entrypoint-initdb.d/01-settings.sql:ro
- ../jy-site/migrations/002_auto_publish_setting.sql:/docker-entrypoint-initdb.d/02-auto-publish.sql:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 12
networks: [backend]
admin:
build:
context: ../jy-admin
dockerfile: ../deploy/admin.Dockerfile
restart: unless-stopped
env_file: ./env/admin.env
environment:
NODE_ENV: production
DATABASE_HOST: db
DATABASE_PORT: 3306
depends_on:
db:
condition: service_healthy
expose: ["5174"]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5174/login >/dev/null || exit 1"]
interval: 20s
timeout: 5s
retries: 6
networks: [frontend, backend]
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
depends_on:
admin:
condition: service_healthy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /srv/jy/site-releases:/srv/jy/site-releases:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
networks: [frontend]
site-builder:
profiles: ["tools"]
build:
context: ..
dockerfile: deploy/site-builder.Dockerfile
env_file: ./env/site-build.env
environment:
DATABASE_HOST: db
DATABASE_PORT: 3306
depends_on:
db:
condition: service_healthy
volumes:
- /srv/jy/site-releases:/releases
networks: [backend]
volumes:
mysql-data:
networks:
frontend:
backend:
internal: true
当前 dump 来自 MySQL 5.7.44。示例使用 8.4 LTS 作为目标,不代表已经兼容验证。 实施前必须在空库导入 schema、运行两端检查与静态构建,并验证字符集、时间戳、ENUM 和 SQL 行为。
管理端镜像参考
FROM node:22.22.1-alpine AS deps
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM deps AS build
COPY . .
RUN pnpm build
FROM node:22.22.1-alpine AS runtime
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/.next ./.next
COPY --from=build --chown=node:node /app/public ./public
USER node
EXPOSE 5174
CMD ["pnpm", "start"]
当前 Next 配置没有 output: 'standalone',因此参考镜像复制完整
node_modules。后续可单独启用 standalone 减小镜像,但必须重新验证
iron-session 与文件追踪。
静态构建镜像参考
FROM node:22.22.1-alpine
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /workspace/jy-site
COPY jy-site/package.json jy-site/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY jy-site/ ./
CMD ["pnpm", "build:static"]
Nginx 参考
server {
listen 80;
server_name blog.example.com admin.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
root /srv/jy/site-releases/current;
index index.html;
location / {
try_files $uri $uri.html $uri/ =404;
}
location ^~ /_app/immutable/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location = /search-index.json {
add_header Cache-Control "public, max-age=300";
}
location = /healthz {
access_log off;
return 200 "ok\n";
}
}
server {
listen 443 ssl;
http2 on;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
# 推荐在此加 VPN、SSO 或 allow/deny IP 策略
location / {
proxy_pass http://admin:5174;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
11自动化与回滚
推荐发布原则
- GitHub Actions 只负责鉴权和触发,MySQL 不对 GitHub-hosted runner 开放。
- 构建在服务器内部通过一次性
site-builder容器完成。 - 产物写入版本目录,验证成功后原子切换相对软链
current。 - 任何一步失败都不触碰当前线上版本;保留最近 5 个版本供秒级回滚。
服务器发布脚本参考
#!/usr/bin/env sh
set -eu
release_id="${1:?release id required}"
release_dir="/srv/jy/site-releases/${release_id}"
mkdir -p "${release_dir}"
docker compose -f deploy/compose.production.yml run --rm \
--entrypoint sh site-builder -c \
'pnpm build:static && test -s build/index.html && test -s build/sitemap.xml && test -s build/search-index.json && cp -a build/. "/releases/'"${release_id}"'/"'
ln -sfn "${release_id}" /srv/jy/site-releases/.current.next
mv -Tf /srv/jy/site-releases/.current.next /srv/jy/site-releases/current
docker compose -f deploy/compose.production.yml exec -T nginx nginx -s reload
curl --fail --silent --show-error https://blog.example.com/ >/dev/null
curl --fail --silent --show-error https://blog.example.com/sitemap.xml >/dev/null
上例展示发布语义。实际容器内挂载点与宿主机路径必须统一;旧版本清理由独立、受测试的保留策略完成,
并严格限制在 /srv/jy/site-releases。上线前先用临时目录演练。
Actions 触发参考
- name: Configure SSH
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
- name: Build and atomically publish on server
env:
HOST: ${{ secrets.DEPLOY_SSH_HOST }}
USER: ${{ secrets.DEPLOY_SSH_USER }}
RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
run: |
ssh -i ~/.ssh/deploy_key "$USER@$HOST" \
"cd /srv/jy/app && ./deploy/deploy-site.sh '$RELEASE_ID'"
不要使用运行时 ssh-keyscan 自动信任未知主机;把经过人工核验的 host key 存入
DEPLOY_KNOWN_HOSTS。
回滚
cd /srv/jy/site-releases
test -s "20260813-previous/index.html"
ln -sfn "20260813-previous" .current.next
mv -Tf .current.next current
docker compose -f /srv/jy/app/deploy/compose.production.yml exec -T nginx nginx -s reload
curl --fail https://blog.example.com/
数据库备份与恢复演练
umask 077
stamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p /srv/jy/backups
docker compose -f deploy/compose.production.yml exec -T db sh -c \
'exec mysqldump --single-transaction --routines --triggers \
-u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \
| gzip -9 > "/srv/jy/backups/blog_site-${stamp}.sql.gz"
sha256sum "/srv/jy/backups/blog_site-${stamp}.sql.gz" \
> "/srv/jy/backups/blog_site-${stamp}.sha256"
- 备份文件应复制到异机或对象存储,不能只放在同一块磁盘。
- 至少每季度在隔离数据库完成一次恢复演练并记录耗时。
- 迁移前、批量内容修改前和账号变更前创建额外备份。
12安全与可观测性
生产安全基线
- 数据库仅加入内部 Docker 网络,不映射宿主机 3306。
- 管理端使用独立子域名和 HTTPS,并优先置于 VPN、零信任访问或 IP 白名单后。
- 使用独立最小权限数据库用户;构建只读、管理端读写、备份单独授权。
- SESSION_SECRET、数据库密码、GitHub PAT 和 SSH 私钥全部由 secret store 注入。
- 对登录 API 加速率限制、失败审计和告警;高风险操作按角色授权。
- Markdown 原始 HTML 只允许可信作者;当前公开渲染开启
html: true。 - 为管理端和 Nginx 设置安全响应头,并关闭不必要的版本暴露。
建议的最小 RBAC
| 角色 | 建议权限 |
|---|---|
| admin | 全部内容、用户、设置、部署与密钥状态。 |
| editor | 编辑和发布所有文章、管理分类标签;不能改系统设置或用户。 |
| author | 创建并编辑自己的草稿;发布需 editor/admin 审核。 |
健康检查与监控
| 检查 | 当前 | 建议目标 |
|---|---|---|
| 公开站 HTTP | 无专用端点 | Nginx /healthz + 首页与代表文章 synthetic check。 |
| 管理端就绪 | 无专用端点 | 新增只检查进程的 liveness 与检查 DB 的 readiness。 |
| 数据库 | 连接时才暴露错误 | Compose healthcheck、连接池错误率与备份新鲜度告警。 |
| 部署 | 只显示 Actions 结论 | 构建产物校验、部署后 smoke test、失败自动回滚。 |
| 日志 | console + Nginx 默认日志 | JSON 结构化日志、request ID、轮转与集中保留。 |
13完备度审计
它适合本地使用、可信维护者的小型博客以及受控的静态站发布;在补齐下列 P0/P1 项之前,不应按“无人值守、可恢复、可审计的生产系统”来承诺。
上线阻塞:先解决安全与可恢复性
- 替换并轮换示例环境文件中的凭证式值,确认未进入 Git 历史。
- 建立可用管理员账号生成流程,禁止继续使用占位 hash。
- 启用 HTTPS,限制管理端访问,并把 MySQL 放入私网。
- 建立异机数据库备份与恢复演练。
- 修复 Actions 部署目标分支逻辑,取消数据库公网直连依赖。
- 确认代码位于真实 Git 仓库并配置受保护默认分支。
生产工程化:让部署可验证、可回滚
- 落地 Docker/Compose、管理端部署、健康检查和结构化日志。
- 静态站使用版本目录原子发布,部署后 smoke test,保留最近版本。
- CI 覆盖 site check/lint/build 与 admin check/build;测试脚本纳入自动化。
- 搜索索引失败改为构建失败;补齐 404、sitemap 与 PWA 更新策略。
- 实现 RBAC、登录限流、失败审计;为高风险设置和发布操作加授权。
- 补齐或移除缺失字体,清理旧部署脚本和过期路径文档。
产品完整性:按真实需求扩展
- 文章删除/回收站、媒体上传与对象存储。
- 用户管理、评论审核、浏览/点赞统计。
- 服务端文章搜索/筛选与大数据量分页。
- 删除死依赖、未使用 DB VIEW 与无消费者配置,统一默认值。
上线前验收清单
- 两个项目的 check/build 在干净环境通过。
- MySQL 迁移、管理员登录、文章草稿/发布/归档均已演练。
- 首页、文章、分类、标签、搜索、RSS、sitemap 与 404 已验证。
- 管理端 HTTPS、Cookie、退出登录、RBAC 和限流已验证。
- 从发布触发到静态上线的整条链路有日志和失败告警。
- 静态版本回滚与数据库恢复各演练至少一次。
- 域名、证书续期、备份保留、监控联系人有明确负责人。
14故障排查
| 现象 | 优先检查 | 处理 |
|---|---|---|
| 管理端启动即报 SESSION_SECRET | jy-admin/.env | 生成并持久保存至少 32 字符 secret,重启进程。 |
| 数据库导入后无法登录 | users.password_hash | seed 是占位 hash;生成 bcrypt hash 并更新用户。 |
| 文章已发布但线上没有 | Actions 最近 run、文章状态、DB 连通 | 确认发生了完整 build:static + deploy,而非只保存数据库。 |
| 生产搜索为空 | build/search-index.json | 检查索引生成日志、文件 total 与缓存头;重新完整构建。 |
| RSS/sitemap 出现占位域名 | site_settings.site_url、SITE_URL | 配置真实 HTTPS origin 后重新构建。 |
| 页面字体与设计不一致 | Network 中 /fonts/* 404 | 补齐授权字体文件或删除对应 @font-face。 |
| Next 开发热更新后 500 | .next 缓存 | 运行 pnpm dev:reset。 |
| 未知 URL 返回首页而非 404 | adapter fallback 与 Nginx try_files | 生产 Nginx 使用目录入口并以 =404 收尾。 |
| SSH 部署分支未执行 | workflow 的 secret 条件 | 改用 repository variable/input 选择 deploy target。 |
| 部署后静态资源混用旧版 | SW、浏览器缓存、原地 rsync | 更新 SW 策略并采用原子版本目录,必要时注销旧 SW。 |
诊断顺序
- 先确认是哪一层:浏览器、Nginx、admin Node、构建容器还是 MySQL。
- 记录请求 URL、时间、发布 run ID 与对应代码版本。
- 只执行不会破坏现场的只读检查;备份后再进行迁移或回滚。
- 修复后从用户路径复测,而不是只看进程退出码。
15事实来源
本手册把“仓库已有事实”和“建议生产方案”分开描述。以下文件是核对当前行为的主要来源; 相对链接在以文件方式打开手册时可直接定位到同一工作区文件。
| 主题 | 源文件 |
|---|---|
| 整体架构 | README.md |
| 公开站依赖与命令 | jy-site/package.json |
| 静态适配器 | jy-site/svelte.config.js |
| 完整静态构建 | jy-site/scripts/build-static.ts |
| 搜索索引 | jy-site/scripts/generate-search-index.ts |
| 公开站数据库连接 | jy-site/src/lib/server/mysql-pool.ts |
| 管理端依赖与命令 | jy-admin/package.json |
| 认证会话 | jy-admin/lib/session.ts |
| 文章 Server Actions | jy-admin/lib/actions/article.ts |
| GitHub 发布集成 | jy-admin/lib/github.ts |
| 数据库 Schema | jy-site/blog_site.sql |
| 当前发布 workflow | .github/workflows/deploy-jy-site.yml |
| 当前 Nginx 样例 | jy-site/nginx.conf |
当路由、环境变量、schema、构建入口或 workflow 发生变化时,同一 PR 应同步更新本手册。 建议每次生产发布前把“上线前验收清单”作为可执行 checklist 复核。