两年前,我用一个不到三十行的 Dockerfile,把 WordPress、SQLite 和官方镜像拼到了一起。两年后,这个 Dockerfile 明显变长了。SQLite 的接入方式没有变化,新增代码主要集中在跨架构发布、旧数据卷升级,以及运行状态诊断。

写在前面

2024 年的《WordPress SQLite Docker 镜像封装细节》主要讲了三件事:继续使用 WordPress 官方镜像,把准备好的文件放进 /usr/src/wordpress,再通过 wp-content/db.php 和 MU 插件接管数据库层。

这些做法今天仍然奏效。只是当年常写的“不需要数据库”,现在看容易让人误会。这个镜像仍然使用数据库,只是省去了单独运行的 MySQL 或 MariaDB 服务。外部数据库地址、账号和独立服务进程都省了,备份也可以跟着站点目录一起处理。对于个人博客、小型内容站、演示环境和开发测试,这样会省事不少。

SQLite 是面向单机本地存储的嵌入式数据库,WordPress 插件里也有不少 MySQL 专用写法。镜像依靠 db.php 接管 $wpdb,再完成查询解析和翻译。具体某个插件能否正常使用,最后还是要实际跑一遍。

这篇文章继续拆解 soulteary/docker-sqlite-wordpress。当前项目已经从 WordPress 6.5.2、PHP 8.3 和 SQLite Integration 2.1.9,升级到 WordPress 7.0.2、PHP 8.5 和 SQLite Integration 3.0.0-rc.8。下面从新版 Dockerfile 开始,把新增的实现和几个容易忽略的限制说清楚。

本文基于 2026 年 8 月 8 日更新后的 7.0.2-plugin-v3.0.0-rc.8 标签,对应代码提交 0d476ef。未来项目继续更新时,请以仓库中的实际代码为准。

WordPress 7.0.3 版本目前刚刚发布,但写下本文时还没有进入 Official Docker Images 发布索引。官方基础镜像就绪后,项目还会更新构建并发布新版本。如果你等不及,可以使用当前的版本,在启动后的 WordPress 后台直接点击升级。

这个方案适合谁?

选不选 WordPress SQLite,主要还是看站点的读写方式和运维要求。单机、读多写少时,它能省下不少工作;多实例和高并发写入场景,MySQL、MariaDB 或 PostgreSQL 更稳妥。

适合优先尝试的场景:

  • 个人博客、作品集、小型内容站和文档站。
  • 本地开发、自动化测试和临时演示环境。
  • 单机内部工具、离线环境和边缘设备。
  • 希望减少 MySQL 运维成本,又愿意验证插件兼容性的团队。

需要谨慎评估的场景:

  • 写请求很多,或者存在长事务和高频后台任务。
  • 多个 WordPress 实例需要共同写入一份数据库。
  • 数据库目录位于 NFS 等网络文件系统。
  • 业务依赖复杂 MySQL 特性,或者有严格的数据库 HA、审计和恢复要求。

快速开始

可以从 Docker Hub 或 GHCR 拉取镜像:

# Docker Hub
docker pull soulteary/sqlite-wordpress:7.0.2-plugin-v3.0.0-rc.8

# GitHub Container Registry
docker pull ghcr.io/soulteary/sqlite-wordpress:7.0.2-plugin-v3.0.0-rc.8

这次诊断功能更新沿用了现有长标签。机器上如果缓存过同名镜像,请主动执行一次 docker pull

仓库里的 7.0.27.0.2-plugin-v3.0.0-rc.8 是两种不同的镜像。7.0.2 对应的 Dockerfile使用 SQLite Integration 2.2.17,尚未包含 3.0 RC、Rust 原生扩展、根目录 UI Loader 和自愈入口。复现本文内容时,请使用 7.0.2-plugin-v3.0.0-rc.8

接着创建一个本地目录,并启动容器:

mkdir -p wordpress

docker run -d \
  --name sqlite-wordpress \
  --restart unless-stopped \
  -p 127.0.0.1:8080:80 \
  -v "$(pwd)/wordpress:/var/www/html" \
  ghcr.io/soulteary/sqlite-wordpress:7.0.2-plugin-v3.0.0-rc.8

浏览器访问 http://localhost:8080,按照 WordPress 的引导完成初始化即可。安装过程中不需要填写 MySQL 服务器地址、用户名和密码。

如果你更习惯 Docker Compose,可以使用下面的配置:

services:
  wordpress:
    image: ghcr.io/soulteary/sqlite-wordpress:7.0.2-plugin-v3.0.0-rc.8
    container_name: sqlite-wordpress
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./wordpress:/var/www/html
docker compose up -d

完成安装后,登录后台,打开「工具 → SQLite Diagnostics」,可以核对原生扩展、SQLite 引擎、连接参数、数据库文件占用、PHP 环境、drop-in 和集成插件版本。「工具 → SQLite id key fix」则用于检查后文提到的 id 结果键兼容修复。

两年间发生了什么

2024 年的基准是文章发布当天的 4556e85,两版之间的完整差异可以在 GitHub Compare 中查看。

项目 2024 年 4 月 2026 年 8 月
WordPress 6.5.2 7.0.2
PHP 8.3 8.5
SQLite Integration 2.1.9 3.0.0-rc.8
上游结构 单插件仓库布局 Monorepo、多 Package
镜像构建 单阶段下载压缩包 Rust Builder + 最终运行镜像
查询解析 纯 PHP amd64/arm64 原生扩展,其他架构回退 PHP
数据卷初始化 依赖官方入口首次复制 每次启动对账 SQLite 关键文件
运行诊断 主要依赖容器命令 后台提供只读 SQLite Diagnostics 页面
兼容处理 上游插件 为部分单表 SELECT 补充显式列别名
多架构发布 单 Runner 通过 QEMU 构建 原生 amd64/arm64 + 32 位 ARM QEMU 并行构建

两年前的实现首要目标是“保持足够简单”。这次代码主要增长在几个地方:上游换了目录结构,Rust 扩展需要重新安排多架构构建,旧数据卷也暴露了升级问题。下面逐个看。

核心做法没有变化:用 db.php 接管数据库层

这个项目始终基于 Docker Official Image for WordPress 构建。最终运行环境里的 Apache、PHP、WordPress 核心和官方入口逻辑,仍然来自上游镜像。

SQLite 的接入点位于 WordPress 加载数据库对象的位置:wp-content/db.php。WordPress 会优先加载这个特殊的数据库 drop-in。SQLite Integration 在这里创建一个兼容 wpdb 的实现,将 WordPress 和插件发来的 MySQL 查询交给解析器,再翻译成 SQLite 能执行的语句。

flowchart TD
    A["WordPress 与插件发出 MySQL 查询"] --> B["db.php 接管 wpdb"]
    B --> C["MySQL Lexer 与 Parser"]
    C --> D["SQL 翻译与 MySQL 行为模拟"]
    D --> E["PDO SQLite 与数据库文件"]

查询先经过 Lexer 和 Parser,再由驱动翻译成 SQLite 能执行的语句。SQLite 并不认识 SHOW TABLESALTER TABLE 的许多 MySQL 写法,也不会天然提供完全相同的类型、排序规则和返回值语义。

SQLite Database Integration 3.0 已经整理成 Monorepo,分别维护 MySQL Lexer、Parser、SQLite Driver、MySQL Proxy、WordPress Plugin 和测试套件。它正常要求 SQLite 3.37.0 或更高版本;最低到 3.27.0 的 Legacy Mode 需要显式启用 WP_SQLITE_UNSAFE_ENABLE_UNSUPPORTED_VERSIONS,不适合作为日常部署路径。

目前其他插件和 SQLite 插件兼容性仍然需要靠实测。插件上游还在处理字符集与排序规则部分 MySQL 专有语法,以及 GIS、Dump 等能力。有一些插件会绕开 $wpdb直接建立 mysqli 连接,这些查询根本不会经过 db.php。部署前最好把实际使用的插件、主题、后台任务和导入导出流程都跑一遍。

为什么还需要一个根目录 Loader

两年前的文章里,我们说把 SQLite Integration 放进 wp-content/mu-plugins,WordPress 就会自动启用它。这个说法省略了一个重要细节:Must-Use Plugins 只会自动加载 mu-plugins 根目录中的 PHP 文件,不会递归寻找子目录里的入口文件。

SQLite 数据库驱动不受这个问题影响,因为它通过 wp-content/db.php 直接加载。但上游插件自己的 sqlite-database-integration/load.php 还负责设置页面、健康检查和管理界面;如果没有根目录入口,这部分 UI 就不会自动挂载。

当前镜像因此增加了一个只有几行代码的 sqlite-database-integration-loader.php

<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }
$sqlite_ui = __DIR__ . '/sqlite-database-integration/load.php';
if ( is_readable( $sqlite_ui ) ) {
    require_once $sqlite_ui;
}

加载过程由此分成两条:wp-content/db.php 在 WordPress 建立数据库对象时加载 SQLite Driver;根目录 MU Loader 在插件加载阶段启用设置页和健康检查。

上游升级 Monorepo,镜像组装升级

2024 年的 Dockerfile 可以直接下载一个 GitHub Tag 压缩包,把内容复制进 mu-plugins

RUN curl -L -o sqlite-database-integration.tar.gz \
  "https://github.com/WordPress/sqlite-database-integration/archive/refs/tags/v${SQLITE_DATABASE_INTEGRATION_VERSION}.tar.gz"

3.0 版本的项目布局已经不同。WordPress 插件位于 packages/plugin-sqlite-database-integration,MySQL-on-SQLite 驱动位于 packages/mysql-on-sqlite/src;插件目录中的 wp-includes/database 在开发仓库里还是一个链接,正式发布时才会被物化为真实文件。

当前镜像因此改成浅克隆指定 Tag,并手动完成一次最小化的发布组装:

RUN git clone --depth 1 --branch "v${SQLITE_DATABASE_INTEGRATION_VERSION}" \
      https://github.com/WordPress/sqlite-database-integration.git /src && \
    cp -R /src/packages/plugin-sqlite-database-integration /plugin && \
    rm /plugin/wp-includes/database && \
    cp -R /src/packages/mysql-on-sqlite/src /plugin/wp-includes/database && \
    rm -rf /plugin/composer.json /plugin/vendor /plugin/node_modules

这段组装有三个目的:固定上游版本、把链接落成普通文件,并从最终插件中去掉 Composer、Node.js 依赖和开发工具。上游的发布结构变了,镜像也要跟着调整。

用 Rust 加速最热的解析路径

SQLite Integration 3.0 可以使用纯 PHP 完成词法分析和语法解析。这样最容易部署,但每一条进入数据库层的 MySQL 查询,都要在 PHP 中完成一遍 Lexer 和 Parser 工作。

上游新增的 wp_mysql_parser 是一个可选 PHP 原生扩展。它用 Rust 实现这条热点路径,并在 PHP 启动时注册原生 Lexer 和 Parser 类。驱动会自动检测这些类:存在就走原生实现,不存在就回退纯 PHP,无需修改 WordPress 业务代码。

构建这个扩展需要 Rust Toolchain、PHP 开发头文件、php-config、Clang 和 libclang。当前项目采用两阶段构建:

  1. ext-builder 阶段安装构建依赖、组装插件并执行 cargo build --release
  2. 最终阶段仍从 wordpress:7.0.2-php8.5-apache 开始,只复制插件和 libwp_mysql_parser.so

编译工具只留在 Builder 阶段,运行层依旧来自官方 WordPress 镜像。

根据上游在 Apple Silicon、PHP 8.4.5 CLI 上对 69,577 条 MySQL 查询语料进行的微基准测试,结果如下:

测试 纯 PHP 原生扩展 加速比
MySQL Lexer 约 71,553 QPS 约 343,124 QPS 4.80×
MySQL Parser 约 7,015 QPS 约 108,354 QPS 15.45×

15.45× 只代表 Parser 微基准中的处理速度,不能直接换算成 WordPress 页面性能。一次页面请求还包含 PHP 业务逻辑、SQLite 执行、磁盘 I/O、插件钩子、模板渲染、对象缓存和网络传输,最终收益仍要靠站点实测。

五种架构,两条解析路径

项目发布五种 Linux 架构,其中 amd64、arm64 编译 Rust 扩展,三种 32 位 ARM 使用纯 PHP:

架构 构建方式 wp_mysql_parser 查询解析路径
linux/amd64 原生 x86 Runner 启用 Rust 原生路径
linux/arm64 原生 ARM Runner 启用 Rust 原生路径
linux/arm/v7 ARM Runner + QEMU 跳过 纯 PHP 回退
linux/arm/v6 ARM Runner + QEMU 跳过 纯 PHP 回退
linux/arm/v5 ARM Runner + QEMU 跳过 纯 PHP 回退

32 位 ARM 跳过扩展,是因为 Rustup 通过 QEMU 运行时容易遇到动态链接器缺失或不匹配。纯 PHP 解析器保留了完整功能,只是解析速度慢一些。

Dockerfile 为这些架构生成空的 .so 占位文件,保证后续跨阶段 COPY 的路径始终存在;最终阶段再用 -s 判断文件是否有内容。只有真正构建出的共享库才会写入 PHP 配置:

RUN if [ -s /usr/local/lib/php/extensions/wp_mysql_parser.so ]; then \
      echo "extension=/usr/local/lib/php/extensions/wp_mysql_parser.so" \
        > /usr/local/etc/php/conf.d/wp_mysql_parser.ini ; \
    else \
      rm -f /usr/local/lib/php/extensions/wp_mysql_parser.so ; \
    fi

发布流水线也随之调整。当前 GitHub Actions 使用原生 x86 Runner 构建 amd64、原生 ARM Runner 构建 arm64,三种 32 位 ARM 才启用 QEMU。各架构分别推送到 Docker Hub 和 GHCR,最后用目标 Registry 返回的 Digest 合并 Multi-Arch Manifest。

两个 Registry 的 Digest 分开保存,因为某个 Digest 已经存在于 Docker Hub,并不能证明 GHCR 中也有同一个对象。当前同一次 Tag Push 还会同时触发版本标签和 latest 两条发布流程,五个平台又分别为 Docker Hub 与 GHCR 构建,因此一轮发布存在不少重复工作。

后台可以直接看 SQLite 运行状态

项目增加了 sqlite-diagnostics.php。Dockerfile 将它复制到 wp-content/mu-plugins 根目录,WordPress 会自动加载,并在后台「工具」菜单中注册一个 SQLite Diagnostics 页面。

管理员打开页面后,可以查看六组信息:

分组 显示内容 主要用途
Native Extension 扩展、原生 Lexer、原生 Parser 和当前解析路径 检查原生扩展及 PHP 回退状态
SQLite sqlite_version()sqlite_source_id() 核对实际链接的 SQLite 引擎
PRAGMA (live connection) 日志模式、同步级别、锁等待、页、缓存、外键和检查点参数 查看当前连接的运行配置
Storage Main、WAL、SHM 的路径、大小和合计 观察相关文件的即时磁盘占用
Environment PHP、CPU、pdo_sqlite、drop-in 和数据库路径 判断镜像与运行环境是否符合预期
Integration Plugin SQLite Integration 版本 核对卷内插件版本

原生路径判断会分别检查 WP_MySQL_Native_LexerWP_MySQL_Native_Parser,可以区分完整原生路径、部分原生路径和纯 PHP 回退。PRAGMA 组读取的是 WordPress 当前 SQLite 连接,重点可以看 journal_modesynchronousbusy_timeoutwal_autocheckpoint

页面还会分别统计主数据库、WAL 和 SHM 文件。它们会随着写入和检查点变化,因此页面展示的是打开时的状态快照。文件合计反映磁盘占用,不等于逻辑数据量,也不能作为备份完整性的依据。

这个页面适合排查版本错配、架构回退、扩展漏装、WAL 增长和连接参数。它不会执行完整性检查,也不记录页面耗时、慢查询和 SQL 翻译结果;遇到具体的业务查询问题,仍要结合日志,必要时另行安装 Query Monitor。

idID 的大小写问题继续往下查

MySQL 与 SQLite 的差异有时会藏在结果集的键名里。WordPress 的文章表将主键声明为大写的 ID,某些插件却会写出下面这样的查询:

SELECT P.id FROM wp_posts AS P;

MySQL 通常按照查询中写出的形式返回键名 id。SQLite 返回未显式取别名的列时,会使用真实声明名 ID。我用一个只包含 ID 字段的最小 SQLite 表复核,SELECT P.id 的结果元数据确实仍是 ID。如果插件接下来读取 $item['id']$row->id,在 MySQL 中正常,在 SQLite 中却可能得到空值。

仓库为此增加了 sqlite-select-id-key-fix.php。早期版本依赖 SQLite Integration 2.x 的 pre_query_sqlite_db Hook,3.0.0-rc.8 已经没有这个调用点。随后提交的 edce047 保留旧版兼容,同时接入 WordPress 的 query Filter:

add_filter( 'query', 'sqlite_select_id_key_fix_rewrite_query', 10, 1 );

rc.8 会在查询进入 Driver 前经过这个 Filter,前面的查询会变成:

SELECT P.id AS "id" FROM wp_posts AS P;

显式别名把结果键固定为查询中写下的 id。当前改写主要覆盖结构简单的单表 SELECTDISTINCTJOINUNION、子查询、函数表达式等复杂查询仍会原样交给上游。插件作者如果能直接修改 SQL,显式写出 SELECT P.ID AS id 会更稳妥。

最新的 0d476ef 又在后台「工具」菜单中增加了 SQLite id key fix 页面。它会显示 Filtered SQL,并用同一条 SELECT P.id 分别检查 ARRAY_AOBJECT 两种结果。站点中至少有一篇文章时,可以同时看到查询已改写为 P.id AS "id",以及两种结果中是否出现了小写 id。这个探针只读取文章表,不会写入数据库。

旧数据卷为什么会报数据库连接错误

2024 年文章专门解释过,为什么构建时要修改 /usr/src/wordpress,不能直接修改 /var/www/html

WordPress 官方镜像把 /usr/src/wordpress 当作镜像内的程序源,把 /var/www/html 当作实际运行目录。官方 docker-entrypoint.sh 只在运行目录尚未安装 WordPress 时,才会将程序复制进去。

新卷启动没有问题,麻烦出在升级旧卷时:

  1. 用户已经有一个初始化完成的 ./wordpress 目录。
  2. 用户更换了新的 SQLite WordPress 镜像。
  3. 官方入口看到 WordPress 已经存在,不再执行完整复制。
  4. 新镜像中的 wp-content/db.php 或 MU 插件没有进入旧卷。
  5. WordPress 找不到 SQLite drop-in,回到默认的 MySQL 路径,最后显示“Error establishing a database connection”。

数据库文件通常还在,缺的是负责加载 SQLite 的 db.php 和 MU 插件。

当前版本新增的 docker-entrypoint-sqlite.sh 会在每次启动时完成这些工作:

  1. 调用官方的 docker-ensure-installed.sh true,完成标准初始化和配置生成。
  2. 对比镜像源目录与运行目录中的 wp-content/db.php,缺失或变化时覆盖。
  3. 将镜像提供的 MU 插件同步到运行目录。
  4. 确保默认数据库目录和 .ht.sqlite 存在。
  5. 以 root 身份启动时,尽力修正 www-data 所有权和必要权限。
  6. 最后用 exec 启动原始的 Apache 命令。

现在流程分成两段:构建时准备 /usr/src/wordpress;容器启动后,再把 SQLite 关键文件同步到挂载卷。

脚本只维护 SQLite drop-in、配套 MU 插件和默认数据库目录,不会删除用户安装的主题、普通插件和上传文件,也不负责升级卷内的 WordPress 核心。镜像提供的 MU 插件应视为镜像管理文件;自己的 MU 插件最好使用独立文件名,避免下一次启动时被覆盖。

挂载卷的权限怎么处理

SQLite 没有独立的数据库服务账号,但运行 WordPress 的 PHP 进程必须能够创建和写入数据库文件、WAL 文件以及共享内存文件。

当前 Dockerfile 会预先准备这些常用目录:

wp-content/database
wp-content/plugins
wp-content/themes
wp-content/uploads
wp-content/upgrade

随后将 wp-content 交给 www-data:www-data,目录设置为 755,默认数据库文件 .ht.sqlite 设置为 640。启动脚本还会针对挂载卷再次进行尽力而为的修正。

这里和上游 rc.8 有一个细微差别:上游只在数据库目录不存在时以 0700 创建,并把新建的 .htaccessindex.php 设为 0600;本镜像提前把目录建成 0755,主要为了适配 www-data 和 bind mount。

使用 bind mount 时,容器中的 UID/GID 与宿主机文件所有权仍然可能冲突。如果日志中出现 readonly databaseunable to open database file,或者无法创建 -wal-shm 文件,先检查宿主目录权限,别急着重装 WordPress:

docker exec sqlite-wordpress id www-data
docker exec sqlite-wordpress ls -ld /var/www/html/wp-content/database
docker exec sqlite-wordpress ls -la /var/www/html/wp-content/database

启动脚本只有在容器以 root 启动时才能执行 chown。如果使用非 root 用户、只读文件系统或更严格的容器安全策略,需要提前准备一个可写的数据目录。

数据库文件放在 Web 目录里安全吗

默认数据库文件仍然是:

/var/www/html/wp-content/database/.ht.sqlite

当前镜像固定使用 Apache。Apache 的默认配置会拒绝直接访问以 .ht 开头的文件,上游插件还会在数据库目录中创建拒绝访问的 .htaccess 和用于阻止目录浏览的 index.php。在这个镜像的默认组合下,直接请求 .ht.sqlite 会被 Web 服务器拒绝。

.ht 文件名需要配合 Web 服务器规则才能发挥作用。如果换成 Nginx、Caddy 或自建 PHP-FPM 镜像,要重新配置数据库目录的拒绝规则。更严格的环境可以使用 DB_DIR 把数据库移到 Web 根目录之外:

define( 'DB_DIR', '/var/lib/wordpress-sqlite' );
define( 'DB_FILE', 'site.sqlite' );

使用官方镜像支持的 WORDPRESS_CONFIG_EXTRA 时,Compose 可以这样配置:

services:
  wordpress:
    image: ghcr.io/soulteary/sqlite-wordpress:7.0.2-plugin-v3.0.0-rc.8
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_CONFIG_EXTRA: |
        define( 'DB_DIR', '/var/lib/wordpress-sqlite' );
        define( 'DB_FILE', 'site.sqlite' );        
    volumes:
      - ./wordpress:/var/www/html
      - ./database:/var/lib/wordpress-sqlite

此时要确保 ./database 对容器内的 www-data 可写。自愈脚本只自动处理默认的 wp-content/database,不会修改自定义目录的所有权。

为什么 Dockerfile 里多了几条 test

上游进入 3.0 RC 后,文件布局变化更频繁。如果 Dockerfile 只是一连串 cp,目录变化有时不会让构建立刻报错,缺失文件可能要等到容器启动时才暴露。

当前构建会主动验证:

  • SQLite Driver 的核心文件存在。
  • wp-includes/database/load.php 已经从链接变成真实文件。
  • Query Monitor 集成需要的 boot.php 存在。
  • 生成的 db.php 包含 SQLITE_DB_DROPIN_VERSION
  • {SQLITE_IMPLEMENTATION_FOLDER_PATH} 占位符已经被替换。

我很喜欢这种朴素检查:把最容易断的几个路径写死,出问题时很快就能看到。不过,文件和字符串断言无法验证运行时 Hook、PHP ABI 和查询结果,构建通过后仍要启动容器跑一次真实查询。

WAL 模式下怎样备份 SQLite

主要数据在 .sqlite 文件里,WAL 模式还会使用 -wal-shm。站点运行时只复制主文件,备份可能不完整。

离线备份最省心:先停止写入,再整体打包持久化目录。

docker compose stop wordpress

tar --create --gzip \
  --file "wordpress-backup-$(date +%Y%m%d-%H%M%S).tar.gz" \
  wordpress

docker compose start wordpress

如果数据库放在独立目录,也要把那个目录纳入同一份备份。

需要不停机备份时,不要直接对活跃文件执行 cp。SQLite 官方提供了 Online Backup API,也可以使用 VACUUM INTO 生成一致的数据库副本。具体选哪一种,要结合数据库大小、可接受的锁等待和恢复流程决定。

升级前应把镜像引用和数据库快照配对保存。如果新版 Driver 修改了内部元数据,单独回退镜像可能无法恢复旧状态。

恢复后别只看首页,至少运行下面几项检查:

PRAGMA quick_check;
PRAGMA integrity_check;
PRAGMA foreign_key_check;

quick_check 适合先做快速筛查,完整的 integrity_check 成功时返回 ok;它不检查外键,因此还要单独运行 foreign_key_check。当前镜像没有承诺内置 sqlite3 CLI,可以使用 PHP PDO 或独立的 SQLite 工具容器完成检查。

WAL 能解决多少并发问题

在支持 WAL 的环境里,3.0.0-rc.8 的上游 Driver 默认配置是:

  • journal_mode=WAL
  • synchronous=NORMAL
  • 可写锁等待时间 10 秒

WAL 允许读取与写入并行,通常比传统回滚日志更适合 Web 请求。不过,SQLite 官方文档写得很清楚:同一时间仍然只有一个写事务;WAL 的共享内存机制还要求所有访问者位于同一台机器,不适合把数据库文件放在网络文件系统上让多台主机共同写入。

rc.8 在写事务上使用 BEGIN IMMEDIATE,可以减少读事务升级为写事务时遇到 SQLITE_BUSY_SNAPSHOT 的概率。另一个写事务已经持锁时,新请求最多等待默认的 10 秒,之后仍可能收到 SQLITE_BUSY

SQLite 默认在 WAL 累积到约 1000 个数据库页时自动执行检查点;持续时间很长的读事务会阻止检查点向前推进,让 -wal 文件不断增长。因此,监控时除了主数据库文件大小,还要看锁错误、写入延迟和 WAL 文件增长。

synchronous=NORMAL 是性能与持久性的选择。在 WAL 模式中,事务仍保持原子性、一致性和隔离性,但机器突然断电或系统硬重启时,最近已经提交的事务可能回滚。若业务要求在电源故障后保住最近提交的数据,应进一步评估 FULL 的性能成本。

SQLite 官方将低到中等流量网站列为合适场景。请求量只是一个参考,读写比例和事务长度更关键。一个缓存命中率高的站点可能请求很多却很稳定;持续长事务和并发写入的站点,请求不多也会较早遇到锁竞争。

还要检查实际的 SQLite 引擎

项目固定了 WordPress、PHP 和 SQLite Integration 的版本,却没有自行编译或固定 SQLite 引擎。官方 php:8.5-apache 基于 Debian Trixie,并通过系统库提供 PDO SQLite。Debian Trixie 在本文写作时提供的是 3.46.1-7+deb13u1,最终发布镜像链接的具体版本和发行版回补情况,仍应以诊断页与 Debian 安全公告为准。

这在 2026 年尤其值得检查。SQLite 官方披露了一个罕见但可能导致损坏的 WAL-reset Bug:上游判断 3.7.0 到 3.51.2 可能受影响,3.51.3 及以后版本已经修复,3.44.6 和 3.50.7 也提供了回补版本。触发条件是同一个 WAL 数据库至少存在两个连接,并在非常窄的时序中并发写入或执行检查点。

部署验收不能停在“SQLite 高于 3.37”这个功能兼容下限,还要核对诊断页中的实际引擎版本、Source ID 和发行版补丁状态。单独更新 WordPress 插件解决不了引擎层的问题。

最后

两年之后,这个镜像仍然沿用最初的接入方式:以 WordPress 官方镜像为基础,通过 db.php 接管数据库层。新增代码解决的是项目走向长期维护后出现的实际问题,包括上游 Monorepo 组装、Rust 解析扩展、多架构发布、旧卷修复和运行诊断。

对于单机、小规模、读多写少的 WordPress 站点,它依然是一种省事的部署方式。真正上线前,把插件兼容、目录权限、备份恢复和 SQLite 引擎版本逐项跑通,再开始承载正式内容会更稳妥。

这篇文章就先写到这里吧。

–EOF