老师反馈上一版没修好(附真机截图)。按 Claude 的静态分析复核后确认,根因是 view() 的时序竞态:
· view() 每次都新建一个 <img>:先写成"屏幕中心的点"(宽高 0 + margin 居中),读自然尺寸后
再加 viewer-transition 写最终尺寸,靠 CSS 过渡补间。
· CSS 过渡的起点 = 浏览器"上一次真正渲染过的样式"。第 1 步默认样式是"满宽贴顶",第 2 步是
"中心点",第 3 步是最终值。哪一帧被渲染过决定动画从哪开始:第 2、3 步之间有帧 ⇒ 从中心
长出来;只渲染到第 1 步 ⇒ 从屏幕上方飞入;一帧都没渲染 ⇒ 无动画直接出现。取决于图片是否
缓存、网络快慢、主线程是否空闲 —— 这就是"没规律"的来源。
· 上一版在 pointerup 里 viewer.image.classList.remove('viewer-transition') 无效:view() 新建
的是另一个 <img>,且 load() 会按 options.transition 给新图重新加类。已删除该段及错误注释
(原注释"从上一张图 margin 滑过来"也不成立)。
修法(Claude 方案 A):包一层 viewer.initImage,在它读自然尺寸之前强制回流一次
(void viewer.image.offsetWidth),把"中心点"提交为已渲染样式 ⇒ 起点恒为屏幕中心。
覆盖手指滑动、缩略图条、键盘、底部按钮等所有切图路径(都在 view() 内)。
本机实测(延迟图片响应 + 限速,模拟"未缓存的大图",即 Claude 给的复现法):
切换瞬间 top 与稳定后 top 的差值:A 组 0px / 0px,C 组重跑 0px / 0px ✓
对照组(剥掉补丁)未取得有效数据(生成对照版本时脚本把页面切坏),"无补丁会飞"留待真机确认。
本机 9 套 jsdom 测试全绿。
434 lines
32 KiB
Docker
434 lines
32 KiB
Docker
# ---------------------------------------------------------------------------
|
||
# Lsky Pro Docker 镜像 —— mole404/lsky-pro-docker
|
||
#
|
||
# 上游应用:https://github.com/lsky-org/lsky-pro (GPL-3.0,官方已停止维护)
|
||
# 上游镜像:https://github.com/HalcyonAzure/lsky-pro-docker (AGPL-3.0,本仓库由它衍生)
|
||
#
|
||
# 本仓库 = 上游应用源码快照(vendored 在 src/)+ Docker 构建脚本 + 少量前端补丁。
|
||
#
|
||
# 与上游镜像的差异(详见 README):
|
||
# 1. 应用源码**已 vendored 进本仓库**(src/ = 上游 commit 38d52c46… 即 2025-11-24 的 master 完整树)。
|
||
# 构建不再联网拉 GitHub archive:上游已明示停止维护,万一仓库被删/归档/改名也还能重建镜像;
|
||
# 之后要改 PHP 代码,直接改 src/ 下的文件、提交即可,不用再维护 overlay 叠加。
|
||
# 2. 前端补丁已就地落在 src/ 里(iOS Safari 长按弹菜单 + 菜单交互修复)。
|
||
# "我们相对上游改了什么"见 patches/ios-longpress.patch,可用 tools/diff-vs-upstream.sh 重新生成/核对。
|
||
# 3. 基础镜像显式写成 Debian bookworm 变体(与 2024-04 那版线上镜像同一 Debian 大版本)
|
||
# 4. install-php-extensions 钉到具体版本,不再用 latest
|
||
# 5. 构建期自证:① 我们从不修改的上游文件按上游 md5 校验(快照没被动过)
|
||
# ② 补丁产物与「有意偏离上游」的 composer.lock 按预期 md5 校验(改了不更新就构建失败)
|
||
# 6. 运行时加固(F23,2026-09-30):PHP 关掉 expose_php / display_errors
|
||
# (/usr/local/etc/php/conf.d/zz-lsky-hardening.ini)、Apache 关掉版本号外泄
|
||
# (/etc/apache2/conf-enabled/zz-lsky-hardening.conf:ServerTokens Prod + ServerSignature Off),
|
||
# 两处都带构建期断言;CI 的「Verify published image」再对真实响应头断言一次。
|
||
# 7. 认证类 POST 加路由级节流(src/routes/auth.php:login/register/forgot-password/confirm-password)
|
||
# 8. 测试版内存收紧(2026-10-01):PHP memory_limit 收到 64M(可运行时用 env PHP_MEMORY_LIMIT
|
||
# 覆盖,entrypoint 渲染进上面的 hardening ini)、opcache memory_consumption 128→64、
|
||
# Apache MaxConnectionsPerChild 保持默认 5(低流量站,每 ~3 分钟换班开销可忽略,
|
||
# 取最严的内存纪律;可运行时用 env APACHE_MAX_CONNECTIONS_PER_CHILD 覆盖,0 = 永不回收)。
|
||
# 详见各段注释与 CI「Verify published image」的新断言。
|
||
#
|
||
# 构建:docker build -t lsky-pro-docker .
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# 上游源码快照的版本号(只用于版本标记与镜像元数据;代码本体是仓库里的 src/)。
|
||
# 改这里 = 必须同时用 tools/vendor-upstream.sh 重新 vendor src/,否则标记与实际代码不符(自证 1 会失败)。
|
||
ARG LSKY_COMMIT=38d52c4609eb85236b45ac75acac2ced55174953
|
||
# PHP 版本:8.1 已于 2025-12-31 EOL,8.2 的 EOL 是 2026-12-31(都太近),
|
||
# 所以选 8.3(安全维护到 2027-12-31)。依赖侧配套升到同一大版本线内的最新:
|
||
# laravel/framework 9.52.22(9.x 最后一个补丁)+ symfony/* 6.4 LTS(Laravel 9 的 ^6.0 正好允许)。
|
||
# 要回退 PHP:改这个数字即可,但请连镜像 tag 一起回退(新 lock 里的包可能要求 ≥8.3)。
|
||
ARG PHP_VERSION=8.3
|
||
ARG DEBIAN_RELEASE=bookworm
|
||
ARG PHP_EXT_INSTALLER_VERSION=2.12.0
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# F15:基镜像钉 digest —— 上游重推 tag(同 tag 指向新内容)时,构建出来的仍是同一份东西。
|
||
# 两个阶段用的**不是**同一个基镜像(builder = cli,runtime = apache),各自钉自己的。
|
||
# 更新方法(改 PHP_VERSION / DEBIAN_RELEASE 时必须一并更新,否则构建会因 manifest 不匹配而失败):
|
||
# 1) 取 token(本机没有 docker,别用 docker manifest):
|
||
# curl -s 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/php:pull'
|
||
# -> 取返回 JSON 里的 .token
|
||
# 2) 读 digest(tag 换成要查的那个):
|
||
# curl -sI -H "Authorization: Bearer $TOKEN"
|
||
# -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json'
|
||
# https://registry-1.docker.io/v2/library/php/manifests/8.3-cli-bookworm
|
||
# -> 看响应头 Docker-Content-Digest(8.3-apache-bookworm 同理,两个都要查)
|
||
# 3) 把拿到的 sha256 填进下面的 ARG(**tag 保留**,digest 附在 @ 之后,形如 php:8.3-apache-bookworm@sha256:…)
|
||
# 注意:钉死后上游的安全修复**不会**自己进来 —— 想拿新修复就得重跑上面两步、更新 digest,再重建镜像。
|
||
# 试构建(CI 的 workflow_dispatch 换 PHP 版本)也要一并覆盖对应的 digest ARG:
|
||
# 带 `@digest` 时 **digest 优先、tag 只作装饰**,只换 PHP_VERSION 不换 digest 会静默构建出旧版本。
|
||
# CI 已自动化:build-image.yaml 的 Resolve 步骤会按请求的 PHP 版本解析出对应 digest 并覆盖这两个 ARG
|
||
# (也可在 workflow_dispatch 里手工传 php_cli_digest / php_apache_digest)。上面这套手工方法仅用于本地/离线场景。
|
||
# 实测(2026-09-30,Docker Hub Registry API 原始响应头,见提交说明/报告):
|
||
# php:8.3-cli-bookworm -> sha256:4687aec76c4a895b68b91bcd5e48f1ba7a3dea120f6de50bba7e1a93af5372dd
|
||
# php:8.3-apache-bookworm -> sha256:06df07e2e1d72581dde4fd97bc98d2d0b78686fc13d6a041c520da9a6ce73055
|
||
ARG PHP_CLI_IMAGE_DIGEST=sha256:4687aec76c4a895b68b91bcd5e48f1ba7a3dea120f6de50bba7e1a93af5372dd
|
||
ARG PHP_APACHE_IMAGE_DIGEST=sha256:06df07e2e1d72581dde4fd97bc98d2d0b78686fc13d6a041c520da9a6ce73055
|
||
|
||
FROM php:${PHP_VERSION}-cli-${DEBIAN_RELEASE}@${PHP_CLI_IMAGE_DIGEST} AS build
|
||
|
||
ARG LSKY_COMMIT
|
||
ARG FORK_SHA=unknown
|
||
|
||
WORKDIR /build
|
||
|
||
# 安装必要的依赖
|
||
# curl —— 装 composer
|
||
# unzip —— composer 解压 dist 包要用(去掉它会导致 composer install 直接失败:CI 实测踩过)
|
||
# git —— 部分包会走 source 安装时的兜底
|
||
RUN docker-php-ext-install ftp
|
||
|
||
# 依赖安装(builder 阶段的平台要求必须满足,否则 composer install 直接失败)
|
||
# F16:composer 安装器**先校验、再执行**(不能像以前那样 `curl … | php` 直接把网上的字节喂给解释器)。
|
||
# 官方做法(2026-09-30 实查 https://getcomposer.org/download/):预期 sha384 来自
|
||
# https://composer.github.io/installer.sig,与 https://getcomposer.org/installer 分属两个域名,
|
||
# 单一站点被投毒时另一侧能拦住。校验不过(sig 拉不到 / 格式不对 / 对不上)就不往下走 = 构建失败。
|
||
# 这里**故意不写死**哈希:installer 会随每个 composer 版本重新生成(官方文档明确警告别复制传播这段代码),
|
||
# 写死的话 composer 一发新版、构建就必然红。想连 composer 版本一起钉,改用带版本号的
|
||
# https://getcomposer.org/download/<version>/composer.phar + 同目录 .sha256sum 校验。
|
||
RUN apt-get update && \
|
||
apt-get install -y curl unzip git && \
|
||
curl -fsSL -o /tmp/composer-setup.php https://getcomposer.org/installer && \
|
||
curl -fsSL -o /tmp/composer-setup.sig https://composer.github.io/installer.sig && \
|
||
grep -Eq '^[0-9a-f]{96}$' /tmp/composer-setup.sig && \
|
||
printf '%s %s\n' "$(cat /tmp/composer-setup.sig)" /tmp/composer-setup.php | sha384sum -c - && \
|
||
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
|
||
rm -f /tmp/composer-setup.php /tmp/composer-setup.sig && \
|
||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||
|
||
# 应用源码:本仓库 src/ 里的 vendored 快照(= 上游那个 commit 的完整树,304 个文件)。
|
||
# 前端补丁已经就地在 src/ 里改好,这里不做任何"叠加覆盖"。改代码 = 改 src/ 然后提交。
|
||
COPY src/ /build/
|
||
|
||
# 自证 1:vendored 快照确实是上游那一版。
|
||
# 下面这个文件我们从不修改,md5 等于上游原值;对不上就说明 src/ 被误改
|
||
# (或与 LSKY_COMMIT 不符)→ 直接构建失败,不产出不可信的镜像。
|
||
RUN printf '%s\n' \
|
||
'3b58bff18126a61c142cc576457c82a6 ./public/index.php' \
|
||
| md5sum -c -
|
||
|
||
# 自证 1b:有意偏离上游、但仍需锁死的文件。
|
||
# config/convention.php 与 routes/web.php 被本 fork 改过 —— 移除了「画廊」「系统升级」,
|
||
# 且首页 / 在登录状态下改为跳转「我的图片」(游客上传页只留给未登录用户)
|
||
# 两个功能(删配置项常量、删路由表条目,详见 README)。
|
||
# app/Services/ImageService.php 同样是有意偏离的:移除 SVG 支持时删掉了几个「只可能因为
|
||
# svg 数据而命中」的分支(上传跳过图片处理 / 跳过违规扫描的 in_array svg、makeThumbnail
|
||
# 里 svg 直拷原文件),详见 README「与上游的差异」第 7 条。
|
||
# routes/auth.php 也是有意偏离(F23 防爆破):给 login / register / forgot-password /
|
||
# confirm-password 四个 POST 端点加了 throttle(详见文件内注释)。它同样被钉住,
|
||
# 改了不更新这行 md5 就构建失败。
|
||
# 这几个文件以后只要被改动(哪怕手滑),构建就会红 —— md5 必须随改动同步更新。
|
||
RUN printf '%s\n' \
|
||
'8136e73b50315d783f105dd4ac9971bb ./config/convention.php' \
|
||
'881fdbaed19ef39027783f093448875d ./routes/web.php' \
|
||
'c1ab546f3e7f5237c1d45435171858ce ./routes/auth.php' \
|
||
'e7edc0fc9dc8c654c4debca0e1d2eac4 ./app/Services/ImageService.php' \
|
||
| md5sum -c -
|
||
|
||
# 有意偏离上游的另一个文件:composer.lock。
|
||
# 上游冻结在 2024-12 的解析结果(laravel 9.52.17 / symfony 6.0.x —— 两者都已 EOL),
|
||
# 我们把它升到「同一大版本线内的安全版本」:
|
||
# laravel/framework 9.52.22(9.x 最后一个补丁;9.52.21 -> 9.52.22 是一次安全修复)、symfony/* 6.4 LTS(Laravel 9 的 ^6.0 允许)、
|
||
# guzzlehttp/guzzle 7.15.5、guzzlehttp/psr7 2.13.1、phpseclib 3.0.57、
|
||
# league/commonmark 2.10.3、aws/aws-sdk-php 3.398.1、laminas-diactoros 2.26.0
|
||
# psy/psysh 0.11.23(2026-09-30:原 0.11.12 有 GHSA-4486-gxhx-5mg7 —— CWD 下 .psysh.php 自动加载
|
||
# 导致本地提权;上游在同一条 0.11 线发了修复,随 laravel/tinker 一起进镜像,故一并升掉。
|
||
# osv-scanner.toml 里对应那条白名单已随之删除=扫描真的干净,而不是被豁免。)
|
||
# —— 修掉了除「Laravel 9 框架自身那几条(9.x 线没有修复版本)」以外的已知公告。
|
||
# 所以它不再等于上游值 —— 但仍钉 md5:任何改动都必须同步更新这里(防止有人别处悄悄改)。
|
||
RUN printf '%s\n' \
|
||
'12ab233331630b2dd1c48cee9e5ce927 ./composer.lock' \
|
||
| md5sum -c -
|
||
|
||
# --no-dev:镜像只装运行时依赖。开发包(debugbar / ignition / whoops / phpunit / faker / sail…)
|
||
# 对线上没有用途,留着既是体积也是暴露面(debugbar 那类在调试模式下会漏内部信息)。
|
||
# 连带项:卷里的 bootstrap/cache/packages.php 是"当年装着开发包"时生成的,里面记着这些包的
|
||
# ServiceProvider 类名 —— 包没了之后,Laravel 引导时加载该清单会 Class not found → 整站 500。
|
||
# 所以 entrypoint 的同步分支会作废这份清单(连同 services.php),CI 里也有对应的回归断言。
|
||
RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||
&& composer install --no-dev
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# fork 补丁:iOS 长按菜单修复 + 菜单交互修复(已就地在 src/ 里,这里只做校验)
|
||
#
|
||
# 补丁涉及三个文件(两份 JS 内容必须一致,别只改一份):
|
||
# src/resources/js/context-js.js —— 可读源码
|
||
# src/public/js/context-js/context-js.js —— 浏览器实际加载的那份
|
||
# src/resources/views/user/images.blade.php —— 引入脚本那一行(资源版本串)
|
||
# (上游 webpack.mix.js 里本来也是 mix.copy('resources/js/context-js.js', 'public/js/context-js'),
|
||
# 只是仓库里 public/ 下那份是旧工具链留下的压缩产物,一直没跟着源码更新。)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# 自证 2:补丁内容与预期完全一致,blade 的资源版本串也在。
|
||
# 改 src/ 里这三个文件后必须同步更新这里的 md5(故意做成"改了不更新就构建失败"),
|
||
# 并重新生成 patches/ios-longpress.patch(tools/diff-vs-upstream.sh)。
|
||
RUN printf '%s\n' \
|
||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \
|
||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \
|
||
'15878ee87c65d46137249dde8e40931f ./resources/views/user/images.blade.php' \
|
||
| md5sum -c - \
|
||
&& grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \
|
||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'LONG_PRESS_DELAY = 250' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'LONG_PRESS_DELAY = 250' ./resources/js/context-js.js \
|
||
&& grep -q 'installMenuGuard' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'MENU_OPEN_GRACE' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'touch-open' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'fitSubmenu' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'clampMenu' ./resources/js/context-js.js \
|
||
&& grep -q 'submenu-inplace' ./public/js/context-js/context-js.js \
|
||
&& grep -q 'SUBMENU_GUARD' ./public/js/context-js/context-js.js
|
||
|
||
# 代码版本标记:入口脚本用它判断「卷里的代码是不是当前镜像这一版」,不一致才同步(见 entrypoint.sh)。
|
||
# 它由「源码 commit + 补丁 md5 + 内容指纹」组成:任何代码/补丁变化都会让它变。
|
||
# 改 src/ 下任何文件(哪怕一行)后,都要把对应的 md5 一并更新,否则容器不会重新同步进卷。
|
||
#
|
||
# 内容指纹(F22):上面那两个 md5 只覆盖「我们打补丁的那几个文件」—— app.js / PHP 这类**没被钉 md5**
|
||
# 的文件改了之后标记不变,卷里的旧代码就永远不会被同步(镜像与卷不同步,且没有任何报错)。
|
||
# 所以这里再对三处构建期内容做确定性哈希,把它们也编进标记:
|
||
# app_src_md5 = app/ config/ routes/ 三个目录下所有文件「按路径排序后内容拼接」的 md5
|
||
# (排序用 sort -z,与文件系统返回顺序无关 = 同一份源码在任何机器上结果一致);
|
||
# app_js_md5 = public/js/app.js 的 md5;app_css_md5 = public/css/app.css 的 md5;
|
||
# composer_lock_md5 = composer.lock 的 md5(应用根目录,构建后镜像里是 /var/www/lsky/composer.lock)。
|
||
# 补这一条的背景:上面的指纹只覆盖「代码」,而 composer.lock 变了(只升级 vendor 依赖、
|
||
# 其它 PHP/前端都没动)时,标记不变 → 老卷不会重新同步 → 卷里是旧依赖、镜像里是新依赖。
|
||
# composer install 是在 build 阶段、镜像自带的 vendor/ 已经按新 lock 装好;
|
||
# 加了这一条,只换依赖的镜像也会让标记变、触发一次同步。
|
||
# 任何一个不存在 / 命令失败都写 none(标记照写)—— **绝不能让构建失败**。
|
||
# 入口脚本比的是整个标记文件的内容,所以多出这几行 = 换镜像时自动触发同步,entrypoint 不需要改。
|
||
RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z | xargs -0 -r cat 2>/dev/null | md5sum | cut -d' ' -f1); \
|
||
[ -n "$APP_SRC_MD5" ] || APP_SRC_MD5=none; \
|
||
APP_JS_MD5=$(md5sum public/js/app.js 2>/dev/null | cut -d' ' -f1); \
|
||
[ -n "$APP_JS_MD5" ] || APP_JS_MD5=none; \
|
||
APP_CSS_MD5=$(md5sum public/css/app.css 2>/dev/null | cut -d' ' -f1); \
|
||
[ -n "$APP_CSS_MD5" ] || APP_CSS_MD5=none; \
|
||
COMPOSER_LOCK_MD5=$(md5sum composer.lock 2>/dev/null | cut -d' ' -f1); \
|
||
[ -n "$COMPOSER_LOCK_MD5" ] || COMPOSER_LOCK_MD5=none; \
|
||
printf '%s\n' \
|
||
"fork_sha=${FORK_SHA}" \
|
||
"lsky_commit=${LSKY_COMMIT}" \
|
||
"context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \
|
||
"images_blade_md5=15878ee87c65d46137249dde8e40931f" \
|
||
"app_src_md5=${APP_SRC_MD5}" \
|
||
"app_js_md5=${APP_JS_MD5}" \
|
||
"app_css_md5=${APP_CSS_MD5}" \
|
||
"composer_lock_md5=${COMPOSER_LOCK_MD5}" \
|
||
> .code-revision \
|
||
&& cat .code-revision
|
||
|
||
FROM php:${PHP_VERSION}-apache-${DEBIAN_RELEASE}@${PHP_APACHE_IMAGE_DIGEST}
|
||
|
||
ARG LSKY_COMMIT
|
||
ARG PHP_EXT_INSTALLER_VERSION
|
||
ARG FORK_SHA=unknown
|
||
|
||
LABEL org.opencontainers.image.source="https://github.com/mole404/lsky-pro-docker" \
|
||
org.opencontainers.image.title="lsky-pro-docker (ios-longpress + menu-ux)" \
|
||
org.opencontainers.image.description="Lsky Pro 图床的 Docker 镜像;应用源码 vendored 取自 lsky-org/lsky-pro@${LSKY_COMMIT}(构建不再依赖上游在线),并在其上做了 iOS 长按菜单修复与菜单交互修复(点菜单外只关菜单、触摸端二级菜单点击展开、滚动/缩放/Esc 关闭)" \
|
||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||
org.opencontainers.image.revision="${FORK_SHA}" \
|
||
lsky.source.commit="${LSKY_COMMIT}"
|
||
|
||
# 如果构建速度慢可以换源
|
||
# RUN sed -i -E "s@http://***@http://mirrors.cloud.tencent.com@g" /etc/apt/sources.list
|
||
# 安装相关拓展(钉到具体版本,不使用 latest)
|
||
# F16:mlocati 的 release **只有脚本本身、没有官方校验资产**(2026-09-30 实查 2.12.0 的 release assets
|
||
# 只有 1 个),所以期望 sha256 直接写死在仓库里,下载后立即校验,验不过就构建失败。
|
||
# 换 PHP_EXT_INSTALLER_VERSION 时**必须**同步更新下面这个值(故意的:忘了就红):
|
||
# 新值取法:curl -fsSL -o /tmp/ipe "https://github.com/mlocati/docker-php-extension-installer/releases/download/<新版本>/install-php-extensions" && sha256sum /tmp/ipe
|
||
# 当前值对应 2.12.0(2026-09-30 实测):3f49c71fa66c79b8b2b96bc0ce92885dafd7946f3b5a46f545b390d6f1617e2c
|
||
ARG PHP_EXT_INSTALLER_SHA256=3f49c71fa66c79b8b2b96bc0ce92885dafd7946f3b5a46f545b390d6f1617e2c
|
||
RUN curl -fsSL -o /usr/local/bin/install-php-extensions \
|
||
"https://github.com/mlocati/docker-php-extension-installer/releases/download/${PHP_EXT_INSTALLER_VERSION}/install-php-extensions" \
|
||
&& echo "${PHP_EXT_INSTALLER_SHA256} /usr/local/bin/install-php-extensions" | sha256sum -c -
|
||
|
||
# 开启SSL
|
||
RUN a2enmod ssl && a2ensite default-ssl
|
||
|
||
# F21 修正(CI 实撞,2026-09-30):Debian 自带的 /etc/apache2/sites-available/default-ssl.conf
|
||
# 由上面的 a2ensite 启用,而它**仍指向被删掉的那对 snakeoil 证书文件** → Apache 启动时配置校验报
|
||
# AH00526: SSLCertificateFile: file '/etc/ssl/certs/ssl-cert-snakeoil.pem' does not exist or is empty
|
||
# 整个 Apache 起不来(症状很难认:容器进程在、安装页返回 000)。
|
||
# 把它也指到入口脚本运行期生成的那对证书(路径固定,与 000-default.conf.template 的 HTTPS vhost 一致)。
|
||
# 护栏按**文件路径**判而不是按 "snakeoil" 这个词判 —— 该文件里有一句提到 snakeoil 的注释是正常的。
|
||
# (本地在真容器里模拟过:改完 + 证书就位 → apache2ctl -t 输出 Syntax OK)
|
||
RUN sed -i \
|
||
-e 's#/etc/ssl/certs/ssl-cert-snakeoil.pem#/etc/apache2/ssl/lsky-selfsigned.crt#' \
|
||
-e 's#/etc/ssl/private/ssl-cert-snakeoil.key#/etc/apache2/ssl/lsky-selfsigned.key#' \
|
||
/etc/apache2/sites-available/default-ssl.conf \
|
||
&& ! grep -rn '/etc/ssl/certs/ssl-cert-snakeoil\|/etc/ssl/private/ssl-cert-snakeoil' /etc/apache2/ \
|
||
&& grep -q 'lsky-selfsigned.crt' /etc/apache2/sites-available/default-ssl.conf
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# F23:Apache 版本号外泄 —— 关掉 Server 头里的精确版本与页脚签名。
|
||
# 实测(2026-09-30,本机 docker run 已发布镜像):响应头 `Server: Apache/2.4.68 (Debian)`。
|
||
# (同一次探测还看到 `X-Powered-By: PHP/8.3.35` —— 那条由下面的 PHP ini 加固负责。)
|
||
# ServerTokens Prod → Server 头只发 "Apache"(不带版本 / 模块 / OS);
|
||
# ServerSignature Off → 错误页与目录列表不再追加 "Apache/2.4.68 (Debian) Server at …" 页脚
|
||
# (Debian 出厂默认本就是 Off,这里显式写死:基镜像换版本时默认值可能变)。
|
||
# 为什么放 conf-enabled 的独立文件、而不是 000-default.conf.template 那个 vhost 模板:
|
||
# ① ServerTokens 是**全局(main server config)指令**,写进 <VirtualHost> 里 Apache 会直接
|
||
# 拒绝加载该配置("ServerTokens not allowed in <VirtualHost> context")→ 整站起不来;
|
||
# 它必须落在全局上下文,而模板整份就是 vhost。
|
||
# ② 那个模板同时管 HTTPS vhost、目录权限等一堆东西,改它等于改 vhost 行为,风险面远大于
|
||
# 「加一个 conf-enabled 文件」。
|
||
# ③ apache2.conf 先 IncludeOptional mods-enabled/ 再 IncludeOptional conf-enabled/,入口
|
||
# 脚本写 mpm.conf 也走这里;conf-enabled 不会被入口脚本覆写,换镜像照样生效。
|
||
# 构建期自证:① 文件真的在;② 两行**逐行精确**在里面(grep -qx,不是模糊包含)。
|
||
# 语法由真容器自证(CI 的 smoke 步骤会起容器跑 Apache,配置写错站点就起不来)。
|
||
# ---------------------------------------------------------------------------
|
||
RUN printf '%s\n' \
|
||
'# fork 加固(F23):不对外泄露 Apache 精确版本' \
|
||
'ServerTokens Prod' \
|
||
'ServerSignature Off' \
|
||
> /etc/apache2/conf-enabled/zz-lsky-hardening.conf \
|
||
&& test -f /etc/apache2/conf-enabled/zz-lsky-hardening.conf \
|
||
&& grep -qx 'ServerTokens Prod' /etc/apache2/conf-enabled/zz-lsky-hardening.conf \
|
||
&& grep -qx 'ServerSignature Off' /etc/apache2/conf-enabled/zz-lsky-hardening.conf
|
||
|
||
# ftp 必须**显式**装进运行时镜像:官方 php:8.1 镜像自带 ftp,php:8.3 的没有
|
||
# (2026-09-28 换 8.3 时 CI 实测:builder 阶段 composer install 直接失败,
|
||
# 报 league/flysystem-ftp requires ext-ftp)。它是 Lsky 的 FTP 存储驱动要用的扩展,
|
||
# 不能依赖"从基镜像继承"。CI 里也加了 ftp 的硬断言。
|
||
#
|
||
# opcache(测试版收紧,先量后改):原值 memory_consumption=128 / max_accelerated_files=10000。
|
||
# 实测(2026-10-01,本机真容器 Apache 进程内 opcache_get_status):
|
||
# · 真实负载(安装页 + 登录/注册/首页等通跑一轮)预热集合 = 827 个脚本 / 已用 25.0MB;
|
||
# · 全树编译(把 /var/www/lsky 下 10842 个 .php 全部塞进缓存)上限 = 9949 个脚本 / 255.9MB
|
||
# (在 256MB 上限处被截断 —— 这是"理论上限",没有任何单次请求会走到)。
|
||
# 真实使用约 25MB,故 memory_consumption 收到 64(≈实测 2.6 倍,且不低于 64):
|
||
# 够用又有余量,比原来的 128 省 64MB 共享内存(本机 965MB,内存是瓶颈)。
|
||
# max_accelerated_files 保持 10000 —— 已远超实测预热集合(827)的 12 倍;哈希表本身只占 KB 级,
|
||
# 调低没有收益、调高没有意义(64MB 的脚本缓存也装不下全树)。这组值下 opcache 不会因容量不足频繁重启。
|
||
# validate_timestamps 保持 1(显式写死,防基镜像默认变化):单机热更新靠它 —— 卷里代码一改,
|
||
# 带 mtime 校验的请求就会重新编译;关掉它换镜像后要重启容器才生效。
|
||
RUN apt-get update && \
|
||
apt-get install -y gettext && \
|
||
apt-get clean && rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/* && \
|
||
a2enmod rewrite && chmod +x /usr/local/bin/install-php-extensions && \
|
||
install-php-extensions imagick bcmath pdo_mysql pdo_pgsql redis ftp && \
|
||
\
|
||
{ \
|
||
echo 'post_max_size = 100M;';\
|
||
echo 'upload_max_filesize = 100M;';\
|
||
echo 'max_execution_time = 600S;';\
|
||
} > /usr/local/etc/php/conf.d/docker-php-upload.ini; \
|
||
\
|
||
{ \
|
||
echo 'opcache.enable=1'; \
|
||
echo 'opcache.interned_strings_buffer=8'; \
|
||
echo 'opcache.max_accelerated_files=10000'; \
|
||
echo 'opcache.memory_consumption=64'; \
|
||
echo 'opcache.save_comments=1'; \
|
||
echo 'opcache.revalidate_freq=1'; \
|
||
echo 'opcache.validate_timestamps=1'; \
|
||
} > /usr/local/etc/php/conf.d/opcache-recommended.ini; \
|
||
\
|
||
echo 'apc.enable_cli=1' >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini; \
|
||
\
|
||
echo 'memory_limit=512M' > /usr/local/etc/php/conf.d/memory-limit.ini; \
|
||
\
|
||
mkdir /var/www/data; \
|
||
chown -R www-data:root /var/www; \
|
||
chmod -R g=u /var/www
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# F23:PHP ini 加固 —— 关掉「版本号外泄」与「错误直出」。
|
||
# 实测(2026-09-30,本机 docker run 已发布镜像):响应头带 `X-Powered-By: PHP/8.3.35`。
|
||
# 官方 php 镜像**不带 php.ini**(只有 php.ini-production/-development 两个样本),于是
|
||
# PHP 编译期的默认值直接生效:expose_php=1、display_errors=1。
|
||
# expose_php=0 → 不再发 X-Powered-By(少给扫描器一条精确版本情报);
|
||
# display_errors=0 → 运行期错误不往 HTTP 响应里吐(Laravel 自己的错误处理/日志不受影响)。
|
||
# 文件名用 zz- 前缀:conf.d 里的 ini 按**文件名 ASCII 序**加载、后加载的覆盖先前的,
|
||
# zz- 排最后 = 以后基镜像或扩展安装器再加什么 ini,都盖不掉这两项(顺序就是这里的关键)。
|
||
# 本次(测试版)追加 memory_limit=64M:先读现状 —— PHP 编译期默认 128M,而镜像里
|
||
# (上游)memory-limit.ini 写的是 512M,所以**原来的生效值是 512M**。把 64M 放进这份
|
||
# zz- 文件即可生效(排最后、覆盖 memory-limit.ini),不必去改那份上游 ini。
|
||
# 默认 64M 而不是写死:entrypoint.sh 运行期会把 PHP_MEMORY_LIMIT(默认 64M)渲染进这一项,
|
||
# 在 compose 里改一个 env 就能回 128M/256M、不用重建镜像(脏值只忽略该项并警告)。
|
||
# 其余 ini 未动:upload(100M 上传上限)保持原样;opcache-recommended.ini 的
|
||
# memory_consumption 本次从 128 收到 64(依据见上面 opcache 段的实测注释)。
|
||
# 构建期自证:用 php -r **真读回 ini_get()** 断言已是目标值 —— 不是 grep 配置文件里有没有那几行
|
||
# (那只证明"文件写了",证明不了"PHP 真的读到了")。expose_php/display_errors 判定用
|
||
# filter_var(FILTER_VALIDATE_BOOL),兼容 Off / false / 0 / 空串 几种写法;memory_limit 直接比对
|
||
# 字符串 "64M"。CLI 与 Apache 模块用的是同一份 /usr/local/etc/php/conf.d。
|
||
# ---------------------------------------------------------------------------
|
||
RUN printf '%s\n' \
|
||
'; fork 加固(F23 + 测试版):不泄露版本号 / 错误不直出 / 收紧内存上限' \
|
||
'expose_php=0' \
|
||
'display_errors=0' \
|
||
'memory_limit=64M' \
|
||
> /usr/local/etc/php/conf.d/zz-lsky-hardening.ini \
|
||
&& test -f /usr/local/etc/php/conf.d/zz-lsky-hardening.ini \
|
||
&& php -r 'foreach (["expose_php", "display_errors"] as $k) { if (filter_var(ini_get($k), FILTER_VALIDATE_BOOL)) { fwrite(STDERR, "ini 加固自证失败:".$k." 仍开着 (".var_export(ini_get($k), true).")".PHP_EOL); exit(1); } echo $k."=".var_export(ini_get($k), true)." 已关闭".PHP_EOL; }' \
|
||
&& php -r '$m = ini_get("memory_limit"); if ($m !== "64M") { fwrite(STDERR, "memory_limit 自证失败:读回 ".var_export($m, true).",期望 64M(zz-lsky-hardening.ini 是否被别的 ini 覆盖?)".PHP_EOL); exit(1); } echo "memory_limit=".$m."(entrypoint 可用 PHP_MEMORY_LIMIT 运行期覆盖)".PHP_EOL;'
|
||
|
||
# F21:不再随镜像发货证书。
|
||
# 原来这里是 `COPY ./ssl /etc/ssl` —— 把仓库里那对 Debian snakeoil 证书(ssl/certs + ssl/private,
|
||
# 在 GitHub 上人人可见)拷进镜像,模板的 HTTPS vhost 直接引用它 = 每个使用者共用同一份公开私钥。
|
||
# 现在改成运行期自签:入口脚本首次启动时用 openssl 生成到 /etc/apache2/ssl/(模板指向那里),
|
||
# 所以镜像里**不再有任何证书/私钥文件**。openssl 必须存在 —— 由下面的护栏钉住。
|
||
|
||
COPY --from=build /build /var/www/lsky/
|
||
COPY ./000-default.conf.template /etc/apache2/sites-enabled/
|
||
COPY ./ports.conf.template /etc/apache2/
|
||
# Apache MPM 模板:入口脚本会把它(按 APACHE_* 渲染后)写到 /etc/apache2/conf-enabled/mpm.conf,
|
||
# 覆盖 mods-enabled 里 mpm_prefork.conf 的出厂值(apache2.conf 先 include mods-enabled 再 include conf-enabled)。
|
||
COPY ./mpm.conf.template /etc/apache2/
|
||
COPY entrypoint.sh /
|
||
|
||
# 数据安全护栏:强制同步的排除清单缺任何一项都可能覆盖站点数据/配置,缺了就构建失败。
|
||
# (镜像自带入口脚本运行时才会执行,这里只做静态断言,保证清单不被误删。)
|
||
RUN grep -q -- '--exclude=.\/.env' /entrypoint.sh \
|
||
&& grep -q -- '--exclude=.\/storage' /entrypoint.sh \
|
||
&& grep -q -- '--exclude=.\/database' /entrypoint.sh \
|
||
&& grep -q -- '--exclude=.\/bootstrap\/cache' /entrypoint.sh \
|
||
&& grep -q -- '--exclude=.\/public\/i' /entrypoint.sh \
|
||
&& grep -q 'views/\*.php' /entrypoint.sh \
|
||
&& grep -q 'code-revision' /entrypoint.sh \
|
||
&& test -f /var/www/lsky/.code-revision \
|
||
&& grep -q '^fork_sha=' /var/www/lsky/.code-revision \
|
||
&& grep -q '^composer_lock_md5=' /var/www/lsky/.code-revision \
|
||
&& grep -q 'tar cf - --exclude=./.env --exclude=./.code-revision' /entrypoint.sh \
|
||
&& grep -q 'cp -a "$IMAGE_MARKER" "$VOLUME_MARKER"' /entrypoint.sh
|
||
|
||
# MPM 模板护栏:模板必须真的进镜像、入口脚本必须真的往 conf-enabled 写(漏了 COPY 或改了目标路径就构建失败)。
|
||
# 这里只做静态断言;渲染/覆盖是否生效由 CI 里的真容器自证(Verify Apache MPM defaults and overrides)。
|
||
RUN test -f /etc/apache2/mpm.conf.template \
|
||
&& test -d /etc/apache2/conf-enabled \
|
||
&& grep -q 'conf-enabled/mpm.conf' /entrypoint.sh \
|
||
&& grep -q 'APACHE_START_SERVERS' /etc/apache2/mpm.conf.template \
|
||
&& grep -q 'APACHE_MIN_SPARE_SERVERS' /etc/apache2/mpm.conf.template \
|
||
&& grep -q 'APACHE_MAX_SPARE_SERVERS' /etc/apache2/mpm.conf.template \
|
||
&& grep -q 'APACHE_MAX_REQUEST_WORKERS' /etc/apache2/mpm.conf.template \
|
||
&& grep -q 'APACHE_MAX_CONNECTIONS_PER_CHILD' /etc/apache2/mpm.conf.template \
|
||
&& grep -q 'APACHE_KEEP_ALIVE' /etc/apache2/mpm.conf.template
|
||
|
||
# F10 护栏:健康检查要用的 curl 在运行镜像里真的存在(php:*-apache 自带,但别依赖「我记得它自带」)。
|
||
RUN command -v curl > /dev/null
|
||
|
||
# F21 护栏:运行期自签 HTTPS 证书要用 openssl(php:*-apache 基于 debian:bookworm,自带 openssl;
|
||
# 但别依赖「我记得它自带」)。缺了它入口脚本只能打警告,HTTPS vhost 起不来。
|
||
RUN command -v openssl > /dev/null
|
||
WORKDIR /var/www/html/
|
||
VOLUME /var/www/html
|
||
ENV WEB_PORT=8089
|
||
ENV HTTPS_PORT=8088
|
||
EXPOSE ${WEB_PORT}
|
||
EXPOSE ${HTTPS_PORT}
|
||
|
||
# F10:健康检查。原来没有 —— 「Apache 进程还在」不代表站点还活着(卷同步失败的容器进程照样在)。
|
||
# 探本机 http://127.0.0.1:${WEB_PORT}/:shell 形式下 ${WEB_PORT} 是**运行期**由 ENV 展开的,
|
||
# 所以 compose/`-e WEB_PORT=xxxx` 换了端口也照样探对端口(不用改这里)。
|
||
# curl -f:HTTP 状态 ≥400 或连不上都算失败(非 0 退出 = unhealthy);302/200 都算健康
|
||
# —— 未安装状态下 / 会 302 去安装页,登录后首页也可能 302,这两种都不该被判定为病态。
|
||
# start-period=90s:入口脚本要渲染 Apache/MPM 配置再把应用播种进卷(CI 里最长等到 60s),
|
||
# 这段时间内的失败不计入 retries,避免容器刚起来就 HEALTING→unhealthy。
|
||
# 依赖 curl 在运行镜像里必然存在(php:*-apache 官方镜像自带;下面的护栏会钉住这一点)。
|
||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||
CMD curl -fsS -o /dev/null "http://127.0.0.1:${WEB_PORT}/"
|
||
|
||
RUN chmod a+x /entrypoint.sh
|
||
ENTRYPOINT ["/entrypoint.sh"]
|
||
CMD ["apachectl","-D","FOREGROUND"]
|