自播种 Docker 镜像:代码/数据分离与首启动引导
YouTeacher 游戏商店是一套 WordPress/WooCommerce 栈,被打包成一个能自我播种的镜像。整个设计的核心是一道干净的分界:代码住在镜像里,数据住在卷(volume)里。 要发布代码改动就重建镜像;数据库和上传的媒体从不被触碰。同一个镜像可服务本地、dev、prod——环境之间的差异全部由环境变量驱动。
镜像烤进了什么(代码)
Dockerfile 基于 wordpress:php8.3-apache,加装了 wp-cli、unzip 和一个 MySQL 客户端。在 WP core 之上,它烤进以下内容,全部锁定到与线上商店一致的精确版本:
- 从 wordpress.org 拉取的免费 WooCommerce 插件(WooCommerce 本体,以及 Stripe、PayPal、Mailgun、Mailchimp 集成),
- 一个锁版的免费主题(Astra),
- 一个不在 wordpress.org 上、有授权的付费插件——因为构建时无法在线获取,所以直接随镜像捆绑,
- 项目自己的代码:一个自定义的 activator 插件和一个 Astra 子主题。
它还烤进了 WP 的重写规则 .htaccess,这样一旦 WP core 落到 web 根目录,漂亮链接(/product/…、/activate/…)立即生效,无需手动步骤;此外还有一份小小的 PHP ini,为后台的媒体上传与导入调高了上传/请求体积、内存和执行时间上限。
有一个微妙之处:最终的 WORKDIR 被重置回 /var/www/html。WordPress 官方 entrypoint 通过检查当前目录里是否有 index.php 来决定是否把 WP core 拷进 web 根目录;如果把工作目录停在 wp-content(那里有它自己的 index.php),就会让它错误地跳过拷贝。
种子(作为首启动产物的数据)
镜像还携带一份种子,好让新部署不至于空空如也:一份商店内容的 SQL 转储(商品、已激活的插件、测试模式的支付配置),以及这些商品引用的媒体。两者都是拷进镜像内的 /usr/local/share/ 而非挂载进来——因为部署平台(Coolify)不会把仓库文件 bind-mount 进服务容器,所以数据库容器自己的初始化步骤够不到它们,于是由 WordPress 容器在首启动时导入种子。
Entrypoint:幂等、由环境变量驱动的引导
store-entrypoint.sh 包裹了官方 WordPress entrypoint,跑几个每次启动都能安全重复的步骤:
- 从镜像刷新代码。 基础镜像把
/var/www/html声明为卷,这本会让陈旧代码遮蔽一次更新。所以脚本在每次启动时,都从镜像的/usr/src/wordpress把plugins和themes重新拷回运行目录(用临时目录换入的方式),让镜像成为代码的唯一真相来源——同时不动数据目录(uploads、数据库)。 - 等待,然后为空库播种。 一个后台任务先等 WP core 文件出现、再等数据库真正接受连接(平台是异步拉起数据库的)。若 WordPress 尚未安装,就导入烤好的 SQL 种子。
- 仅一次地恢复媒体。 因为 uploads 卷会遮蔽镜像里烤进的媒体,脚本在首启动时把种子媒体拷进卷、修正属主、按主题所需重新生成缩略图尺寸,并落一个标记文件,使这一步保持一次性、幂等。
- 改写种子的站点 URL。 种子是在某个 URL 下捕获的;若部署配置的站点 URL 环境变量与已存的不同,脚本会对所有表做一次 search-replace(跳过
guid列),更新siteurl/home选项,再刷新缓存。同一份种子就是这样适配本地、dev 或 prod 域名的。 - 刷新重写规则,让插件路由(如
/activate/、/download/)得以注册,然后交棒给docker-entrypoint.sh apache2-foreground。
Compose 拓扑
docker-compose.yml 跑两个服务:由本 Dockerfile 构建的 WordPress 容器,以及 MariaDB。恰好只有两个命名卷——数据库和 wp-content/uploads——而 /var/www/html 被刻意不设为卷,这正是重建能发布新代码却不扰动数据的原因。
配置完全来自部署环境变量:数据库连接、按部署而异的站点 URL(dev 指向 dev 域名,prod 指向 prod 域名)、商店签发下载 URL 所用的私有 R2 桶凭据、打包流水线回调所用的一个共享密钥,以及——很关键的——被钉住的 WordPress auth salts。因为 /var/www/html 不是卷,WordPress 本会在每次重建时重新生成 salts,把所有用户登出;把 salts 钉成环境变量,登录会话才能跨部署存活。两个服务都带健康检查,好让平台等待就绪。
为什么是这个形状
这个设计回应的是一个具体张力:WordPress 把代码和数据混在同一棵目录树里,而容器平台又倾向于把整棵树当成卷持久化。把代码烤进镜像、只把数据库与 uploads 留作卷、再让 entrypoint 在每次启动时把两者对齐——于是代码改动只是一次镜像重建,而一个全新环境能从单一种子里把自己引导成一个可用的商店。