docker-image-and-first-boot-seed

自播种 Docker 镜像:代码/数据分离与首启动引导

YouTeacher 游戏商店是一套 WordPress/WooCommerce 栈,被打包成一个能自我播种的镜像。整个设计的核心是一道干净的分界:代码住在镜像里,数据住在卷(volume)里。 要发布代码改动就重建镜像;数据库和上传的媒体从不被触碰。同一个镜像可服务本地、dev、prod——环境之间的差异全部由环境变量驱动。

镜像烤进了什么(代码)

Dockerfile 基于 wordpress:php8.3-apache,加装了 wp-cliunzip 和一个 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,跑几个每次启动都能安全重复的步骤:

  1. 从镜像刷新代码。 基础镜像把 /var/www/html 声明为卷,这本会让陈旧代码遮蔽一次更新。所以脚本在每次启动时,都从镜像的 /usr/src/wordpresspluginsthemes 重新拷回运行目录(用临时目录换入的方式),让镜像成为代码的唯一真相来源——同时不动数据目录(uploads、数据库)。
  2. 等待,然后为空库播种。 一个后台任务先等 WP core 文件出现、再等数据库真正接受连接(平台是异步拉起数据库的)。若 WordPress 尚未安装,就导入烤好的 SQL 种子。
  3. 仅一次地恢复媒体。 因为 uploads 卷会遮蔽镜像里烤进的媒体,脚本在首启动时把种子媒体拷进卷、修正属主、按主题所需重新生成缩略图尺寸,并落一个标记文件,使这一步保持一次性、幂等。
  4. 改写种子的站点 URL。 种子是在某个 URL 下捕获的;若部署配置的站点 URL 环境变量与已存的不同,脚本会对所有表做一次 search-replace(跳过 guid 列),更新 siteurl/home 选项,再刷新缓存。同一份种子就是这样适配本地、dev 或 prod 域名的。
  5. 刷新重写规则,让插件路由(如 /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 在每次启动时把两者对齐——于是代码改动只是一次镜像重建,而一个全新环境能从单一种子里把自己引导成一个可用的商店。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →