背景
先交代环境,因为后面每个坑都和这些设定有关:
| 项 | 情况 |
|---|---|
| 宿主机 | CM311-1A 电视盒子,Amlogic S905L3A,2G 内存 |
| 系统 | Armbian(Debian 系) |
| 青龙 | Docker 部署,镜像自带 otask.sh / task 那套启动链 |
| 脚本 | JS 与 Python 混用,Python 脚本依赖仓库里的 notify.py 等公共模块 |
| 通知 | Bark 推送 |
青龙本身对环境变量的管理有好几层,加上它启动 Python 任务的方式比较特殊,几个坑叠加起来,报错信息会把你往完全错误的方向带。以下按「排查成本从高到低」排列,第一个坑我耗的时间最长。
坑一:No module named ‘notify’,第一反应 pip install 就错了
报错长这样:
Traceback (most recent call last): File "/ql/data/scripts/xxx/yyy.py", line 5, in <module> import notify ModuleNotFoundError: No module named 'notify'90% 的教程会告诉你:依赖没装,pip install一下。但对notify这个模块,装包是死路——PyPI 上根本没有(或不是你想要的那个)。notify.py通常是脚本仓库自带的文件,和脚本在同一个目录,靠 Python 的路径搜索机制被找到。
所以真正的问题是:任务进程的模块搜索路径(sys.path)里没有脚本所在目录。
根因:otask.sh 重写了 PYTHONPATH,且顺序把你自己的补丁顶掉了
翻青龙容器里的/ql/shell/otask.sh,对.py任务有类似这样的逻辑:
exportPYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"其中dir_preload是/ql/shell/preload,排在最前面。而这个目录里躺着一个青龙官方的sitecustomize.py,末尾有一句模块级的import task_before。
关键在于 Python 解释器启动时的规则:site模块会按顺序尝试导入sitecustomize,只导入第一个找到的。于是发生了这样的事:
- 我按常规思路,把补丁做成了
sitecustomize.py(往 sys.path 里 append 脚本目录),放在依赖缓存目录的 site-packages 里 - 任务启动时 PYTHONPATH 里
/ql/shell/preload排最前,官方的sitecustomize.py先被找到并导入 - 同名模块只有一个能生效 → 我的
sitecustomize.py永远不会执行 task_before钩子执行失败或者啥也没干,脚本目录照样不在 sys.path 里 →import notify失败
也就是说,报错说「缺 notify」,实际缺的是一次正常的路径注入。报错信息和真实原因隔了一层,这是整个排查里最费时间的地方。
正确修法
两步,都不需要重启容器:
第一步:用官方钩子task_before.py。青龙支持/ql/data/config/task_before.py,会在任务前被调用。往里写:
importsys sys.path.append("/ql/data/scripts")第二步:把补丁文件改名。如果你在依赖目录放过自己的sitecustomize.py,把它改名成usercustomize.py。site模块先 import sitecustomize 再 import usercustomize,两者可以共存,这样你的路径注入在所有进程(包括不走 otask.sh 的进程)里都能生效。
注意两点:
- 路径注入用
sys.path.append,不要用insert(0),防止遮蔽标准库 - 改完不用重启容器,下一个任务立即生效
「裸 shell 能 import」不能作为判据
这是我踩过的最阴的一个坑:docker exec进容器,python -c "import notify"一切正常,但面板里跑任务就是报错。
原因:docker exec起的 shell 是干净环境,PYTHONPATH 没被 otask.sh 改写;面板任务走的路径完全不同。唯一可信的验证方式是模拟面板的调用链:
dockerexecqinglongbash-c'cd /ql/data/scripts && QL_DIR=/ql task <仓库目录>/<脚本路径>.py'这条命令等价于你在面板里点「运行」。它通过了,面板里才会通过。
坑二:配置改了没生效——同一个变量有三层,你在改的可能不是生效那层
第二个大坑是环境变量。青龙的配置有三层,优先级从低到高:
- 容器层:compose 的
environment/docker run -e(进容器Config.Env) - 面板层:网页「环境变量」界面(存在
/ql/data/db的 sqlite 里) - 任务层:
/ql/data/config/config.sh和extra.sh,任务启动前被source,优先级最高
排查「改了没生效」的第一原则:先怀疑有更高优先级的同名变量在覆盖你。常见场景是三层里都有HTTP_PROXY,你在面板里改了,但 config.sh 里还有一行旧的,任务实际用的是旧的。
验收不能只看文件,要用任务的真实视角看:
dockerexecqinglongbash-c'source /ql/data/config/config.sh; env | grep -i proxy'这打印出来的才是任务进程实际拿到的值。
还有两个附带的小坑:
export VAR=""不是「恢复默认值」,而是显式把变量覆盖成空串。config.sh 里要填就填真值,不要的整行删掉,绝不留空赋值- 改了 compose 的
environment或command后,必须docker compose up -d --force-recreate。否则docker compose up -d只会说一句Running 0.0s——容器没重建,配置当然没变
坑三:command 被镜像 entrypoint 吞掉,是死代码
有个需求想在容器启动时执行git config,我写进了 compose 的command:,重启后docker inspect一看——根本没执行。
原因:青龙的镜像入口脚本(entrypoint)不会执行你传入的command,那段配置是死代码,不报错、不生效,纯摆设。
替代方案是给任务进程配GIT_CONFIG_GLOBAL环境变量,指向仓库目录里的一个文件:
GIT_CONFIG_GLOBAL=/ql/data/.gitconfig注意把user.name、user.email、http.postBuffer一起写进这个文件。另外不要用 bind mount 单文件去覆盖容器里的/root/.gitconfig——单文件挂载在容器重建后容易出 inode 问题,目录挂载更稳。
坑四:NO_PROXY 写法不对,容器互访被代理截走,症状是 503
这套环境里配了 HTTP 代理(HTTP_PROXY/HTTPS_PROXY),容器里跑的任务会继承这几个变量。但容器之间互访不能走代理,NO_PROXY 写错就会被截走。症状很有迷惑性:
容器 A 用容器名访问容器 B,回503,不是超时也不是 404。
看到 503 第一反应是「服务挂了」,实际上是请求被代理截走了——代理服务器不认识内网容器名,回了 503。
NO_PROXY 有三种常见的无效写法,全都见过:
- glob 通配:
NO_PROXY=172.*|192.168.*|10.*—— curl 和 Python requests 都不解析这种写法,整条丢弃 - 只写 CIDR 不写主机名:
NO_PROXY=172.16.0.0/12—— 对 IP 地址有效,但对主机名无效 - 空赋值:
NO_PROXY=""—— 赋值动作本身就会覆盖上层的正确值
第 2 条的根因值得单独说:Python 的requests不做 DNS 解析来匹配 NO_PROXY,只做字符串的hostname.endswith(条目)判断。主机名永远不会匹配上 CIDR。curl 会解析 DNS,所以同一个 NO_PROXY 下 curl 正常、Python 不正常,又是一个两边行为不一致的陷阱。
正确口径一句话:IP 用 CIDR 兜底,主机名逐字列出。
NO_PROXY=localhost,127.0.0.1,qinglong,yyb-go,host.docker.internal,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8验收标准也不止是「env 值对了」,要看 Python 的真实判断:
importurllib.requestprint(urllib.request.proxy_bypass("qinglong"))# 必须是 Trueimportrequestsprint(requests.get("http://qinglong:5700/api/system").status_code)# 必须 200两个都过,才算修好。
坑五:同一个脚本多份副本,你看的日志可能不是它
青龙里跑签到类脚本,同一个脚本常常存在多份副本(不同仓库目录各一份)。排查问题时你打开的日志,可能对应的是另一份禁用着的副本,你在那份里改半天当然没用。
锁定真身的办法:
cat/ql/data/config/crontab.list每行任务后面带的路径才是实际执行的文件。再拿日志目录名对一下:
ls/ql/data/log/<任务log_name>/先确认「跑的是哪一份」,再去改代码。另外crontab.list行首的#注释状态和面板里的启用状态可能不一致,以面板为准。
写在最后
回头看,这五个坑里有三个的共性是:报错信息 / 表面现象,和真实原因之间隔了一层。
No module named 'notify'→ 不是缺模块,是路径注入被打断- 配置「改对了没生效」 → 改的那层不是生效的那层
- 容器互访 503 → 不是服务问题,是代理截胡
- curl 正常 Python 不正常 → 不是网络问题,是两家对 NO_PROXY 的解析规则不同
所以排查这类问题,我的习惯变成了:先找一条能复现任务真实环境的验证命令(比如task命令全链路跑一遍),再动配置。验证命令不存在的话,先造一条,再排查——不然每改一次都只能「面板点运行 + 祈祷」,排查效率天差地别。
机器还在跑,欢迎交流。
本文同步发布于 xiaoyiweb.xyz,青龙面板的其他踩坑记录和这台 2G 盒子的完整部署手册都在站内。