MENU

【歪门邪道】博客升级、容器化、PHP 8.3

June 1, 2026 • 瞎折腾

博客自2017年独立设站以来,历经几次更新。最近一次还是2023年,从Debian换成了Ubuntu 2204,后来又升级到了2404。那个时候lnmp脚本还没卖给国内的公司,脚本也没有挂上后门,于是就一直用到上个月。大约是4月底,Ubuntu 2604 LTS发布,我开始盘算这次的升级。

确定升级方案

由于Typecho一直更新缓慢,所以我的服务器每次更新妥当之后也就没动过。升级前用的是typecho 1.2.1和php 8.1。前阵子typecho突然发布了1.3.0,但这次更新包含很多破坏性修改,按semantic versioning来说不应该继续沿用1.x版本。我之前试过更新,结果更新之后主题不兼容,导致网站爆炸。幸好服务器和数据库都有快照,分别恢复之后继续如常运行,后来为了缓解xss攻击的隐患,我在垃圾评论检查的插件中配置了一些规则,虽然不能完全防止xss攻击,但至少能防住一些靠复制粘贴搞事情的人。

前几天日常检查的时候发现我这个主题难得更新了,这次更新适配了typecho 1.3.0和php 8.3,于是想着要不要就此机会升级一下服务器的系统。虽然我自己平时用OpenSUSE风滚草和Fedora,但服务器上我的日常之选还是Ubuntu LTS,主打一个更新方便,支持周期长。但那是2404到2604的升级路线还没有发布,要等2604.1发布之后才行。而另一个迫在眉睫的问题在于,lnmp脚本已经不能使用了,要升级服务器必须得自己搞一个替代品。

很幸运的是,现在的我比建站时的那个17岁小伙子已经进步了不少了,有了更多的见识和经验,即便我不会写php,也能搭建出一套可持续的部署方案了。

鉴于我写了那么多年Java,谈到服务器部署,自然首选是容器化。目前网站只有一台服务器,所以大可不必上什么docker swarm甚至k8s这种东西,为了确保配置能够追踪,我选择在私有repo上存储docker compose的yaml文件。这样未来我做的每一次修改都可以追踪并记录原因。同时未来迁移服务器的时候也可以直接克隆后部署,不必考虑一套配置重复写好几次的事情。另外容器化部署的好处就是不挑系统,因此未来可以直接升级后续的LTS,或者我要是突然来了性质,重装个别的系统然后重新部署docker compose,也是轻而易举的。

因此正式的升级路线就确定了:

  • 重装到Ubuntu 2604 LTS
  • 使用容器化部署PHP
  • 升级主题和typecho
  • 升级到PHP 8.3

虽然PHP 8.3已经结束支持了,到2027年年底,安全支持也要结束了,但这也没办法,主题还在用老的jQuery呢。先把服务器整明白了,后面再慢慢修改主题。

容器化部署PHP

一开始我打算每个网站一个独立的nginx + php-fpm,最外层在主机上用Caddy做反向代理。这样对于现存环境的兼容性是最高的。但想来想去总觉得别扭,一来是php不能顺利复用,二来要配置的东西重复性比较高,第三就是尽管Nginx很高效,但我并没有真正理解过它那个难写的配置文件,大部分时候都是照葫芦画瓢,能用就行了。后来请LLM评论我的计划时,LLM提到Caddy也能配合PHP使用,我喜出望外。搜索了一番发现,Caddy不光能和PHP一起用,对于typecho伪静态的支持是开箱即用(Caddy2),不需要配置什么rewrite规则。此外使用Caddy还有一个好处,就是它能够自动处理TLS证书,不需要我手动每年更新,也不需要我再花钱买专门的TLS证书了。

于是根据Nginx的配置写了Caddyfile,根据旧的php配置(主要是php.iniphp-fpm.conf)编写新的php配置。

Caddyfile的编写其实还挺简单的,由于不需要处理rewrite之类的东西,并且caddy自带了对php fastcgi的支持,因此不停include的nginx配置文件就简化成了这一小段:

skyblond.info, www.skyblond.info {
    # 网站挂载到容器的/srv/目录下
    root * /srv/skyblond.info
    # 启用gzip和zstd压缩
    encode gzip zstd
    # 配置PHP,超时时间匹配nginx配置
    php_fastcgi php-fpm:9000 {
        dial_timeout  30s
        read_timeout  5m
        write_timeout 5m
    }
    # 允许caddy返回静态文件
    file_server
    # HSTS header,本站很久之前就加入HSTS preload列表了
    # 这个header告诉浏览器skyblond.info以及所有子域名必须强制使用https
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    }
    # 对于可变文件的缓存,主要是css和js,因为后期要对主题进行修改
    @cacheMutable {
            file
            path *.css *.js
    }
    handle @cacheMutable {
        # 缓存12小时
        header Cache-Control "public, max-age=43200"
    }
    # 匹配旧的Nginx配置,允许最大50MB的请求体
    request_body {
        max_size 50MB
    }
    # 不可变文件的缓存,主要是字体、图片等
    @cacheImmutable {
            file
            path *.ico *.gif *.jpg *.jpeg *.png *.svg *.woff *.bmp
    }
    handle @cacheImmutable {
        # 缓存30天
        header Cache-Control "public, max-age=2592000"
    }
    # 禁止访问.开头的文件,但.well-known除外
    @forbidden {
        not path /.well-known/*
        path /.*
    }
    respond @forbidden 403

    # https://caddy.community/t/example-typecho-blogging-platform/9476
    # Caddy2的php_fastcgi对index.php自动开启了rewrite,因此对于typecho来说,伪静态不需要做任何额外配置
}

什么?你问TLS证书怎么没配置?Caddy在配置好域名之后能够自动申请并更新TLS证书,不需要手动配置,开箱即用。非常好使。

PHP的配置则麻烦一些,根据官方docker镜像的说明,我这里主要覆盖两个文件:

  • /usr/local/etc/php/php.ini:主php.ini文件,基础文件从/usr/local/etc/php/php.ini-production获取,同步旧的php.ini

    • 这里关闭了一些旧php.ini的兼容设置,例如short_open_tag设定为off,cgi.fix_pathinfo设定为1
  • /usr/local/etc/php-fpm.d/www.conf:php fpm的配置

此外,由于typecho和主题需要一些额外的扩展,因此还需要自己构建要给php容器来启用这些扩展:

FROM php:8.3-fpm

# 安装扩展依赖的系统库
#   gd
#   zip 
#   intl
RUN apt-get update && apt-get install -y --no-install-recommends \
  libpng-dev libjpeg-dev libfreetype6-dev libwebp-dev \
  libzip-dev \
  libicu-dev \
  && rm -rf /var/lib/apt/lists/*

# 配置gd,启用freetype、jpeg和webp支持
# 自PHP 7.4.0开始,png是强制支持的,不可关闭
# ref: https://www.php.net/manual/en/image.installation.php
RUN docker-php-ext-configure gd \
  --with-freetype --with-jpeg --with-webp

# 安装扩展
# 其中curl、mbstring和iconv已经自带了,不需要重复安装
RUN docker-php-ext-install -j$(nproc) gd pdo_mysql zip intl

# 启用opcache
RUN docker-php-ext-enable opcache 

由于完整版的php.ini有一千多行,这里就不贴出完整版了,只是列出几点:

  • max_execution_time:设置为300秒,匹配旧nginx设置
  • post_max_size / upload_max_filesize:50M,匹配Nginx设置
  • date.timezoneAsia/Shanghai,这个不多解释了
  • opcache.enable:设置为1启用opcache
  • opcache.jit:设置为function,即CRTO = 1205,这个意思是尝试启用AVX支持、使用全局寄存器分配、脚本加载后立刻编译所有方法、优化整个脚本。不明白什么意思就去查php的文档
这里我禁用掉了open_basedir,在原本的lnmp脚本中,这个配置旨在限制php能够读取的文件。如果网站被黑,攻击者通过php脚本读取允许列表外的文件,php会拒绝。但注意我的用词:“旨在”。根据 https://www.bencteux.fr/posts/open_basedir/ 这篇讨论,这个选项并不能真正保证安全,能够轻而易举地绕过这个限制。而production的php.ini同样也没有开启这个选项。因此我也选择不开启。

php-fpm的配置就简单许多了,同样从容器里复制出来一份,修改的内容是:

  • listen = 0.0.0.0:9000,默认只听127.0.0.1,因为在容器里,所以可以听0.0.0.0
  • pm:基本上匹配旧配置,一共20个线程,每个线程执行1024个请求就杀掉,防止内存泄漏。

部署的之后用这个compose就可以了:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    cap_add:
      # for HTTP/3
      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - "/root/server-configs/blog-server/webserver/caddy/etc:/etc/caddy"
      - "/root/www-content:/srv"
      - "caddy-data:/data"
      - "caddy-config:/config"
    networks:
      - shared
    deploy:
      resources:
        limits:
          memory: 1G


  php-fpm:
    build:
      context: .
      dockerfile: php-fpm.Dockerfile
    restart: unless-stopped
    volumes:
      - "/root/www-content:/srv"
      - "/root/extra-data:/data"
      - "/root/server-configs/blog-server/webserver/php/php.ini:/usr/local/etc/php/php.ini"
      - "/root/server-configs/blog-server/webserver/php/fpm-www.conf:/usr/local/etc/php-fpm.d/www.conf"
      - "php-session:/session"
    networks:
      - shared
    deploy:
      resources:
        limits:
          memory: 1G

volumes:
  caddy-data:
  caddy-config:
  php-session:

networks:
  shared:
    name: app-shared-network
    external: true

容器化部署其他服务

之前为了方便我做评论推送,我写了个插件,在评论时自动给一个webhook发送请求,然后我编写一个配套的程序来接受请求,处理发邮件之类的事情。这样就能避开我不擅长的php,从而在jvm的世界发邮件、给pushover推送,或者是直接在telegram bot上通知我。不过这也意味着我需要容器化部署另一个服务,并且让这些服务能够互通。

为此,我考虑了一下,决定将存储部署文件的repo这样设计:

repo-root
├── blog-server
│   ├── kopia
│   │   └── compose.yaml
│   ├── README.md
│   ├── typecho-mate
│   │   ├── compose.yaml
│   │   └── config.properties
│   └── webserver
│       ├── caddy
│       │   └── etc
│       │       └── Caddyfile
│       ├── compose.yaml
│       ├── php
│       │   ├── fpm-www.conf
│       │   └── php.ini
│       └── php-fpm.Dockerfile
└── README.md

根目录下一个readme简单介绍一下项目。未来还会加入agents.md,因为我打算让AI辅助检查一些设置,虽然不准确,但能够提供一些我的经验与见识之外的事情。并且AI在搜索和总结方面做的不错,我可以让它根据配置的说明(尤其是php这种每个配置都有详细解释的ini文件)去验证我的配置合不合理,并自动发现一些问题。

根目录下的每一个文件夹对应一个服务器,目前只有blog-server,我现在还有一个hetzner的服务器在跑别的,还没有迁移到docker compose,后续也会加入进来。服务器文件夹内每一个文件夹就对应一个服务了。目前我有webserver、typecho-mate(评论通知)和kopia做备份。

为了方便,我决定让服务器的文件组织标准化:

  • 这个repo会克隆到/root/server-configs文件夹
  • 所有需要备份的容器数据就放在/root/container-data/app-name/volume-name里面,未来切换服务器的话,这些是可以通过备份恢复的
  • 对于session一类需要持久化,但是可以重建的数据,统一采用docker volume
  • 对于需要访问但不需要备份的数据,则映射到符合Linux HFS的路径。例如kopia的日志映射到/var/log/kopia文件夹

这样一来每台机器只要克隆server configs,配置好对应的文件夹,就可以直接部署了。

测试升级路线

为了最小化停机时间,我决定先测试一下我这个升级路线行不行。当然,也是因为上次盲目升级,给服务器弄炸了,属于是害怕了。

Linode的托管服务器有个十分优秀的功能,就是允许你根据过去14天内,精确到秒的时间点创建数据库的fork,因此我直接从现在的数据库fork出了一个测试用的库,放在了德国。在Hetzner上创建了一个很便宜的服务器(毕竟主服务器只有3核2GB的配置)进行测试。

首先通过rsync将旧服务器的文件同步过去,修改typecho的配置,将数据库指向测试库,确保dns完全传播,在浏览器里打开测试网站一看:

NOTICE: PHP message: PHP Warning:  Unknown: open_basedir restriction in effect. File(/srv/skyblond.info/index.php) is not within the allowed path(s): (/home/wwwroot/skyblond.info:/tmp/:/proc/) in Unknown on line 0

我的脑袋里浮现出一个问号。这是为什么呢?我寻思我的新配置也没有写open_basedir,更没用lnmp脚本,哪来的wwwroot呢?仔细查阅后发现网站的根目录下有一个.user.ini,里面配置了要给open_basedir。罪魁祸首就是它,删掉即可。由于网站更新时需要写入权限,为了方便,我直接chmod 777 -R了。

首先升级主题,升级后发现控制台渲染不正常,抓请求说资源不存在,我检查了文件,确实在。仔细一看chrome抓的请求,原来是请求到博客去了,因为typecho配置的域名还是主服务器,不是测试服务器。更新了就好了。此外,由于开启了opcache,并且扫描间隔设置为了120秒,因此主题更新的php文件并不会很快利用。要么等120秒,要么直接重启php容器,我选择后者。

接下来是更新typecho 1.3.0。首先需要暂时关闭webserver stack,然后删除下列文件:

/admin/
/var/
/install/
/index.php
/install.php

再从typecho的发布版里面将上述文件上传进去。这次1.3.0版本还包含了一个新的默认主题,在usr/theme中,但我不用默认主题,所以就没上传。

完成后启动webserver stack,访问控制台,需要更新数据库结构,更新后简单测试了一番,一切正常。

最后就是测试新版本的php了。因为我需要自己构建php容器启用扩展,因此只需要修改dockerfile里面的基础镜像,然后重新构建容器再启动docker compose就好了。测试了一下也没有问题,那么就可以正式迁移了。

正式迁移

为了最小化停机时间,我决定在hetzner上创建一个临时服务器来替代主服务器。首先要提前一小时将DNS记录的TTL设置为一分钟,这样确保DNS能够快速更新。可以使用这个工具检测dns传播: https://www.whatsmydns.net

Caddy的自动申请TLS证书只在启动时进行,如果申请失败,后续请求不会触发重新申请,一开始我还不理解,后来我就亲身实践了。

在等待一个小时后,我更新DNS记录,将域名指向hetzner的临时服务器,检查DNS传播状况,等绝大部分dns记录都更新之后就可以重启caddy容器了。重启后caddy应该可以正确获取TLS证书。等到全部DNS都更新之后,就可以关闭主服务器,重装系统了。

在等主服务器重装系统的时候也别闲着,按照刚才的路线进行升级。实际上主服务器重装系统比较慢,我在hetzner的服务器上连php8.3都升级好了,主服务器还没起来呢。

等到主服务器成功启动,配置好github凭证和docker后,就可以再把数据同步过来,更新DNS,等待服务恢复了。不过由于我最后比较心急,不停地在重启caddy容器请求证书,因为失败的次数过多,所以被let's encrypt限流了,因此被TLS证书硬控了10分钟。大约5月15日18点,网站重新正常运行在主服务器上。

可喜可贺,可喜可贺。

后记

至此,博客以一种更先进的形式部署,我的心头一患也算是暂时放下。接下来我准备对主题做一些修改。虽然是买来的,但考虑到作者更新缓慢,并且我不太需要什么新功能,因此我打算从当前的版本分叉出去,删去一些我用不到的功能,并自行维护。不过这些是后话了,希望能够在2027年12月之前完成对PHP 8.4的测试吧。

-全文完-


知识共享许可协议
【歪门邪道】博客升级、容器化、PHP 8.3天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code