create-dwy 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/index.js +5 -3
- package/package.json +3 -2
- package/src/searchable-select.js +419 -0
- package/src/skills-install.js +48 -7
- package/src/sync-all.js +17 -25
- package/src/sync.js +38 -32
- package/templates/ai-tools/CHANGELOG.md +76 -0
- package/templates/ai-tools/CLAUDE.md +27 -0
- package/templates/ai-tools/VERSION +1 -0
- package/templates/ai-tools/hook-manifests/hooks.json +7 -0
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
- package/templates/ai-tools/rules/Android/dwy-android-core.md +215 -4
- package/templates/ai-tools/rules/Android/dwy-android-pitfalls.md +52 -0
- package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
- package/templates/ai-tools/rules/Python/dwy-python-backend.md +16 -63
- package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +89 -0
- package/templates/ai-tools/rules/Python/dwy-python-pitfalls.md +49 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-base-lib.md +127 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +22 -86
- package/templates/ai-tools/rules/Vue/dwy-vue-pitfalls.md +66 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +59 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +47 -2
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +511 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +126 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +88 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +195 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +179 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +147 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +230 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +100 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +265 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +94 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +3 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +51 -9
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +58 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-explore/SKILL.md +83 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +14 -9
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +65 -13
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +30 -11
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +3 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +17 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +42 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +22 -10
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +13 -4
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +13 -4
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +12 -2
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +15 -6
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +2 -1
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -0
- package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-ideate/SKILL.md +0 -21
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +0 -113
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +0 -56
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md +0 -117
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +0 -92
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +0 -72
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/templates/prd-coarse.md +0 -61
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +0 -76
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/templates/prd-detailed.md +0 -119
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +0 -104
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +0 -81
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +0 -212
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/templates/impl-plan.md +0 -115
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +0 -105
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +0 -64
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/templates/version-plan.md +0 -72
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 硬件规格识别 + 当前服务资源配置盘点 (只读)
|
|
3
|
+
#
|
|
4
|
+
# 输出 raw data, 由主 Claude 对照 SKILL.md 4.10 的推荐分级表生成
|
|
5
|
+
# "推荐 vs 当前" 对比报告。
|
|
6
|
+
#
|
|
7
|
+
# 检查范围:
|
|
8
|
+
# - 宿主硬件: CPU 核数 / 总内存 / 根盘容量 / swap
|
|
9
|
+
# - 当前容器: mem_limit / cpus / shm_size / 实际占用
|
|
10
|
+
# - Postgres 启动参数: shared_buffers / effective_cache_size / work_mem 等
|
|
11
|
+
# - Redis 启动参数: maxmemory / maxmemory-policy / appendonly / save
|
|
12
|
+
# - compose 文件中的资源声明 (供 grep 比对)
|
|
13
|
+
|
|
14
|
+
TARGET="$1"; shift
|
|
15
|
+
SSH_OPTS=("$@")
|
|
16
|
+
|
|
17
|
+
ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
|
|
18
|
+
echo "--- 宿主硬件规格 ---"
|
|
19
|
+
CPU_CORES=$(nproc 2>/dev/null || echo "?")
|
|
20
|
+
CPU_MODEL=$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/^Model name/ {gsub(/^[ \t]+/,"",$2); print $2; exit}')
|
|
21
|
+
[[ -z "$CPU_MODEL" ]] && CPU_MODEL=$(awk -F: '/model name/ {gsub(/^[ \t]+/,"",$2); print $2; exit}' /proc/cpuinfo 2>/dev/null)
|
|
22
|
+
MEM_TOTAL_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)
|
|
23
|
+
MEM_AVAIL_KB=$(awk '/MemAvailable/ {print $2}' /proc/meminfo 2>/dev/null)
|
|
24
|
+
SWAP_TOTAL_KB=$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null)
|
|
25
|
+
MEM_TOTAL_MB=$(( ${MEM_TOTAL_KB:-0} / 1024 ))
|
|
26
|
+
MEM_AVAIL_MB=$(( ${MEM_AVAIL_KB:-0} / 1024 ))
|
|
27
|
+
SWAP_TOTAL_MB=$(( ${SWAP_TOTAL_KB:-0} / 1024 ))
|
|
28
|
+
|
|
29
|
+
# 推荐分档(对齐 SKILL.md 4.10 的 2/4/8/16 GB 表)
|
|
30
|
+
if [[ $MEM_TOTAL_MB -ge 14000 ]]; then TIER="16GB"
|
|
31
|
+
elif [[ $MEM_TOTAL_MB -ge 7000 ]]; then TIER="8GB"
|
|
32
|
+
elif [[ $MEM_TOTAL_MB -ge 3500 ]]; then TIER="4GB"
|
|
33
|
+
elif [[ $MEM_TOTAL_MB -ge 1500 ]]; then TIER="2GB"
|
|
34
|
+
else TIER="<2GB"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "CPU 核数: $CPU_CORES"
|
|
38
|
+
echo "CPU 型号: ${CPU_MODEL:-unknown}"
|
|
39
|
+
echo "总内存: ${MEM_TOTAL_MB} MB"
|
|
40
|
+
echo "可用内存: ${MEM_AVAIL_MB} MB"
|
|
41
|
+
echo "Swap: ${SWAP_TOTAL_MB} MB"
|
|
42
|
+
echo "推荐分档: ${TIER} (主 Claude 据此查推荐表)"
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "--- 根盘容量 ---"
|
|
46
|
+
df -h / 2>/dev/null
|
|
47
|
+
ROOT_FREE_MB=$(df -BM --output=avail / 2>/dev/null | tail -1 | tr -d ' M')
|
|
48
|
+
echo "根盘可用: ${ROOT_FREE_MB:-?} MB"
|
|
49
|
+
|
|
50
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
51
|
+
echo ""
|
|
52
|
+
echo "[i] Docker 未安装,跳过容器资源检查"
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
echo "--- 当前运行容器资源限制 (raw, 0=无限制) ---"
|
|
58
|
+
SUM_MEM_LIMIT=0
|
|
59
|
+
HAS_UNLIMITED_DB=0
|
|
60
|
+
docker ps -q 2>/dev/null | while read c; do
|
|
61
|
+
name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
|
|
62
|
+
image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
|
|
63
|
+
mem=$(docker inspect -f '{{.HostConfig.Memory}}' "$c" 2>/dev/null)
|
|
64
|
+
cpu_q=$(docker inspect -f '{{.HostConfig.CpuQuota}}' "$c" 2>/dev/null)
|
|
65
|
+
cpu_p=$(docker inspect -f '{{.HostConfig.CpuPeriod}}' "$c" 2>/dev/null)
|
|
66
|
+
shm=$(docker inspect -f '{{.HostConfig.ShmSize}}' "$c" 2>/dev/null)
|
|
67
|
+
pids=$(docker inspect -f '{{.HostConfig.PidsLimit}}' "$c" 2>/dev/null)
|
|
68
|
+
|
|
69
|
+
mem_mb=$(( mem / 1024 / 1024 ))
|
|
70
|
+
shm_mb=$(( shm / 1024 / 1024 ))
|
|
71
|
+
if [[ -n "$cpu_q" && "$cpu_q" != "0" && -n "$cpu_p" && "$cpu_p" != "0" ]]; then
|
|
72
|
+
cpus=$(awk -v q="$cpu_q" -v p="$cpu_p" 'BEGIN{printf "%.2f", q/p}')
|
|
73
|
+
else
|
|
74
|
+
cpus="unlimited"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
printf " %-32s image=%-40s mem_limit=%s cpus=%s shm=%sm pids=%s\n" \
|
|
78
|
+
"$name" "$image" \
|
|
79
|
+
"$([ "$mem" = "0" ] && echo 'unlimited' || echo "${mem_mb}m")" \
|
|
80
|
+
"$cpus" "$shm_mb" "${pids:-unlimited}"
|
|
81
|
+
|
|
82
|
+
# 关键服务无 mem_limit
|
|
83
|
+
if [[ "$mem" == "0" ]] && echo "$image" | grep -qiE 'redis|postgres|mysql|mariadb|mongo|clickhouse|elasticsearch'; then
|
|
84
|
+
echo " [!!] HIGH: $name (image=$image) 未设 mem_limit, 异常时可能耗尽宿主内存"
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "--- 容器实际内存占用 (docker stats 单帧) ---"
|
|
90
|
+
timeout 8 docker stats --no-stream --format \
|
|
91
|
+
'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}' 2>/dev/null \
|
|
92
|
+
|| echo "[i] docker stats 超时或不可用"
|
|
93
|
+
|
|
94
|
+
echo ""
|
|
95
|
+
echo "--- Postgres 启动参数 (从 docker inspect Args 提取) ---"
|
|
96
|
+
PG_FOUND=0
|
|
97
|
+
for c in $(docker ps -q 2>/dev/null); do
|
|
98
|
+
image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
|
|
99
|
+
if echo "$image" | grep -qi "postgres"; then
|
|
100
|
+
PG_FOUND=1
|
|
101
|
+
name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
|
|
102
|
+
# Cmd / Args 拼成空格分隔字符串
|
|
103
|
+
cmd_str=$(docker inspect -f '{{range .Config.Cmd}}{{.}} {{end}}' "$c" 2>/dev/null)
|
|
104
|
+
args_str=$(docker inspect -f '{{range .Args}}{{.}} {{end}}' "$c" 2>/dev/null)
|
|
105
|
+
full="$cmd_str $args_str"
|
|
106
|
+
|
|
107
|
+
echo ""
|
|
108
|
+
echo "[$name] image=$image"
|
|
109
|
+
# 从 -c key=value 提取 Postgres 调优参数
|
|
110
|
+
for key in shared_buffers effective_cache_size work_mem maintenance_work_mem max_connections \
|
|
111
|
+
wal_buffers max_wal_size random_page_cost log_min_duration_statement; do
|
|
112
|
+
val=$(echo "$full" | grep -oE "${key}=[^ ]+" | head -1)
|
|
113
|
+
printf " %-30s %s\n" "$key" "${val:-(默认)}"
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
# 从 docker inspect 取 shm_size (Postgres 关键)
|
|
117
|
+
shm=$(docker inspect -f '{{.HostConfig.ShmSize}}' "$c" 2>/dev/null)
|
|
118
|
+
shm_mb=$(( shm / 1024 / 1024 ))
|
|
119
|
+
if [[ "$shm_mb" -lt 128 ]]; then
|
|
120
|
+
echo " [!] MEDIUM: shm_size=${shm_mb}m 偏低 (大查询可能 'no space left on /dev/shm', 推荐 >=256m)"
|
|
121
|
+
else
|
|
122
|
+
echo " [OK] shm_size=${shm_mb}m"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# 校验 shared_buffers 是否过大
|
|
126
|
+
sb_val=$(echo "$full" | grep -oE 'shared_buffers=[^ ]+' | head -1 | cut -d= -f2)
|
|
127
|
+
if [[ -n "$sb_val" && -n "$MEM_TOTAL_MB" ]]; then
|
|
128
|
+
sb_mb=$(echo "$sb_val" | awk '
|
|
129
|
+
/[Gg][Bb]?$/ { sub(/[Gg][Bb]?$/,""); print $0*1024; next }
|
|
130
|
+
/[Mm][Bb]?$/ { sub(/[Mm][Bb]?$/,""); print $0; next }
|
|
131
|
+
/[Kk][Bb]?$/ { sub(/[Kk][Bb]?$/,""); print $0/1024; next }
|
|
132
|
+
{ print $0/1024/1024 }
|
|
133
|
+
')
|
|
134
|
+
sb_mb=${sb_mb%.*}
|
|
135
|
+
if [[ -n "$sb_mb" && "$sb_mb" -gt $((MEM_TOTAL_MB / 2)) ]]; then
|
|
136
|
+
echo " [!!] HIGH: shared_buffers=${sb_val} 已超过宿主总内存 50%, 易触发 OOM"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
done
|
|
141
|
+
[[ "$PG_FOUND" == "0" ]] && echo "[i] 未发现 Postgres 容器"
|
|
142
|
+
|
|
143
|
+
echo ""
|
|
144
|
+
echo "--- Redis 启动参数 (从 docker inspect Args 提取) ---"
|
|
145
|
+
REDIS_FOUND=0
|
|
146
|
+
for c in $(docker ps -q 2>/dev/null); do
|
|
147
|
+
image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
|
|
148
|
+
if echo "$image" | grep -qi "redis"; then
|
|
149
|
+
REDIS_FOUND=1
|
|
150
|
+
name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
|
|
151
|
+
cmd_str=$(docker inspect -f '{{range .Config.Cmd}}{{.}} {{end}}' "$c" 2>/dev/null)
|
|
152
|
+
args_str=$(docker inspect -f '{{range .Args}}{{.}} {{end}}' "$c" 2>/dev/null)
|
|
153
|
+
full="$cmd_str $args_str"
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
echo "[$name] image=$image"
|
|
157
|
+
# Redis 启动参数格式: --key value
|
|
158
|
+
for key in maxmemory maxmemory-policy appendonly appendfsync save tcp-keepalive timeout; do
|
|
159
|
+
# 把密码字段脱敏
|
|
160
|
+
val=$(echo "$full" | grep -oE -- "--${key} [^ ]+" | head -1 | sed "s/^--${key} //")
|
|
161
|
+
printf " %-25s %s\n" "$key" "${val:-(默认/未设)}"
|
|
162
|
+
done
|
|
163
|
+
|
|
164
|
+
# 是否设了密码 (脱敏只看是否存在)
|
|
165
|
+
if echo "$full" | grep -qE -- '--requirepass [^ ]+'; then
|
|
166
|
+
echo " requirepass <REDACTED, 已设>"
|
|
167
|
+
else
|
|
168
|
+
echo " requirepass (未设, 见 check_db.sh CRITICAL)"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# maxmemory 二元判定
|
|
172
|
+
mm=$(echo "$full" | grep -oE -- '--maxmemory [^ ]+' | head -1 | awk '{print $2}')
|
|
173
|
+
if [[ -z "$mm" || "$mm" == "0" ]]; then
|
|
174
|
+
echo " [!!!] CRITICAL: maxmemory 未设置或为 0, Redis 可能耗尽宿主内存"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# appendonly + 检查 volume 是否挂载
|
|
178
|
+
if ! echo "$full" | grep -qE -- '--appendonly +yes'; then
|
|
179
|
+
echo " [!] MEDIUM: 未启用 AOF, 重启会丢最近 RDB save 间隔内的数据"
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
done
|
|
183
|
+
[[ "$REDIS_FOUND" == "0" ]] && echo "[i] 未发现 Redis 容器"
|
|
184
|
+
|
|
185
|
+
echo ""
|
|
186
|
+
echo "--- compose 文件中的资源声明 (供人工核对) ---"
|
|
187
|
+
COMPOSE_FILES=$(sudo -n find /home /opt /srv /root -maxdepth 5 \
|
|
188
|
+
\( -name "docker-compose*.yml" -o -name "compose*.yml" \) 2>/dev/null \
|
|
189
|
+
| grep -vE '\.bak|\.old' | head -10)
|
|
190
|
+
[[ -z "$COMPOSE_FILES" ]] && COMPOSE_FILES=$(find /home /opt /srv /root -maxdepth 5 \
|
|
191
|
+
\( -name "docker-compose*.yml" -o -name "compose*.yml" \) 2>/dev/null \
|
|
192
|
+
| grep -vE '\.bak|\.old' | head -10)
|
|
193
|
+
|
|
194
|
+
for f in $COMPOSE_FILES; do
|
|
195
|
+
echo ""
|
|
196
|
+
echo "[$f]"
|
|
197
|
+
grep -nE '^[[:space:]]+(mem_limit|mem_reservation|cpus|shm_size|memory|cpu_quota|--maxmemory|--maxmemory-policy|--appendonly|shared_buffers|effective_cache_size|work_mem)[[:space:]:]' \
|
|
198
|
+
"$f" 2>/dev/null | head -30 \
|
|
199
|
+
|| echo " (无资源声明)"
|
|
200
|
+
done
|
|
201
|
+
|
|
202
|
+
echo ""
|
|
203
|
+
echo "--- 日志兜底推荐档 (基于根盘 + 容器规模, 对照 SKILL.md 4.10) ---"
|
|
204
|
+
ROOT_TOTAL_MB=$(df -BM --output=size / 2>/dev/null | tail -1 | tr -d ' M')
|
|
205
|
+
ROOT_TOTAL_GB=$(( ${ROOT_TOTAL_MB:-0} / 1024 ))
|
|
206
|
+
RUNNING_COUNT=$(docker ps -q 2>/dev/null | wc -l | tr -d ' ')
|
|
207
|
+
echo "根盘容量: ${ROOT_TOTAL_GB} GB"
|
|
208
|
+
echo "运行容器数: ${RUNNING_COUNT}"
|
|
209
|
+
|
|
210
|
+
# 推荐档分级
|
|
211
|
+
if [[ $ROOT_TOTAL_GB -lt 50 ]]; then REC_TIER="入门"; REC_SIZE="10m"; REC_FILE="3"
|
|
212
|
+
elif [[ $ROOT_TOTAL_GB -lt 150 ]]; then
|
|
213
|
+
REC_TIER="标准"
|
|
214
|
+
if [[ $RUNNING_COUNT -gt 5 ]]; then REC_SIZE="20m"; REC_FILE="5"
|
|
215
|
+
else REC_SIZE="50m"; REC_FILE="5"; fi
|
|
216
|
+
else REC_TIER="大型"; REC_SIZE="100m"; REC_FILE="5"
|
|
217
|
+
fi
|
|
218
|
+
echo "推荐档: ${REC_TIER}"
|
|
219
|
+
echo "推荐 daemon log-opts: max-size=${REC_SIZE}, max-file=${REC_FILE}, compress=true"
|
|
220
|
+
|
|
221
|
+
# 拿当前 daemon.json log-opts (复用 check_docker.sh 的逻辑)
|
|
222
|
+
DAEMON_JSON=$(sudo -n cat /etc/docker/daemon.json 2>/dev/null \
|
|
223
|
+
|| cat /etc/docker/daemon.json 2>/dev/null || echo "")
|
|
224
|
+
CUR_SIZE=$(echo "$DAEMON_JSON" | grep -oE '"max-size"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
|
225
|
+
| head -1 | grep -oE '"[^"]+"$' | tr -d '"')
|
|
226
|
+
CUR_FILE=$(echo "$DAEMON_JSON" | grep -oE '"max-file"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
|
227
|
+
| head -1 | grep -oE '"[^"]+"$' | tr -d '"')
|
|
228
|
+
echo "当前 daemon.json: max-size=${CUR_SIZE:-(未配置)}, max-file=${CUR_FILE:-(未配置)}"
|
|
229
|
+
|
|
230
|
+
# 简单对比标记
|
|
231
|
+
if [[ -z "$CUR_SIZE" ]]; then
|
|
232
|
+
echo "[!!!] CRITICAL: daemon 无 log-opts.max-size, 长跑容器日志会无限增长"
|
|
233
|
+
elif [[ "$CUR_SIZE" == "$REC_SIZE" ]]; then
|
|
234
|
+
echo "[OK] max-size 与推荐档一致"
|
|
235
|
+
else
|
|
236
|
+
echo "[i] 当前 max-size=${CUR_SIZE} 与推荐档 ${REC_SIZE} 不同 (主 Claude 据 SKILL.md 4.10 判定是否偏离 50%)"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# 估算所有容器日志合计 quota = max-size × max-file × 容器数, 看占根盘比
|
|
240
|
+
# 不依赖 bc(部分镜像/最小系统不带), 用 awk 解析单位
|
|
241
|
+
if [[ -n "$CUR_SIZE" ]]; then
|
|
242
|
+
size_mb=$(echo "$CUR_SIZE" | awk '
|
|
243
|
+
BEGIN{IGNORECASE=1}
|
|
244
|
+
/[gG]$/ { sub(/[gG]$/,""); print int($0*1024); exit }
|
|
245
|
+
/[mM]$/ { sub(/[mM]$/,""); print int($0); exit }
|
|
246
|
+
/[kK]$/ { sub(/[kK]$/,""); print int($0/1024); exit }
|
|
247
|
+
{ print int($0); exit }
|
|
248
|
+
')
|
|
249
|
+
file_n=${CUR_FILE:-1}
|
|
250
|
+
total_quota_mb=$(( ${size_mb:-0} * file_n * RUNNING_COUNT ))
|
|
251
|
+
if [[ "$ROOT_TOTAL_MB" -gt 0 ]]; then
|
|
252
|
+
pct=$(( total_quota_mb * 100 / ROOT_TOTAL_MB ))
|
|
253
|
+
echo "所有容器最大日志 quota 合计: ${total_quota_mb} MB (占根盘 ${pct}%, 期望 < 5%)"
|
|
254
|
+
if [[ "$pct" -gt 5 ]]; then
|
|
255
|
+
echo "[!!] HIGH: 日志 quota 合计 ${pct}% 已超根盘 5% 上限"
|
|
256
|
+
fi
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
echo ""
|
|
261
|
+
echo "--- 资源占比汇总 (供 Claude 校验 65% 上限规则) ---"
|
|
262
|
+
echo "宿主总内存: ${MEM_TOTAL_MB} MB"
|
|
263
|
+
TOTAL_LIMIT_MB=$(docker ps -q 2>/dev/null \
|
|
264
|
+
| xargs -I{} docker inspect -f '{{.HostConfig.Memory}}' {} 2>/dev/null \
|
|
265
|
+
| awk 'BEGIN{s=0} {s+=$1} END{print int(s/1024/1024)}')
|
|
266
|
+
echo "所有容器 mem_limit 合计: ${TOTAL_LIMIT_MB} MB"
|
|
267
|
+
if [[ "$MEM_TOTAL_MB" -gt 0 ]]; then
|
|
268
|
+
pct=$(( TOTAL_LIMIT_MB * 100 / MEM_TOTAL_MB ))
|
|
269
|
+
echo "占宿主总内存: ${pct}%"
|
|
270
|
+
if [[ "$pct" -gt 75 ]]; then
|
|
271
|
+
echo "[!!] HIGH: 容器硬限合计 > 75% 宿主内存, OS/nginx/监控 留 buffer 不足"
|
|
272
|
+
elif [[ "$pct" -lt 30 ]]; then
|
|
273
|
+
echo "[i] INFO: 容器硬限合计 < 30%, 可能资源利用不足"
|
|
274
|
+
else
|
|
275
|
+
echo "[OK] 容器硬限合计在 30%-75% 区间"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
REMOTE
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 数据库 (PostgreSQL / Redis) 暴露检查
|
|
3
|
+
TARGET="$1"; shift
|
|
4
|
+
SSH_OPTS=("$@")
|
|
5
|
+
|
|
6
|
+
ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
|
|
7
|
+
echo "########## PostgreSQL ##########"
|
|
8
|
+
if ! command -v psql >/dev/null 2>&1 && ! pgrep -f "postgres" >/dev/null 2>&1; then
|
|
9
|
+
echo "[i] PostgreSQL 未安装/未运行,跳过"
|
|
10
|
+
else
|
|
11
|
+
echo "--- 进程 ---"
|
|
12
|
+
pgrep -af postgres | head -5
|
|
13
|
+
|
|
14
|
+
echo ""
|
|
15
|
+
echo "--- 监听端口 (期望: 仅 127.0.0.1 或内网) ---"
|
|
16
|
+
(ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(5432|5433)" || echo "[i] 未发现 5432/5433 监听"
|
|
17
|
+
|
|
18
|
+
echo ""
|
|
19
|
+
echo "--- 配置文件位置 ---"
|
|
20
|
+
PG_CONF=$(sudo -n -u postgres psql -tAc "SHOW config_file" 2>/dev/null || \
|
|
21
|
+
find /etc/postgresql /var/lib/pgsql /var/lib/postgresql -name postgresql.conf 2>/dev/null | head -1)
|
|
22
|
+
PG_HBA=$(sudo -n -u postgres psql -tAc "SHOW hba_file" 2>/dev/null || \
|
|
23
|
+
find /etc/postgresql /var/lib/pgsql /var/lib/postgresql -name pg_hba.conf 2>/dev/null | head -1)
|
|
24
|
+
echo "postgresql.conf: ${PG_CONF:-未找到}"
|
|
25
|
+
echo "pg_hba.conf: ${PG_HBA:-未找到}"
|
|
26
|
+
|
|
27
|
+
echo ""
|
|
28
|
+
echo "--- postgresql.conf 关键项 ---"
|
|
29
|
+
if [[ -r "$PG_CONF" ]]; then
|
|
30
|
+
grep -E "^[[:space:]]*(listen_addresses|port|password_encryption|ssl|log_connections|log_disconnections|log_min_duration_statement)[[:space:]]*=" "$PG_CONF" 2>/dev/null
|
|
31
|
+
else
|
|
32
|
+
sudo -n cat "$PG_CONF" 2>/dev/null | grep -E "^[[:space:]]*(listen_addresses|password_encryption|ssl|log_connections)" || echo "[!] 配置不可读"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
echo "--- pg_hba.conf 认证方式 (期望: 无 trust) ---"
|
|
37
|
+
if [[ -r "$PG_HBA" ]]; then
|
|
38
|
+
grep -vE "^[[:space:]]*#|^[[:space:]]*$" "$PG_HBA" 2>/dev/null
|
|
39
|
+
else
|
|
40
|
+
sudo -n cat "$PG_HBA" 2>/dev/null | grep -vE "^[[:space:]]*#|^[[:space:]]*$" || echo "[!] pg_hba.conf 不可读"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo ""
|
|
44
|
+
echo "--- PG 版本 & 数据库列表 ---"
|
|
45
|
+
PG_VERSION_RAW=$(sudo -n -u postgres psql -tAc "SELECT version()" 2>/dev/null | head -1 || true)
|
|
46
|
+
if [[ -n "$PG_VERSION_RAW" ]]; then
|
|
47
|
+
echo "$PG_VERSION_RAW"
|
|
48
|
+
PG_VERSION=$(echo "$PG_VERSION_RAW" | grep -oE 'PostgreSQL[[:space:]]+[0-9]+(\.[0-9]+){0,2}' | awk '{print $2}' | head -1)
|
|
49
|
+
echo "postgresql_version_normalized=${PG_VERSION:-unknown}"
|
|
50
|
+
echo "official_doc_pg_runtime=https://www.postgresql.org/docs/current/runtime-config-connection.html"
|
|
51
|
+
if [[ -n "$PG_VERSION" ]]; then
|
|
52
|
+
echo "search_hint_1=postgresql ${PG_VERSION} CVE"
|
|
53
|
+
echo "search_hint_2=postgresql ${PG_VERSION} security advisory"
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
echo "[i] 无法连接 PG"
|
|
57
|
+
fi
|
|
58
|
+
sudo -n -u postgres psql -tAc "SELECT datname FROM pg_database WHERE datistemplate=false" 2>/dev/null || true
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "########## Redis ##########"
|
|
63
|
+
if ! command -v redis-cli >/dev/null 2>&1 && ! pgrep -f "redis-server" >/dev/null 2>&1; then
|
|
64
|
+
echo "[i] Redis 未安装/未运行,跳过"
|
|
65
|
+
else
|
|
66
|
+
echo "--- 进程 ---"
|
|
67
|
+
pgrep -af redis-server | head -5
|
|
68
|
+
|
|
69
|
+
echo ""
|
|
70
|
+
echo "--- 监听端口 (期望: 仅 127.0.0.1 或内网) ---"
|
|
71
|
+
(ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(6379|6380)" || echo "[i] 未发现 6379/6380 监听"
|
|
72
|
+
|
|
73
|
+
echo ""
|
|
74
|
+
echo "--- redis.conf 位置与关键项 ---"
|
|
75
|
+
REDIS_CONF=$(ps -eo cmd | grep "[r]edis-server" | grep -oE "/[^ ]+\.conf" | head -1)
|
|
76
|
+
REDIS_CONF=${REDIS_CONF:-/etc/redis/redis.conf}
|
|
77
|
+
echo "redis.conf: $REDIS_CONF"
|
|
78
|
+
if [[ -r "$REDIS_CONF" ]]; then
|
|
79
|
+
grep -E "^[[:space:]]*(bind|port|protected-mode|requirepass|rename-command|tls-port|maxmemory|maxmemory-policy)" "$REDIS_CONF" 2>/dev/null \
|
|
80
|
+
| sed -E 's/(requirepass[[:space:]]+).*/\1<REDACTED>/'
|
|
81
|
+
else
|
|
82
|
+
sudo -n grep -E "^[[:space:]]*(bind|port|protected-mode|requirepass|rename-command|tls-port)" "$REDIS_CONF" 2>/dev/null \
|
|
83
|
+
| sed -E 's/(requirepass[[:space:]]+).*/\1<REDACTED>/' || echo "[!] redis.conf 不可读"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo ""
|
|
87
|
+
echo "--- maxmemory 判定 (运行时取值,优于 conf 文件) ---"
|
|
88
|
+
MAXMEM=$(timeout 3 redis-cli -h 127.0.0.1 CONFIG GET maxmemory 2>/dev/null | tail -1)
|
|
89
|
+
MAXMEM_POLICY=$(timeout 3 redis-cli -h 127.0.0.1 CONFIG GET maxmemory-policy 2>/dev/null | tail -1)
|
|
90
|
+
if [[ -z "$MAXMEM" ]]; then
|
|
91
|
+
echo "[i] redis-cli 无密码连接失败,从 conf 文件读取"
|
|
92
|
+
MAXMEM=$(grep -E "^[[:space:]]*maxmemory[[:space:]]" "$REDIS_CONF" 2>/dev/null | awk '{print $2}' | tail -1)
|
|
93
|
+
MAXMEM_POLICY=$(grep -E "^[[:space:]]*maxmemory-policy[[:space:]]" "$REDIS_CONF" 2>/dev/null | awk '{print $2}' | tail -1)
|
|
94
|
+
fi
|
|
95
|
+
echo "maxmemory: ${MAXMEM:-(未取得)}"
|
|
96
|
+
echo "maxmemory-policy: ${MAXMEM_POLICY:-(未取得)}"
|
|
97
|
+
|
|
98
|
+
TOTAL_MEM=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)
|
|
99
|
+
if [[ -n "$TOTAL_MEM" ]]; then
|
|
100
|
+
TOTAL_MEM_BYTES=$((TOTAL_MEM * 1024))
|
|
101
|
+
echo "服务器总内存: $((TOTAL_MEM / 1024)) MB ($TOTAL_MEM_BYTES bytes)"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [[ "$MAXMEM" == "0" || -z "$MAXMEM" ]]; then
|
|
105
|
+
echo "[!!!] HIGH: maxmemory=0(无上限),Redis 可能耗尽服务器内存触发 OOM kill"
|
|
106
|
+
echo " 建议: 设置为服务器物理内存的 50%-70% (CONFIG SET maxmemory <bytes>)"
|
|
107
|
+
else
|
|
108
|
+
echo "[OK] maxmemory 已设置"
|
|
109
|
+
if [[ "$MAXMEM_POLICY" == "noeviction" ]]; then
|
|
110
|
+
echo "[!] INFO: maxmemory-policy=noeviction,达到上限时拒写但不淘汰,业务方需感知"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo ""
|
|
115
|
+
echo "--- 尝试无密码连接 (如能成功 → CRITICAL) ---"
|
|
116
|
+
if timeout 3 redis-cli -h 127.0.0.1 ping 2>/dev/null | grep -q PONG; then
|
|
117
|
+
echo "[!!!] CRITICAL: Redis 127.0.0.1 无密码可连!"
|
|
118
|
+
else
|
|
119
|
+
echo "[OK] 无密码连接被拒绝"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
echo ""
|
|
123
|
+
echo "--- Redis INFO server (需 auth, 跳过若失败) ---"
|
|
124
|
+
REDIS_INFO=$(timeout 3 redis-cli -h 127.0.0.1 INFO server 2>/dev/null || true)
|
|
125
|
+
if [[ -n "$REDIS_INFO" ]]; then
|
|
126
|
+
echo "$REDIS_INFO" | grep -E "redis_version|os|arch_bits" | head -5
|
|
127
|
+
REDIS_VERSION=$(echo "$REDIS_INFO" | sed -n 's/^redis_version://p' | head -1)
|
|
128
|
+
echo "redis_version_normalized=${REDIS_VERSION:-unknown}"
|
|
129
|
+
echo "official_doc_redis_security=https://redis.io/docs/latest/operate/oss_and_stack/management/security/"
|
|
130
|
+
if [[ -n "$REDIS_VERSION" ]]; then
|
|
131
|
+
echo "search_hint_redis_1=redis ${REDIS_VERSION} CVE"
|
|
132
|
+
echo "search_hint_redis_2=redis ${REDIS_VERSION} security advisory"
|
|
133
|
+
fi
|
|
134
|
+
else
|
|
135
|
+
echo "[i] 需要密码或无法连接"
|
|
136
|
+
echo "official_doc_redis_security=https://redis.io/docs/latest/operate/oss_and_stack/management/security/"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo "########## 公网可达性自测 ##########"
|
|
142
|
+
PUB_IP=$(curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null)
|
|
143
|
+
echo "公网 IP (best effort): ${PUB_IP:-unknown}"
|
|
144
|
+
if [[ -n "$PUB_IP" ]]; then
|
|
145
|
+
echo "[i] 注意: 真实公网可达性需从外网测试,此处仅本机视角"
|
|
146
|
+
fi
|
|
147
|
+
REMOTE
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Docker 安全检查
|
|
3
|
+
TARGET="$1"; shift
|
|
4
|
+
SSH_OPTS=("$@")
|
|
5
|
+
|
|
6
|
+
ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
|
|
7
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
8
|
+
echo "[i] Docker 未安装,跳过"
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "--- docker version ---"
|
|
13
|
+
docker version --format '{{.Server.Version}} (client: {{.Client.Version}})' 2>/dev/null || docker --version
|
|
14
|
+
DOCKER_SERVER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null || docker --version | grep -oE '[0-9]+(\.[0-9]+){1,3}' | head -1)
|
|
15
|
+
echo "docker_server_version_normalized=${DOCKER_SERVER_VERSION:-unknown}"
|
|
16
|
+
echo "official_doc_docker_security=https://docs.docker.com/engine/security/"
|
|
17
|
+
echo "official_doc_docker_remote_access=https://docs.docker.com/engine/daemon/remote-access/"
|
|
18
|
+
echo "official_doc_docker_logging=https://docs.docker.com/engine/logging/configure/"
|
|
19
|
+
echo "official_doc_docker_owasp=https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html"
|
|
20
|
+
if [[ -n "$DOCKER_SERVER_VERSION" ]]; then
|
|
21
|
+
echo "search_hint_1=docker engine ${DOCKER_SERVER_VERSION} CVE"
|
|
22
|
+
echo "search_hint_2=docker ${DOCKER_SERVER_VERSION} security advisory"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
echo ""
|
|
26
|
+
echo "--- docker daemon 监听 ---"
|
|
27
|
+
(ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(2375|2376)" \
|
|
28
|
+
&& echo "[!!!] CRITICAL: Docker daemon 暴露 TCP 端口" \
|
|
29
|
+
|| echo "[OK] Docker daemon 未暴露 TCP 端口"
|
|
30
|
+
|
|
31
|
+
echo ""
|
|
32
|
+
echo "--- 运行中容器列表 ---"
|
|
33
|
+
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null
|
|
34
|
+
|
|
35
|
+
echo ""
|
|
36
|
+
echo "--- 容器端口绑定 (期望: 内部服务不暴露 0.0.0.0) ---"
|
|
37
|
+
docker ps --format "{{.Names}}|{{.Ports}}" 2>/dev/null | while IFS='|' read -r name ports; do
|
|
38
|
+
if echo "$ports" | grep -qE "0\.0\.0\.0:(5432|6379|3306|27017|9200|11211)"; then
|
|
39
|
+
echo " [!!!] $name 暴露内部服务端口到 0.0.0.0: $ports"
|
|
40
|
+
else
|
|
41
|
+
echo " [OK] $name $ports"
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
echo ""
|
|
46
|
+
echo "--- 镜像 tag 分级检查 (生产期望: 固定到具体 patch) ---"
|
|
47
|
+
# 浮动 tag 黑名单 (无版本号或仅描述类的)
|
|
48
|
+
FLOATING_TAGS='^(latest|stable|mainline|edge|current|alpine|slim|bookworm|bullseye|buster|jammy|focal|noble|nightly)$'
|
|
49
|
+
docker ps --format "{{.Names}}|{{.Image}}" 2>/dev/null | while IFS='|' read -r name image; do
|
|
50
|
+
# 拆 image 与 tag (注意 image 可能含 registry 域名如 ghcr.io/foo/bar:tag)
|
|
51
|
+
if [[ "$image" == *"@sha256:"* ]]; then
|
|
52
|
+
# tag@digest 形式 → digest 钉死, 最严格
|
|
53
|
+
echo " [OK+] $name digest 钉死: $image"
|
|
54
|
+
continue
|
|
55
|
+
fi
|
|
56
|
+
# 取最后一个冒号后内容当 tag (port 形式 registry:5000/foo:tag 也只取最后)
|
|
57
|
+
base="${image%:*}"
|
|
58
|
+
tag="${image##*:}"
|
|
59
|
+
if [[ "$base" == "$image" ]]; then
|
|
60
|
+
# 没有冒号 → 无 tag (等同 :latest)
|
|
61
|
+
echo " [!!!] CRITICAL $name $image 省略 tag (等同 :latest), 不可重现"
|
|
62
|
+
continue
|
|
63
|
+
fi
|
|
64
|
+
# 提取 tag 起始的语义版本前缀, 支持 "X" / "X.Y" / "X.Y.Z" / "X.Y.Z-suffix"
|
|
65
|
+
ver_prefix=$(echo "$tag" | grep -oE '^[0-9]+(\.[0-9]+){0,3}')
|
|
66
|
+
if [[ -n "$ver_prefix" ]]; then
|
|
67
|
+
seg_count=$(echo "$ver_prefix" | awk -F. '{print NF}')
|
|
68
|
+
else
|
|
69
|
+
seg_count=0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [[ "$tag" == "latest" ]]; then
|
|
73
|
+
echo " [!!!] CRITICAL $name $image 使用 :latest"
|
|
74
|
+
elif echo "$tag" | grep -qE "$FLOATING_TAGS"; then
|
|
75
|
+
echo " [!!] HIGH $name $image 浮动 tag :$tag (随上游漂移)"
|
|
76
|
+
elif [[ "$seg_count" == 0 ]]; then
|
|
77
|
+
# 非数字开头, 可能是 commit hash / build id / 自定义 tag
|
|
78
|
+
echo " [i] $name $image 非 semver tag :$tag (确认是否 immutable; 如自建镜像走构建 id 视为 OK)"
|
|
79
|
+
elif [[ "$seg_count" == 1 ]]; then
|
|
80
|
+
echo " [!!] HIGH $name $image 仅 major :$tag, 应固定到 minor 或 patch"
|
|
81
|
+
elif [[ "$seg_count" == 2 ]]; then
|
|
82
|
+
# 两段: postgres/mysql/mariadb/alpine/debian/ubuntu 已是 patch; nginx/redis/node 等三段 semver 则需升 patch
|
|
83
|
+
echo " [i] $name $image 两段 :$ver_prefix (postgres/mysql/alpine 视为 patch OK; redis/nginx/node 等需固定到三段)"
|
|
84
|
+
else
|
|
85
|
+
# 三段及以上 (含 -alpine 等后缀)
|
|
86
|
+
echo " [OK] $name $image"
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
89
|
+
echo ""
|
|
90
|
+
echo "[i] 修复:对每条 [!!!] / [!!] / [!] 的镜像,参考 Docker Hub 选稳定 patch 版本固定"
|
|
91
|
+
|
|
92
|
+
echo ""
|
|
93
|
+
echo "--- 容器是否以 root 运行 ---"
|
|
94
|
+
for c in $(docker ps -q 2>/dev/null); do
|
|
95
|
+
name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
|
|
96
|
+
user=$(docker inspect -f '{{.Config.User}}' "$c")
|
|
97
|
+
user="${user:-root}"
|
|
98
|
+
if [[ "$user" == "root" || "$user" == "0" || "$user" == "0:0" ]]; then
|
|
99
|
+
echo " [!] $name 以 root 运行 (User=${user})"
|
|
100
|
+
else
|
|
101
|
+
echo " [OK] $name user=$user"
|
|
102
|
+
fi
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
echo ""
|
|
106
|
+
echo "--- 特权容器检查 ---"
|
|
107
|
+
for c in $(docker ps -q 2>/dev/null); do
|
|
108
|
+
name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
|
|
109
|
+
priv=$(docker inspect -f '{{.HostConfig.Privileged}}' "$c")
|
|
110
|
+
caps=$(docker inspect -f '{{.HostConfig.CapAdd}}' "$c")
|
|
111
|
+
if [[ "$priv" == "true" ]]; then
|
|
112
|
+
echo " [!!!] $name --privileged"
|
|
113
|
+
fi
|
|
114
|
+
if [[ "$caps" != "[]" && "$caps" != "<no value>" ]]; then
|
|
115
|
+
echo " [!] $name 额外 capabilities: $caps"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
echo "--- docker.sock 挂载检查 ---"
|
|
121
|
+
for c in $(docker ps -q 2>/dev/null); do
|
|
122
|
+
name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
|
|
123
|
+
mounts=$(docker inspect -f '{{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}' "$c")
|
|
124
|
+
if echo "$mounts" | grep -q "docker.sock"; then
|
|
125
|
+
echo " [!!!] $name 挂载了 docker.sock: $mounts"
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
echo ""
|
|
130
|
+
echo "--- docker daemon 配置 ---"
|
|
131
|
+
DAEMON_JSON=$(sudo -n cat /etc/docker/daemon.json 2>/dev/null || cat /etc/docker/daemon.json 2>/dev/null || echo "")
|
|
132
|
+
if [[ -z "$DAEMON_JSON" ]]; then
|
|
133
|
+
echo "[i] 无 daemon.json 或不可读"
|
|
134
|
+
else
|
|
135
|
+
echo "$DAEMON_JSON"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
echo ""
|
|
139
|
+
echo "--- 容器 RestartPolicy (服务器重启后能否自动起来) ---"
|
|
140
|
+
for c in $(docker ps -aq 2>/dev/null); do
|
|
141
|
+
name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
|
|
142
|
+
policy=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$c" 2>/dev/null)
|
|
143
|
+
state=$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null)
|
|
144
|
+
case "$policy" in
|
|
145
|
+
always|unless-stopped)
|
|
146
|
+
echo " [OK] $name state=$state policy=$policy"
|
|
147
|
+
;;
|
|
148
|
+
on-failure)
|
|
149
|
+
echo " [!] $name state=$state policy=on-failure (手动 stop / OOM 后不会重启)"
|
|
150
|
+
;;
|
|
151
|
+
no|"")
|
|
152
|
+
[[ "$state" == "running" ]] \
|
|
153
|
+
&& echo " [!!!] CRITICAL: $name state=running policy=${policy:-no} (服务器重启后不会自动启动)" \
|
|
154
|
+
|| echo " [i] $name state=$state policy=${policy:-no}"
|
|
155
|
+
;;
|
|
156
|
+
*)
|
|
157
|
+
echo " [?] $name policy=$policy (未知)"
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
160
|
+
done
|
|
161
|
+
|
|
162
|
+
echo ""
|
|
163
|
+
echo "--- 日志驱动配置 (防容器日志写满磁盘) ---"
|
|
164
|
+
LOG_DRIVER=$(echo "$DAEMON_JSON" | grep -oE '"log-driver"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
|
|
165
|
+
LOG_MAX_SIZE=$(echo "$DAEMON_JSON" | grep -oE '"max-size"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
|
|
166
|
+
LOG_MAX_FILE=$(echo "$DAEMON_JSON" | grep -oE '"max-file"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
|
|
167
|
+
echo "log-driver: ${LOG_DRIVER:-(默认 json-file)}"
|
|
168
|
+
echo "max-size: ${LOG_MAX_SIZE:-(未配置, 单容器日志可无限增长)}"
|
|
169
|
+
echo "max-file: ${LOG_MAX_FILE:-(未配置)}"
|
|
170
|
+
if [[ -z "$LOG_MAX_SIZE" ]]; then
|
|
171
|
+
echo "[!!] HIGH: 未限制单容器日志大小,长跑容器可能写满磁盘"
|
|
172
|
+
echo " 建议: daemon.json 加 \"log-opts\": {\"max-size\": \"100m\", \"max-file\": \"3\"}"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
echo ""
|
|
176
|
+
echo "--- docker compose 文件位置 (用于人工核查) ---"
|
|
177
|
+
sudo -n find /home /opt /srv /root -maxdepth 5 \( -name "docker-compose*.yml" -o -name "compose.yml" \) 2>/dev/null | head -10
|
|
178
|
+
|
|
179
|
+
echo ""
|
|
180
|
+
echo "--- 镜像源加速检查 ---"
|
|
181
|
+
# daemon.json registry-mirrors (docker.io 拉取较慢时配 mirror 加速, 按服务器所在地区选择)
|
|
182
|
+
MIRRORS=$(echo "$DAEMON_JSON" | grep -oE '"registry-mirrors"[[:space:]]*:[[:space:]]*\[[^]]*\]' | head -1)
|
|
183
|
+
if [[ -n "$MIRRORS" ]]; then
|
|
184
|
+
echo "daemon registry-mirrors:"
|
|
185
|
+
echo " $MIRRORS"
|
|
186
|
+
else
|
|
187
|
+
echo "[i] daemon.json 未配 registry-mirrors (docker.io 拉取较慢时, 建议按服务器所在地区配置合适的 mirror)"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# 运行中容器使用的 registry 来源 (registry-mirrors 仅对 docker.io 生效)
|
|
191
|
+
echo ""
|
|
192
|
+
echo "--- 运行容器 image registry 来源 (registry-mirrors 仅对 docker.io 生效) ---"
|
|
193
|
+
docker ps --format "{{.Names}}|{{.Image}}" 2>/dev/null | while IFS='|' read -r name image; do
|
|
194
|
+
# 拆 registry: 必须含 "/" 且首段含 "." (域名) 或 ":" (port) 才算独立 registry
|
|
195
|
+
# 否则视为隐式 docker.io (单段名 redis 或 namespace/name 格式 bitnami/redis)
|
|
196
|
+
if [[ "$image" != */* ]]; then
|
|
197
|
+
registry="docker.io"
|
|
198
|
+
else
|
|
199
|
+
first_seg="${image%%/*}"
|
|
200
|
+
if [[ "$first_seg" == *.* || "$first_seg" == *:* ]]; then
|
|
201
|
+
registry="$first_seg"
|
|
202
|
+
else
|
|
203
|
+
registry="docker.io"
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
case "$registry" in
|
|
208
|
+
docker.io)
|
|
209
|
+
if [[ -z "$MIRRORS" ]]; then
|
|
210
|
+
tag="[i] 未配 registry-mirrors, docker.io 拉取可能较慢"
|
|
211
|
+
else
|
|
212
|
+
tag="OK 走 daemon registry-mirrors 加速"
|
|
213
|
+
fi
|
|
214
|
+
;;
|
|
215
|
+
gcr.io|ghcr.io|k8s.gcr.io|registry.k8s.io|quay.io|mcr.microsoft.com|nvcr.io|docker.elastic.co)
|
|
216
|
+
tag="[i] 境外 registry, registry-mirrors 不生效, 拉取加速需自行解决"
|
|
217
|
+
;;
|
|
218
|
+
*)
|
|
219
|
+
tag="[i] 自建/私有 registry"
|
|
220
|
+
;;
|
|
221
|
+
esac
|
|
222
|
+
printf " %-32s registry=%-32s %s\n" "$name" "$registry" "$tag"
|
|
223
|
+
done
|
|
224
|
+
|
|
225
|
+
echo ""
|
|
226
|
+
echo "[i] 优化建议:"
|
|
227
|
+
echo " 1. docker.io 拉取慢 → daemon.json 配 registry-mirrors (按所在地区选择)"
|
|
228
|
+
echo " 2. 境外 registry → registry-mirrors 不生效, 需自行解决拉取加速"
|
|
229
|
+
echo " 3. 镜像版本不固定 → 参考 Docker Hub 选稳定 patch 版本固定"
|
|
230
|
+
REMOTE
|