版本: v1.0 | 2026-08-14
适用环境: 阿里云 ECS / Ubuntu 24.04 LTS / 2C2G
本文档已去敏: 所有 IP、密码、密钥路径均为占位符
一、架构总览
公网:8899 → Nginx
├── / → Typecho (PHP-FPM 8.3)
├── /admin/ → Typecho 后台
└── /usr/uploads → 附件目录 (浏览器缓存30天)
数据层: SQLite 单文件数据库 (/opt/typecho/usr/*.db)
主题: Joe (已打 SQLite/PHP8.3 兼容补丁)
服务: Nginx + PHP-FPM (systemd 托管, 开机自启)环境要求:
| 项 | 版本 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| Web | Nginx 1.24+ |
| PHP | 8.3 (FPM) |
| 数据库 | SQLite3 (免 MySQL) |
| 内存 | 2G 足够 (实际占用 ~500M) |
二、部署步骤
第 1 步: 服务器基础
apt update && apt install -y nginx php8.3-fpm \
php8.3-sqlite3 php8.3-mbstring php8.3-curl php8.3-gd php8.3-xml第 2 步: 上传 Typecho
# 本地下载 (Typecho 官方 GitHub master 分支)
# 解压后整体上传到 /opt/typecho/
# 目录权限:
chown -R www-data:www-data /opt/typecho
chmod -R 755 /opt/typecho第 3 步: Nginx 配置
server {
listen 8899; # 非标准端口, 降低扫描风险
server_name 服务器IP;
server_tokens off;
client_max_body_size 50m;
root /opt/typecho;
index index.php index.html;
# gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/css text/javascript application/javascript application/json image/svg+xml;
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# 静态资源缓存 (7天)
location ~* \.(css|js|png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files $uri $uri/ /index.php?$query_string;
}
# 上传文件缓存 (30天)
location /usr/uploads/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# 禁止访问数据库等敏感文件
location ~* \.(db|sqlite|sqlite3)$ { deny all; }
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* { deny all; }
}登录限速 (防暴力破解, 加在 nginx.conf 的 http 块):
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=30r/m;# sites-available 里加:
location = /action/login {
limit_req zone=login_limit burst=5 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}第 4 步: 浏览器安装
访问 http://服务器IP:8899/install.php
→ 数据库适配器选 SQLite
→ 设置管理员账号密码 (自定义)
→ 安装完成后删除 install.php 和 install/ 目录第 5 步: 安全加固 (必做)
# 1. 删除安装文件
rm -f /opt/typecho/install.php
rm -rf /opt/typecho/install
# 2. 数据库文件禁止 web 访问 (已含在 Nginx 配置)
# 3. 后台设置:
# - 设置 → 评论 → 开启评论审核
# - 密码设 12 位以上混合字符第 6 步: 阿里云安全组
| 方向 | 端口 | 说明 |
|---|---|---|
| 入方向 | 22 | SSH (仅密钥登录) |
| 入方向 | 8899 | 博客访问 |
| 其他 | 全部拒绝 | — |
三、Joe 主题部署与补丁
主题安装
# Joe 主题上传到 /opt/typecho/usr/themes/joe/
chown -R www-data:www-data /opt/typecho/usr/themes/joe
# 后台 → 外观 → 启用 Joe → 设置外观 → 保存一次(初始化配置)必打补丁 (Joe 与 Typecho 1.3 + SQLite 兼容)
Joe 原作者已停更,不原生兼容 Typecho 1.3+/SQLite,需打三个补丁:
补丁 1: SQLite 注册 MySQL 兼容函数
文件: var/Typecho/Db/Adapter/Pdo/SQLite.php 的 init() 方法中,PDO 创建后注册:
// RAND() → 0~1 随机数
$pdo->sqliteCreateFunction('RAND', function () {
return mt_rand() / mt_getrandmax();
}, 0);
// FROM_UNIXTIME(ts, fmt)
$pdo->sqliteCreateFunction('FROM_UNIXTIME', function ($ts, $fmt = '%Y-%m-%d %H:%i:%s') {
$phpFmt = str_replace(
['%Y', '%y', '%m', '%c', '%d', '%e', '%H', '%k', '%i', '%s', '%M', '%j'],
['Y', 'y', 'm', 'n', 'd', 'j', 'H', 'G', 'i', 's', 'F', 'z'],
(string) $fmt
);
return date($phpFmt, intval($ts));
}, 2);
// UNIX_TIMESTAMP()
$pdo->sqliteCreateFunction('UNIX_TIMESTAMP', function ($v = null) {
return $v === null ? time() : strtotime((string) $v);
}, 1);
// DATE_FORMAT(date, fmt)
$pdo->sqliteCreateFunction('DATE_FORMAT', function ($date, $fmt) {
$phpFmt = str_replace(
['%Y', '%y', '%m', '%c', '%d', '%e', '%H', '%k', '%i', '%s'],
['Y', 'y', 'm', 'n', 'd', 'j', 'H', 'G', 'i', 's'],
(string) $fmt
);
$ts = is_numeric($date) ? intval($date) : strtotime((string) $date);
return $ts ? date($phpFmt, $ts) : null;
}, 2);补丁 2: 随机文章 RAND() 改 PHP 实现
文件: usr/themes/joe/core/function.php 的 _getAsideAuthorNav(),SQL 里的 RAND() 改为:
$minMax = $db->fetchRow("SELECT MIN(cid) AS minCid, MAX(cid) AS maxCid FROM `{$prefix}contents` WHERE type='post' AND status='publish'");
$randomCid = ($minMax && $minMax['maxCid']) ? mt_rand(intval($minMax['minCid']), intval($minMax['maxCid'])) : 0;
$sql = "SELECT * FROM `{$prefix}contents` WHERE cid >= {$randomCid} and type='post' and status='publish' and (password is NULL or password='') ORDER BY cid LIMIT $limit";补丁 3: 建站日期必须带时分秒
Joe 的运行时间计数器正则要求格式 YYYY/M/D HH:MM:SS:
- 后台 Joe 设置 → 建站日期 → 必须填
2026/8/13 17:30:00格式(带时间),否则计数器显示 00
品牌自定义
| 位置 | 修改 |
|---|---|
| 后台 → 设置 → 基本 → generator | PacketForge 1.3.0(保留版本号,改名前半部分即可,去掉版本号会导致后台 500) |
| 后台登录页 Logo | admin/login.php 中 h1 链接改为自己品牌 |
| Joe 页脚 | 外观设置 → JFooter_Left / JFooter_Right |
四、运维手册
日常操作
| 操作 | 命令/位置 |
|---|---|
| 写文章 | 后台 /admin/ 撰写 |
| 备份数据库 | 复制 /opt/typecho/usr/*.db 文件即可(SQLite 单文件) |
| 备份 Joe 配置 | 复制 typecho_options 表 theme:joe 记录 |
| 查看日志 | tail -f /var/log/nginx/error.log |
备份脚本
#!/bin/bash
# 备份 SQLite 数据库 + Joe 配置
BK=/opt/backups/$(date +%Y%m%d)
mkdir -p $BK
cp /opt/typecho/usr/*.db $BK/
php8.3 -r '$db = new SQLite3("/opt/typecho/usr/数据库名.db");
$v = $db->querySingle("SELECT value FROM typecho_options WHERE name=\"theme:joe\"");
file_put_contents("'$BK'/joe-config.json", json_encode(json_decode($v), JSON_PRETTY_PRINT));'故障恢复
| 故障 | 处理 |
|---|---|
| 前台 500 | 看 /var/log/nginx/error.log 的 PHP message;常见原因是主题配置被清空 → 恢复 joe-config 备份 |
| 后台 500 | 检查 generator 字段格式是否为 "名字 版本号"(两个词) |
| 主题切换后 Joe 配置丢失 | Typecho 切换主题会清空旧主题配置——切之前先备份 theme:joe 记录 |
| 运行时间计数器 00 | 建站日期没带时分秒,补成 YYYY/M/D HH:MM:SS |
已知注意事项
- 不要把 generator 字段改成无版本号格式(如纯 "PacketForge"),Typecho 内部解析版本会崩溃
- 不要在后台切换主题前不备份——Joe 配置会被清空
- Typecho 升级时
Pdo/SQLite.php适配器补丁可能被覆盖,升级后需重新打补丁 1 - 上传限制: PHP-FPM 的
upload_max_filesize=20M,post_max_size=32M
五、安全基线
| 项 | 状态 |
|---|---|
| 数据库文件 web 访问 | ✅ deny all |
| install.php | ✅ 已删除 |
| 登录限速 | ✅ 30r/m + burst 5 |
| 安全响应头 | ✅ 4 项 |
| 版本号泄露 | ✅ server_tokens off |
| 开放注册 | ✅ 关闭 |
| XML-RPC | ✅ 仅登录用户 |
| 附件类型 | ✅ 仅图片 |
| SSH | ✅ 仅密钥 |
| 安全组 | ✅ 仅 22 + 8899 |
去敏说明: 本文档中服务器 IP、管理员密码、数据库文件名、密钥路径均已用占位符替代。实际部署时替换为真实值。
nice
[...]https://packetforge.cn/index.php/archives/6/[...]