从零搭一个 Typecho 博客:LNMP + Nginx + SSL
这个博客是我从零搭起来的——没有用 WordPress 那种一键安装包,也没有买虚拟主机,而是租了一台最便宜的云服务器,从装 Nginx 开始一步一步配出来。这篇文章把整个过程记录下来,包括中间踩的坑。
为什么不用现成的
最省事的做法当然是买虚拟主机、上传 WordPress、五分钟建站。我没这么选,原因有三条:
第一,我想搞清楚一个网站从请求到响应到底经过了什么。虚拟主机把这一层全藏起来了,出问题时你只能提工单。
第二,我想自己控制主题和模板。市面上现成的主题改起来束手束脚,不如自己写。
第三,也是最重要的一条——我想有个能持续折腾的东西。这篇文章本身就是折腾的产物。
服务器与软件栈
租的是阿里云 ECS,配置是最低档:
2 核 CPU / 2GB 内存 / 40GB 系统盘 / 1Mbps 带宽,操作系统 Ubuntu 22.04.5 LTS。
带宽只有 1Mbps,这是最需要提前想清楚的一点。1Mbps 约等于 128KB/s,一张没压缩的大图就能把页面卡住几秒。所以后面做图片上传时,生成缩略图不是可选项,而是必须做的事。
最终的软件栈:
| 组件 | 版本 | 作用 |
|---|---|---|
| Nginx | 1.18.0 | Web 服务器,处理静态文件和反向代理 |
| PHP-FPM | 8.1.2 | 执行 PHP 脚本 |
| MySQL | 8.0.46 | 存储文章、分类、评论 |
| Typecho | 1.3.0 | 博客程序本体 |
第一步:装 LNMP
LNMP 是 Linux + Nginx + MySQL + PHP 的缩写。Ubuntu 上装这几个东西很直接:
apt update && apt upgrade -y
apt install nginx -y
apt install php8.1-fpm php8.1-mysql php8.1-mbstring \
php8.1-curl php8.1-xml php8.1-gd php8.1-zip -y
apt install mysql-server -y
systemctl enable nginx php8.1-fpm mysql
systemctl start nginx php8.1-fpm mysql这里有几个扩展值得单独说:
php8.1-mbstring 处理多字节字符。中文博客少了它,截断字符串时会把一个汉字劈成两半,显示出乱码。
php8.1-gd 是图像处理扩展。后面给相册生成缩略图全靠它,装漏了上传功能直接报错。
php8.1-zip 用于解压和备份,Typecho 的备份功能需要。
建数据库
mysql -u root -p
CREATE DATABASE typecho DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'typecho'@'localhost' IDENTIFIED BY '你的密码';
GRANT ALL PRIVILEGES ON typecho.* TO 'typecho'@'localhost';
FLUSH PRIVILEGES;字符集一定要用 utf8mb4 而不是 utf8。MySQL 里的 utf8 其实是个历史遗留的坑——它每个字符最多只存 3 字节,存不了 emoji 和部分生僻汉字。写文章时打个 emoji 就报错,很难查。
第二步:配置 Nginx
新建站点配置文件 /etc/nginx/sites-available/blog:
server {
listen 80;
server_name 你的域名;
root /var/www/blog;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}几个关键点:
try_files $uri $uri/ /index.php?$args 是伪静态规则。它的意思是:先找有没有这个文件,再找有没有这个目录,都没有就把请求交给 index.php 处理。没有这行,Typecho 的 /archives/17/ 这种链接全是 404。
fastcgi_pass 走的是 Unix socket 而不是 TCP 端口。socket 是同一台机器上的进程间通信,省掉了 TCP 协议栈的开销。Nginx 和 PHP-FPM 在同一台机器上时,socket 是更好的选择。
最后 deny all 那条是拒绝访问所有 .ht* 开头的文件,防止配置文件被下载。
然后启用站点并重启:
ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginxnginx -t 是语法检查。养成改完配置先跑一遍的习惯,否则 reload 之后整个站点都起不来,而错误信息只有一行。
第三步:装 Typecho
wget https://github.com/typecho/typecho/releases/latest/download/typecho.zip
unzip typecho.zip -d /tmp/typecho
cp -r /tmp/typecho/* /var/www/blog/
chown -R www-data:www-data /var/www/blog
chmod -R 755 /var/www/blog注意 www-data 这个用户。Nginx 和 PHP-FPM 都是以它的身份运行的,文件属主不对,Typecho 就没法写配置文件、传不了图片。这个坑我踩过——上传图片一直失败,浏览器控制台只显示 500,最后查 PHP 错误日志才发现是权限问题。
之后浏览器打开域名,跟着安装向导填数据库信息就行。安装完成后记得把 install.php 删掉。
第四步:上 HTTPS
用 Let's Encrypt 的免费证书,配合 certbot 一条命令搞定:
apt install certbot python3-certbot-nginx -y
certbot --nginx -d 你的域名 -d www.你的域名certbot 会问你几个问题,其中一个是要不要自动把 HTTP 重定向到 HTTPS,选是。它还会自动往 Nginx 配置里写入证书路径和跳转规则。
证书有效期 90 天,certbot 装的时候会自动创建一个 systemd timer 来续期。可以手动验证一下续期流程:
certbot renew --dry-run看到 Congratulations, all simulated renewals succeeded 就说明没问题。
踩过的坑,以及怎么解决的
装软件只花了一个下午,真正耗时间的是后面这些。每一个我都写清楚最后是怎么解决的。
坑一:MySQL 半夜被系统杀掉
现象。站点跑起来没几天,出现了几次白天正常、早上起来打不开的情况。看 journalctl 发现 MySQL 进程被 OOM Killer 杀掉了,日志里一行 Out of memory: Killed process (mysqld)。
原因。2GB 内存要同时装下 MySQL、PHP-FPM、Nginx 三套东西。MySQL 8.0 默认的 innodb_buffer_pool_size 是 128M,但它的实际内存占用远不止这个数——还有连接线程、排序缓冲、临时表,以及默认开启的 performance_schema(光这一项就要吃掉一百多兆)。几个请求一叠加,内存就爆了。
解决。分两步。
第一步是加 swap。2GB 的机器上 swap 不是"性能优化",是保命措施——内存峰值时让内核有地方可以换出,而不是直接杀进程:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 写入 fstab,保证重启后自动挂载
echo '/swapfile none swap sw 0 0' >> /etc/fstab最后那行 echo 很容易漏。漏了的话 swap 只在当前会话有效,服务器一重启就没了——而 OOM 往往正是发生在重启之后。
第二步是调 MySQL 的内存占用。新建一个独立的配置文件,而不是直接改默认的 mysqld.cnf:
# /etc/mysql/mysql.conf.d/zzz-typecho-optimization.cnf
[mysqld]
innodb_buffer_pool_size = 256M
performance_schema = OFF
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_log_file_size = 64M用独立文件有个好处:文件名以 zzz 开头,在 conf.d 目录里按字母序排最后,会覆盖前面的默认值。以后升级 MySQL 时,默认配置文件被覆盖了也不影响这些设置。
逐项说明:
| 配置 | 作用 |
|---|---|
innodb_buffer_pool_size = 256M | InnoDB 的缓存池,也是最占内存的一项。给了 256M——比默认的 128M 大,因为博客数据量小,多给缓存能减少磁盘 IO |
performance_schema = OFF | 性能监控表,默认开启,占一百多兆内存。个人博客用不上,直接关掉——这一项省下的内存最多 |
innodb_flush_log_at_trx_commit = 2 | 默认是 1,每次事务提交都刷盘,最安全但最慢。改成 2 表示每秒刷一次,性能明显提升。代价是极端情况下(操作系统崩溃)可能丢最后一秒的数据——对博客可以接受,对银行不行 |
innodb_flush_method = O_DIRECT | 绕过操作系统的文件缓存,避免同一份数据在 MySQL 缓冲池和系统缓存里各存一份,省内存 |
innodb_log_file_size = 64M | 重做日志大小 |
另外把最大连接数从默认的 151 降到 20——我的访问量根本用不到那么多连接,每个空闲连接都要占内存:
max_connections = 20改完重启 MySQL 生效。之后再也没有出现过 OOM。
坑二:PHP-FPM 也会吃光内存
现象。MySQL 稳定之后,偶尔还是会有请求超时。
原因。PHP-FPM 的进程池默认配置是给大机器准备的。默认 pm.max_children 是 5,每个 PHP 进程处理图片这类任务时可能吃到几十兆内存,5 个一起跑就能把剩余内存吃光。
解决。调整进程池配置,把并发数压下来:
; /etc/php/8.1/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3pm = dynamic 表示进程数按需伸缩。max_spare_servers = 3 是关键——待命进程最多 3 个,超出的会被回收,不会一直占着内存。
代价是并发能力弱:同时来 6 个请求,第 6 个要排队等。但博客的实际并发通常就是个位数,这个取舍是划算的。
坑三:带宽是瓶颈,不是 CPU
1Mbps 带宽下,一张 2MB 的原图要传 16 秒。这个限制直接决定了后面所有的设计:图片必须生成缩略图、列表页只加载缩略图、首页不放太大的头图。
这不是一个"改个配置就能解决"的问题——带宽是要花钱买的。我的应对方式是把设计约束前移:与其等页面卡了再优化,不如一开始就假设"每张图都不能超过 200KB"。
具体做法写在了相册那篇里(400px 缩略图 + 只缩不放 + 大图延迟加载)。
坑四:中文搜索搜不到
现象。Typecho 自带的搜索框输入中文关键词,经常搜不出结果。
原因。它底层用的是 LIKE '%关键词%'。这种方式有两个问题:一是全表扫描,数据一多就慢;二是需要连续匹配,而中文没有空格分词,一个词在标题和正文里被标点隔开就匹配不到。
解决。给文章表加 MySQL 的全文索引:
ALTER TABLE typecho_contents ADD FULLTEXT INDEX ft_contents (title, text);全文索引内部会把文本切成词并建立倒排索引,查询时不用扫全表。但要让它对中文生效,还需要在 MySQL 配置里指定分词方式(MySQL 8.0 用 ngram 解析器按固定长度切分中文):
[mysqld]
ngram_token_size = 2ngram_token_size = 2 表示按 2 个字一组切分,这是中文全文检索的常用配置——中文里双字词占比最高。
说实话,这一条我目前还没做——站点文章还少,默认的 LIKE 搜索在几十篇文章的规模下感觉不出差别。但我知道数据量上去之后该怎么办,也知道为什么。
小结
整个搭建过程大概花了一个下午。回头看,真正花时间的不是装软件,而是排查问题:权限、字符集、内存、带宽,每一项出问题时的报错都不太直观。
但也正是这些坑让我第一次真正理解了一个请求的完整路径:浏览器发起请求 → 域名解析 → Nginx 接收 → 判断是静态文件还是 PHP → 转给 PHP-FPM → PHP 查 MySQL → 生成 HTML 返回。以前写代码时这些都是一团模糊的概念,现在每一环都摸过了。
暂无评论