Files
lsky-pro-docker/entrypoint.sh
T
mole404 2da63c7e9a chore(mem): MaxConnectionsPerChild 默认值改回 5(低流量下取最严内存纪律)
老师决定:他这台图床流量约 3 请求/分钟,MCP=5 意味着每 ~3 分钟才退休一个 worker,
换班开销可忽略;换班越勤,单个 worker 允许积累的内存天花板越低。故取回他原来的 5。
(中途试过 30,现按实测与他的偏好改回;env 覆盖机制 APACHE_MAX_CONNECTIONS_PER_CHILD
与 0=永不回收 的语义完全保留。)

同步改回:entrypoint.sh 默认值与旁注、build-image.yaml 6 处期望值(断言 + 自证清单 + 覆盖用例)、
Dockerfile 概览注释、docker-compose.yaml 注释。README 参数表本来就是 5,无需改。

验证:一次性容器集成跑 5 用例(默认→5、env=10→10、env=0→0、脏值 abc→回退 5 且不启动失败、
PHP_MEMORY_LIMIT 仍生效)+ apache2ctl -t = Syntax OK + memory_limit 仍 64M;
全仓 grep 无遗留的 30 期望值;Dockerfile 9 条 md5 自证 9/9。
2026-10-01 18:06:03 +08:00

442 lines
26 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -eu
# ---------------------------------------------------------------- 监听端口(WEB_PORT / HTTPS_PORT)
# 这两行原来直接 envsubst 渲染 000-default.conf / ports.conf:既没有值校验,也没有 || 守卫。
# compose 里写了空值或非数字,生成出来的就是 `Listen `(空)/ `<VirtualHost *:>` 这种废配置 →
# Apache 起不来 → exec "$@" 非 0,配合 `restart: unless-stopped` 就变成无限重启。
# 处理原则与下面的 MPM 段完全一致:只把「出问题的那一项」退回镜像默认值(与 Dockerfile 的 ENV 一致)
# 并打一行警告,绝不让容器起不来;渲染本身失败也只警告(沿用镜像里已有的配置)。
PORT_DEF_WEB=8089
PORT_DEF_HTTPS=8088
port_warn() { echo "[lsky] 警告:$*"; }
# 端口项:必须是 1-65535 的十进制整数(去掉前导零后比大小),否则退回默认值(回显合法值)
port_num() { # $1=变量名 $2=值 $3=默认值
case "$2" in
''|*[!0-9]*)
port_warn "$1=\"$2\" 不是 1-65535 的十进制整数,已退回默认值 $3" >&2
printf '%s' "$3"
return 0
;;
esac
# 位数先挡一道:超过 5 位必然越界,也避免 $((10#...)) 在超长数字上溢出算出一个「假的合法值」
if [ "${#2}" -gt 5 ]; then
port_warn "$1=$2 超出 1-65535 范围,已退回默认值 $3" >&2
printf '%s' "$3"
return 0
fi
local val=$((10#$2))
if [ "$val" -lt 1 ] || [ "$val" -gt 65535 ]; then
port_warn "$1=$2 超出 1-65535 范围,已退回默认值 $3" >&2
printf '%s' "$3"
return 0
fi
printf '%s' "$val"
}
# 注意用 `${VAR-DEF}`(不是 `:-`):变量**未设**时才静默取默认值;显式设成空串是配置错误,
# 交给 port_num 打警告并兜底(日志里能看出是"写了空值"而不是"没写")。
WEB_PORT=$(port_num WEB_PORT "${WEB_PORT-$PORT_DEF_WEB}" "$PORT_DEF_WEB")
HTTPS_PORT=$(port_num HTTPS_PORT "${HTTPS_PORT-$PORT_DEF_HTTPS}" "$PORT_DEF_HTTPS")
# 渲染 vhost / ports.conf。与 MPM 段的 mpm_render 同样先写临时文件再 mv:失败时上一条配置原样留着。
# (直接 `> 目标` 在失败时会把目标截成空文件 —— 那本身就是一份废配置,等于把要防的事故换了个触发点。)
# 注意 envsubst 只看得见**导出**的环境变量,校验后的值此刻只是 shell 变量,必须显式喂进去,
# 否则"变量未设"时渲染出来的就是空的 `Listen`。
render_conf() { # $1=模板 $2=目标
local tmp
# 目标被目录占住时 `mv` 会把文件搬进去、还返回 0(静默假成功)—— 先挡掉
if [ -d "$2" ]; then
return 1
fi
tmp=$(mktemp) || return 1
if ! WEB_PORT="$WEB_PORT" HTTPS_PORT="$HTTPS_PORT" \
envsubst '${WEB_PORT} ${HTTPS_PORT}' <"$1" >"$tmp" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
mv "$tmp" "$2" || { rm -f "$tmp"; return 1; }
return 0
}
render_conf /etc/apache2/sites-enabled/000-default.conf.template /etc/apache2/sites-enabled/000-default.conf \
|| port_warn "渲染 000-default.conf 失败,沿用镜像里已有的配置(Apache 可能起不来)"
render_conf /etc/apache2/ports.conf.template /etc/apache2/ports.conf \
|| port_warn "渲染 ports.conf 失败,沿用镜像里已有的配置(Apache 可能起不来)"
# ---------------------------------------------------------------- 运行期自签 HTTPS 证书(F21)
# 镜像里**不再发货**证书:原来 `COPY ./ssl /etc/ssl` 把一对 Debian snakeoil 证书(含私钥、
# 在 GitHub 上人人可见)拷进镜像,模板的 HTTPS vhost 直接引用 —— 每个使用者共用同一份公开私钥。
# 现在改成首次启动时用 openssl 自签一份,落到下面的固定路径(模板已指向这里),之后复用。
# 与上面的端口 / MPM 段同样的原则:幂等(已存在就跳过)+ 失败只警告 + 绝不让入口脚本(set -eu)挂掉。
SSL_DIR=/etc/apache2/ssl
SSL_CRT=$SSL_DIR/lsky-selfsigned.crt
SSL_KEY=$SSL_DIR/lsky-selfsigned.key
ssl_warn() { echo "[lsky] 警告:$*" >&2; }
# -s:文件存在且非空才算「已就绪」;空文件(上次生成中途死掉)会被当成不存在而重新生成。
if [ -s "$SSL_CRT" ] && [ -s "$SSL_KEY" ]; then
echo "[lsky] HTTPS 自签证书已就绪($SSL_CRT),跳过生成"
elif ! command -v openssl > /dev/null 2>&1; then
# 正常构建出来的镜像一定带 openssl(Dockerfile 有构建期护栏);走到这里说明镜像被人动过。
ssl_warn "找不到 openssl 且没有现成证书,无法自签 HTTPS 证书;HTTPS vhost(端口 ${HTTPS_PORT})会因此起不来,其它功能不受影响"
elif ! mkdir -p "$SSL_DIR" 2>/dev/null; then
# 真实失败路径:$SSL_DIR 被一个同名文件占住 → mkdir -p 失败。
ssl_warn "$SSL_DIR 无法创建(可能被同名文件占住),未生成自签证书;HTTPS vhost(端口 ${HTTPS_PORT})会因此起不来,其它功能不受影响"
elif openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj '/CN=lsky-pro' \
-keyout "$SSL_KEY" -out "$SSL_CRT" > /dev/null 2>&1; then
# 私钥权限收紧到 600(组/其他不可读);chmod 失败不影响启动,只提示。
chmod 600 "$SSL_KEY" 2>/dev/null || ssl_warn "无法把 $SSL_KEY 权限设为 600(不影响启动)"
echo "[lsky] 已生成 HTTPS 自签证书:$SSL_CRT(RSA 2048 / 3650 天 / CN=lsky-pro)"
else
# 生成中途失败:把可能留下的半个文件清掉,避免下次被 -s 误判为「已就绪」。
rm -f "$SSL_KEY" "$SSL_CRT" 2>/dev/null || true
ssl_warn "openssl 自签证书失败,未生成证书;HTTPS vhost(端口 ${HTTPS_PORT})会因此起不来,其它功能不受影响"
fi
# ---------------------------------------------------------------- Apache MPM(低配单用户默认值 + APACHE_* 逐项覆盖)
# 背景:Debian/php-apache 的出厂 MPM 是 prefork(StartServers 5 / MinSpareServers 5 /
# MaxSpareServers 10 / MaxRequestWorkers 150 / MaxConnectionsPerChild 0),对低配单用户机太重。
# apache2.conf 先 include mods-enabled(出厂 mpm_prefork.conf)、再 include conf-enabled,后者覆盖前者 ——
# 所以把轻量值渲染进 /etc/apache2/conf-enabled/mpm.conf 就能生效。
# 每次启动都按当前环境变量重新渲染(幂等,以 compose/环境变量为准)。
# 脏值处理原则:只把「出问题的那一项」退回默认值并打警告 —— 绝不整份回退,更不让容器启动失败。
MPM_DEF_START_SERVERS=2
MPM_DEF_MIN_SPARE_SERVERS=1
MPM_DEF_MAX_SPARE_SERVERS=3
MPM_DEF_MAX_REQUEST_WORKERS=5
# MaxConnectionsPerChild 默认 5(2026-10-01 从 30 改回 5):本站流量极低(约 3 请求/分钟),
# 按这个速率每个 worker 大约 3 分钟才换一次班 —— 换班(fork/退出/再 fork)的开销在这点流量下
# 可忽略;而换班越勤,长跑 worker 累积的内存碎片/潜在泄漏越少。低流量场景下取「最严的内存纪律」。
# (中途试过 30:少换班对低流量站没多少收益,不如 5 稳。历史值 30。)
# 仍然保留 APACHE_MAX_CONNECTIONS_PER_CHILD 覆盖(0 = 永不回收,合法值)。
MPM_DEF_MAX_CONNECTIONS_PER_CHILD=5
MPM_DEF_KEEP_ALIVE=Off
MPM_TEMPLATE=/etc/apache2/mpm.conf.template
MPM_TARGET=/etc/apache2/conf-enabled/mpm.conf
mpm_warn() { echo "[lsky] 警告:Apache MPM $*"; }
# 数值项:必须是不小于下限的十进制整数,否则只把这一项退回默认值(回显合法值)
mpm_num() { # $1=变量名 $2=值 $3=下限 $4=默认值
case "$2" in
''|*[!0-9]*)
mpm_warn "$1=\"$2\" 不是非负整数,已退回默认值 $4" >&2
printf '%s' "$4"
return 0
;;
esac
local val=$((10#$2))
if [ "$val" -lt "$3" ]; then
mpm_warn "$1=$val 小于下限 $3,已退回默认值 $4" >&2
printf '%s' "$4"
return 0
fi
printf '%s' "$val"
}
MPM_START_SERVERS=$(mpm_num APACHE_START_SERVERS "${APACHE_START_SERVERS:-$MPM_DEF_START_SERVERS}" 1 "$MPM_DEF_START_SERVERS")
MPM_MIN_SPARE_SERVERS=$(mpm_num APACHE_MIN_SPARE_SERVERS "${APACHE_MIN_SPARE_SERVERS:-$MPM_DEF_MIN_SPARE_SERVERS}" 1 "$MPM_DEF_MIN_SPARE_SERVERS")
MPM_MAX_SPARE_SERVERS=$(mpm_num APACHE_MAX_SPARE_SERVERS "${APACHE_MAX_SPARE_SERVERS:-$MPM_DEF_MAX_SPARE_SERVERS}" 1 "$MPM_DEF_MAX_SPARE_SERVERS")
MPM_MAX_REQUEST_WORKERS=$(mpm_num APACHE_MAX_REQUEST_WORKERS "${APACHE_MAX_REQUEST_WORKERS:-$MPM_DEF_MAX_REQUEST_WORKERS}" 1 "$MPM_DEF_MAX_REQUEST_WORKERS")
# MaxConnectionsPerChild 的 0 是合法值(= 永不回收),所以它的下限是 0 而不是 1
MPM_MAX_CONNECTIONS_PER_CHILD=$(mpm_num APACHE_MAX_CONNECTIONS_PER_CHILD "${APACHE_MAX_CONNECTIONS_PER_CHILD:-$MPM_DEF_MAX_CONNECTIONS_PER_CHILD}" 0 "$MPM_DEF_MAX_CONNECTIONS_PER_CHILD")
# KeepAlive:只接受 On/Off(大小写不敏感),渲染时统一写成 On / Off;其它值退回 Off
case "$(printf '%s' "${APACHE_KEEP_ALIVE:-$MPM_DEF_KEEP_ALIVE}" | tr '[:upper:]' '[:lower:]')" in
on) MPM_KEEP_ALIVE=On ;;
off) MPM_KEEP_ALIVE=Off ;;
*)
mpm_warn "APACHE_KEEP_ALIVE=\"${APACHE_KEEP_ALIVE:-}\" 不是 On/Off,已退回 Off"
MPM_KEEP_ALIVE=Off
;;
esac
# 两个可选行(本身没有默认值):只在 APACHE_KEEP_ALIVE=On 且显式设置时才各写一行,
# 不设就整行不出现 —— 即跟随 Apache 发行版默认(KeepAliveTimeout 5 秒 / MaxKeepAliveRequests 100)。
# 值的校验与 KeepAlive 无关:显式设了非法值一律警告并忽略该行。
mpm_opt_num() { # $1=变量名 $2=值 → 回显正整数;非法则回显空串
case "$2" in
''|*[!0-9]*)
mpm_warn "$1=\"$2\" 不是正整数,已忽略该行(跟随 Apache 默认)" >&2
printf ''
return 0
;;
esac
local val=$((10#$2))
if [ "$val" -lt 1 ]; then
mpm_warn "$1=$val 不是正整数,已忽略该行(跟随 Apache 默认)" >&2
printf ''
return 0
fi
printf '%s' "$val"
}
MPM_KEEPALIVE_TIMEOUT=''
MPM_MAX_KEEPALIVE_REQUESTS=''
if [ -n "${APACHE_KEEPALIVE_TIMEOUT:-}" ]; then
MPM_KEEPALIVE_TIMEOUT=$(mpm_opt_num APACHE_KEEPALIVE_TIMEOUT "$APACHE_KEEPALIVE_TIMEOUT")
fi
if [ -n "${APACHE_MAX_KEEPALIVE_REQUESTS:-}" ]; then
MPM_MAX_KEEPALIVE_REQUESTS=$(mpm_opt_num APACHE_MAX_KEEPALIVE_REQUESTS "$APACHE_MAX_KEEPALIVE_REQUESTS")
fi
if [ "$MPM_KEEP_ALIVE" != "On" ]; then
MPM_KEEPALIVE_TIMEOUT=''
MPM_MAX_KEEPALIVE_REQUESTS=''
fi
# 一致性:MaxRequestWorkers ≥ MaxSpareServers ≥ MinSpareServers ≥ 1(默认值天然满足)。
# 不满足时只把「出问题的那一项」退回默认值并警告;逐项回退仍修不好(极端值)再全部退回默认值兜底。
mpm_consistent() {
[ "$MPM_MAX_REQUEST_WORKERS" -ge "$MPM_MAX_SPARE_SERVERS" ] &&
[ "$MPM_MAX_SPARE_SERVERS" -ge "$MPM_MIN_SPARE_SERVERS" ] &&
[ "$MPM_MIN_SPARE_SERVERS" -ge 1 ]
}
mpm_pass=0
while ! mpm_consistent && [ "$mpm_pass" -lt 8 ]; do
mpm_pass=$((mpm_pass + 1))
if [ "$MPM_MAX_SPARE_SERVERS" -lt "$MPM_MIN_SPARE_SERVERS" ]; then
if [ "$MPM_MAX_SPARE_SERVERS" != "$MPM_DEF_MAX_SPARE_SERVERS" ]; then
mpm_warn "MaxSpareServers=$MPM_MAX_SPARE_SERVERS < MinSpareServers=$MPM_MIN_SPARE_SERVERS,MaxSpareServers 退回默认值 $MPM_DEF_MAX_SPARE_SERVERS"
MPM_MAX_SPARE_SERVERS=$MPM_DEF_MAX_SPARE_SERVERS
else
mpm_warn "MaxSpareServers=$MPM_MAX_SPARE_SERVERS < MinSpareServers=$MPM_MIN_SPARE_SERVERS,MinSpareServers 退回默认值 $MPM_DEF_MIN_SPARE_SERVERS"
MPM_MIN_SPARE_SERVERS=$MPM_DEF_MIN_SPARE_SERVERS
fi
continue
fi
if [ "$MPM_MAX_REQUEST_WORKERS" -lt "$MPM_MAX_SPARE_SERVERS" ]; then
if [ "$MPM_MAX_REQUEST_WORKERS" != "$MPM_DEF_MAX_REQUEST_WORKERS" ]; then
mpm_warn "MaxRequestWorkers=$MPM_MAX_REQUEST_WORKERS < MaxSpareServers=$MPM_MAX_SPARE_SERVERS,MaxRequestWorkers 退回默认值 $MPM_DEF_MAX_REQUEST_WORKERS"
MPM_MAX_REQUEST_WORKERS=$MPM_DEF_MAX_REQUEST_WORKERS
else
mpm_warn "MaxRequestWorkers=$MPM_MAX_REQUEST_WORKERS < MaxSpareServers=$MPM_MAX_SPARE_SERVERS,MaxSpareServers 退回默认值 $MPM_DEF_MAX_SPARE_SERVERS"
MPM_MAX_SPARE_SERVERS=$MPM_DEF_MAX_SPARE_SERVERS
fi
continue
fi
break
done
if ! mpm_consistent; then
mpm_warn "逐项回退仍无法满足 MaxRequestWorkers ≥ MaxSpareServers ≥ MinSpareServers ≥ 1,三项全部退回默认值(保证 Apache 能起)"
MPM_MIN_SPARE_SERVERS=$MPM_DEF_MIN_SPARE_SERVERS
MPM_MAX_SPARE_SERVERS=$MPM_DEF_MAX_SPARE_SERVERS
MPM_MAX_REQUEST_WORKERS=$MPM_DEF_MAX_REQUEST_WORKERS
fi
# 渲染 + 落盘。任何一步失败都只打警告(|| 守卫),绝不因它中断入口脚本。
mpm_render() {
local tmp
# 目标被目录占住时 `mv` 会把文件搬进去、还返回 0(静默假成功)—— 先挡掉
if [ -d "$MPM_TARGET" ]; then
mpm_warn "$MPM_TARGET 已存在且是目录(预期是文件),放弃写入"
return 1
fi
tmp=$(mktemp) || return 1
if ! APACHE_START_SERVERS="$MPM_START_SERVERS" \
APACHE_MIN_SPARE_SERVERS="$MPM_MIN_SPARE_SERVERS" \
APACHE_MAX_SPARE_SERVERS="$MPM_MAX_SPARE_SERVERS" \
APACHE_MAX_REQUEST_WORKERS="$MPM_MAX_REQUEST_WORKERS" \
APACHE_MAX_CONNECTIONS_PER_CHILD="$MPM_MAX_CONNECTIONS_PER_CHILD" \
APACHE_KEEP_ALIVE="$MPM_KEEP_ALIVE" \
envsubst '${APACHE_START_SERVERS} ${APACHE_MIN_SPARE_SERVERS} ${APACHE_MAX_SPARE_SERVERS} ${APACHE_MAX_REQUEST_WORKERS} ${APACHE_MAX_CONNECTIONS_PER_CHILD} ${APACHE_KEEP_ALIVE}' \
<"$MPM_TEMPLATE" >"$tmp" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
if [ -n "$MPM_KEEPALIVE_TIMEOUT" ]; then
printf 'KeepAliveTimeout %s\n' "$MPM_KEEPALIVE_TIMEOUT" >>"$tmp"
fi
if [ -n "$MPM_MAX_KEEPALIVE_REQUESTS" ]; then
printf 'MaxKeepAliveRequests %s\n' "$MPM_MAX_KEEPALIVE_REQUESTS" >>"$tmp"
fi
mv "$tmp" "$MPM_TARGET" || { rm -f "$tmp"; return 1; }
return 0
}
MPM_WRITTEN=no
if [ ! -r "$MPM_TEMPLATE" ]; then
mpm_warn "找不到模板 $MPM_TEMPLATE,跳过 MPM 配置(Apache 继续用发行版出厂值)"
elif mpm_render && [ -f "$MPM_TARGET" ]; then
MPM_WRITTEN=yes
else
mpm_warn "渲染 $MPM_TARGET 失败,沿用镜像里已有的配置(没有该文件则用 Apache 出厂值)"
fi
# 自查用摘要:docker compose logs 里能看到生效的 6 个值(及 KeepAlive 附加的两行是否写入)
mpm_summary="StartServers=$MPM_START_SERVERS MinSpareServers=$MPM_MIN_SPARE_SERVERS MaxSpareServers=$MPM_MAX_SPARE_SERVERS MaxRequestWorkers=$MPM_MAX_REQUEST_WORKERS MaxConnectionsPerChild=$MPM_MAX_CONNECTIONS_PER_CHILD KeepAlive=$MPM_KEEP_ALIVE"
if [ "$MPM_KEEP_ALIVE" = "On" ]; then
mpm_summary="$mpm_summary(附加行:KeepAliveTimeout=${MPM_KEEPALIVE_TIMEOUT:-未设→跟随 Apache 默认 5 秒} MaxKeepAliveRequests=${MPM_MAX_KEEPALIVE_REQUESTS:-未设→跟随 Apache 默认 100})"
else
mpm_summary="$mpm_summary(KeepAlive=Off,未写 KeepAliveTimeout / MaxKeepAliveRequests)"
fi
if [ "$MPM_WRITTEN" = "yes" ]; then
echo "[lsky] Apache MPM 已写入 $MPM_TARGET:$mpm_summary"
else
echo "[lsky] Apache MPM 未写入(原因见上一行警告),原计划生效值:$mpm_summary"
fi
# ---------------------------------------------------------------- PHP memory_limit(可运行时覆盖,默认 64M)
# 现状(先量后改):PHP 编译期默认 128M,但镜像里 docker-php-upload.ini 只设上传项、
# (上游)memory-limit.ini 设的是 512M —— 生效值是 512M。本段把收紧后的 memory_limit 收进
# zz-lsky-hardening.ini(文件名 zz- 前缀 → conf.d 里排最后 → 覆盖前面所有同名项),
# 并支持用环境变量 PHP_MEMORY_LIMIT 在**运行期**渲染进去:在 compose 里改一个 env 就能回
# 128M/256M,**不用重建镜像**(与上面 MPM / 端口两段完全同一套路:临时文件 + mv、失败只警告)。
# 默认 64M 的理由:本机(1 vCPU / 965MB)内存是瓶颈;Lsky 单请求实际远用不到 64M,
# 需要做大图处理/批量时用 PHP_MEMORY_LIMIT 临时抬高即可。
# 脏值处理原则同上:只忽略这一项并警告,绝不让容器启动失败。
# ⚠ 注意:上传/表单上限仍是 100M(post_max_size / upload_max_filesize,见 docker-php-upload.ini);
# 处理大图若撞到 64M 上限,用 PHP_MEMORY_LIMIT 抬高(或调低那两个上传上限)。
PHP_DEF_MEMORY_LIMIT=64M
PHP_HARDENING_INI=/usr/local/etc/php/conf.d/zz-lsky-hardening.ini
php_warn() { echo "[lsky] 警告:PHP $*" >&2; }
# 合法形状:<数字>[KMG](大小写不敏感),例如 64M / 256M / 1G。其它一律视为脏值。
# 归一化:数字原样 + 单个大写单位字母(1g → 1G)。
php_mem_limit() { # $1=变量名 $2=值 $3=默认值
if [[ "$2" =~ ^([0-9]+)([KMGkmg])$ ]]; then
printf '%s%s' "${BASH_REMATCH[1]}" "$(printf '%s' "${BASH_REMATCH[2]}" | tr '[:lower:]' '[:upper:]')"
return 0
fi
php_warn "$1=\"$2\" 不是 <数字>[KMG] 形状(如 64M / 256M / 1G),已忽略该项并沿用默认值 $3" >&2
printf '%s' "$3"
}
# 注意用 ${VAR-DEF}(不是 :-):变量**未设**才静默取默认值;显式设成空串是配置错误,
# 交给 php_mem_limit 打警告并兜底(日志里能看出是"写了空值"而不是"没写")。
PHP_MEMORY_LIMIT_VAL=$(php_mem_limit PHP_MEMORY_LIMIT "${PHP_MEMORY_LIMIT-$PHP_DEF_MEMORY_LIMIT}" "$PHP_DEF_MEMORY_LIMIT")
# 渲染进 zz-lsky-hardening.ini:保留 expose_php / display_errors 等既有行,**只替换 memory_limit 这一项**。
# 幂等:重启时先删掉旧的 memory_limit 行再追加,不会越写越多行。
php_ini_render() {
local tmp
# 目标被目录占住时 `mv 文件 目录` 会静默假成功还返回 0 —— 先挡掉
if [ -d "$PHP_HARDENING_INI" ]; then
php_warn "$PHP_HARDENING_INI 已存在且是目录(预期是文件),放弃写入"
return 1
fi
[ -f "$PHP_HARDENING_INI" ] || return 1
tmp=$(mktemp) || return 1
grep -v '^[[:space:]]*memory_limit[[:space:]]*=' "$PHP_HARDENING_INI" > "$tmp" 2>/dev/null || true
printf 'memory_limit=%s\n' "$PHP_MEMORY_LIMIT_VAL" >> "$tmp" || { rm -f "$tmp"; return 1; }
mv "$tmp" "$PHP_HARDENING_INI" || { rm -f "$tmp"; return 1; }
return 0
}
PHP_INI_WRITTEN=no
if php_ini_render; then
PHP_INI_WRITTEN=yes
else
php_warn "渲染 $PHP_HARDENING_INI 失败,沿用镜像里已有的 memory_limit"
fi
PHP_MEMORY_SUMMARY="memory_limit=$PHP_MEMORY_LIMIT_VAL(PHP_MEMORY_LIMIT 可覆盖,默认 $PHP_DEF_MEMORY_LIMIT)"
if [ "$PHP_INI_WRITTEN" = "yes" ]; then
echo "[lsky] PHP $PHP_MEMORY_SUMMARY 已写入 $PHP_HARDENING_INI"
else
echo "[lsky] PHP $PHP_MEMORY_SUMMARY 未写入(原因见上一行警告),实际沿用镜像里已有的值"
fi
IMAGE_MARKER=/var/www/lsky/.code-revision
VOLUME_MARKER=/var/www/html/.code-revision
# ---------------------------------------------------------------- 运行时缓存作废(两条分支共用)
# 换镜像、或卷里缺 public/index.php 被重新播种之后,卷里这两类缓存都必须作废,否则:
# storage/framework/views/*.php 编译后的 blade:模板已换,不清的话浏览器拿到的还是旧 HTML;
# bootstrap/cache/packages.php|services.php 包发现清单:记录的是「旧镜像当时装着哪些包」的
# ServiceProvider 类名(含 debugbar / ignition 这类开发包)。镜像现在 composer install --no-dev,
# 这些类已经不存在,Laravel 引导时加载这份清单会 Class not found → 整站 500。
# 两者都是纯缓存:Laravel 会按当前 blade/vendor 按需重新生成,删掉没有副作用。
# 刻意**不**动 bootstrap/cache/config.php —— 那是站点自己的配置缓存(CI 有断言必须保留)。
# 播种分支原来漏了这一步:它在「卷里缺 public/index.php」时也会对**已有卷**触发,
# 那条路径上旧 packages.php 的 Class not found 照样整站 500 —— 所以两条分支都必须调这个函数。
invalidate_runtime_caches() {
rm -f /var/www/html/storage/framework/views/*.php
rm -f /var/www/html/bootstrap/cache/packages.php /var/www/html/bootstrap/cache/services.php
}
if [ ! -e '/var/www/html/public/index.php' ]; then
# ---------------------------------------------------------------- 空卷:首次部署
# 与上游逐字一致地全量播种。
#
# 刻意**不**复制镜像里的 .env:镜像是带 .env 的(构建时由 .env.example 生成、
# APP_KEY 为空),把它带进空卷会让 Lsky 的安装向导(自己生成 .env 的流程)直接 500。
# 这条是实测踩过的坑,别"顺手"改成 cp -a /var/www/lsky/. 。
# 用 tar 而不是 `cp -a /var/www/lsky/*`:cp 的通配符**不带顶层点文件**
# (.env.example、.code-revision 都得单独补拷,以后新增别的顶层点文件就会静默漏掉),
# 而 tar 会完整带上,行为也与下面的「同步分支」完全一致。
# 只排除 .env:库里那份是构建期生成的空壳(APP_KEY 为空),带进空卷会让安装向导 500。
( cd /var/www/lsky && tar cf - --exclude=./.env --exclude=./.code-revision . ) | ( cd /var/www/html && tar xf - )
# 播种分支同样要作废运行时缓存:这条路径不只跑在空卷上 —— 「卷里缺 public/index.php」
# 也会走到这里,那种已有卷里可能正躺着一份引用了已删开发包的 packages.php。
invalidate_runtime_caches
echo "[lsky] 空卷首次部署:已作废卷里的编译视图缓存与包发现缓存"
# 版本标记**最后**写:它同时是"播种完成"的信号(CI 就盯它),必须等其它文件都落盘之后再写
cp -a "$IMAGE_MARKER" "$VOLUME_MARKER"
echo "[lsky] 空卷首次部署:已把镜像应用播种到 /var/www/html"
elif [ ! -f "$VOLUME_MARKER" ] || [ "$(cat "$IMAGE_MARKER" 2>/dev/null)" != "$(cat "$VOLUME_MARKER" 2>/dev/null)" ]; then
# ---------------------------------------------------------------- 已有部署:代码版本变了
# 「换镜像 = 代码也换」:把镜像里的应用代码同步进卷,理由是版本标记不一致。
# 不需要任何环境变量,也不需要人工 cp;标记一致时(没换镜像)会跳过,零开销。
#
# 绝不能覆盖(这些属于站点自己的东西,是数据/配置,不是镜像代码):
# ./.env 站点配置:APP_KEY、数据库、S3 密钥;覆盖会导致登录态失效/数据读不出
# ./storage 上传文件、日志、会话、编译视图缓存
# ./database SQLite 数据库文件(覆盖 = 直接丢数据)
# ./bootstrap/cache Laravel 运行时缓存(config/route 缓存由应用自己生成)
# ./public/i 本地存储策略的软链/目录,属于部署产物
#
# 注意:只增改、不删除。新版镜像里删掉的旧文件不会从卷里消失(需要人工清理)。
#
# --exclude=./.code-revision 是必须的:版本标记既是「卷里的代码已同步到这一版」的标志,
# 就绝不能跟着 tar 流一起写进卷。tar 是流式的 —— 标记一被写进卷,归档还没解完(磁盘满 /
# OOM / 宿主重启)卷里就已经是「新标记 + 半套代码」;下次启动会判定同版本而永远跳过同步,
# 缺文件导致的 500 再也不会自愈。所以标记只在下面归档解完包之后由 cp 显式写一次。
( cd /var/www/lsky && tar cf - \
--exclude=./.env \
--exclude=./.code-revision \
--exclude=./storage \
--exclude=./database \
--exclude=./bootstrap/cache \
--exclude=./public/i \
. ) | ( cd /var/www/html && tar xf - )
# 模板可能刚被覆盖(例如 blade 里改了资源版本串),要清掉编译后的视图缓存,
# 否则浏览器拿到的还是旧 HTML。Laravel 会按需重新编译,代价可忽略。
# 包发现缓存同样必须作废:卷里的 packages.php / services.php 记录的是"旧镜像当时装着
# 哪些包"的 ServiceProvider 类名(含 debugbar / ignition 这类开发包)。镜像现在用
# composer install --no-dev,这些类已经不存在 —— 不清掉的话 Laravel 引导时加载这份清单
# 会 Class not found,整站 500。两个文件都是纯缓存:下次请求会按当前 vendor/ 重新生成,
# 删掉没有副作用(权限已在上面的 chown 里归一化,www-data 可写)。
invalidate_runtime_caches
echo "[lsky] 代码已同步,已作废卷里的编译视图缓存与包发现缓存"
# 版本标记**必须等归档解包完成之后**才写 —— 它同时是「同步完成」的信号。
# 写早了(例如靠 tar 流带进去)会让卷在半套代码上自称已最新,之后就永远跳过同步。
cp -a "$IMAGE_MARKER" "$VOLUME_MARKER"
echo "[lsky] 检测到镜像代码版本变化,已同步进卷:$(head -1 "$VOLUME_MARKER" 2>/dev/null || echo '标记缺失')"
else
# ---------------------------------------------------------------- 已有部署:同一版本
# 标记一致 → 卷里的代码就是当前镜像这一版,跳过同步(免得每次重启白写整棵应用树)。
echo "[lsky] 卷里的代码已是当前镜像版本,跳过同步($(head -1 "$VOLUME_MARKER"))"
fi
chown -R www-data /var/www/html
chgrp -R www-data /var/www/html
chmod -R 755 /var/www/html/
# ---------------------------------------------------------------- fork 修复(2026-09-29)
# 曾把默认头像放在 public/images/,而 /images 是应用路由:Apache 遇到真实目录会 301 到
# /images/(那里没有 index → 403),于是首页 / → 302 → /images → 301 整站打不开。
# 卷同步是「只增不删」,所以从旧镜像升级上来的卷里会残留这个目录 —— 这里无条件清掉。
# 只删我们自己放过的那一个文件(目录空了再删目录),其余一律不碰。
rm -f /var/www/html/public/images/default-avatar.svg 2>/dev/null || true
rmdir /var/www/html/public/images 2>/dev/null || true
exec "$@"