n5105中istoreos_docker运行sing-box-subscribe定时进行节点转换

sing-box-subscribe地址

sing-box-subscribe是根据配置模板生成 sing-box 使用的 config.json,主要用于将机场订阅节点添加到 config.json之中。

现在将它改成在docker容器中定时自动运行main.py来生成sing-box的config.json配置文件。

先将项目目录图片和如下内容丢给ai自动生成代码,然后再根据ai生成的代码再自行修改。

我现在的目录结构如图,帮我写一个在 docker 中运行 pyhon 命令的镜像,主要是运行 main.py,把 parsers、config_template 文件夹和 main.py 文件复制到容器里,在对应的映射卷中只有运行 main.py 时自动生成的 config.json 文件。main.py 在运行过程中会要求从终端输入一个数字,这个数字写进容器的环境变量中,main.py 从环境变量中读取。要能在容器的环境变量 CRON_TIME 中指定自动执行 main.py 的时间,如:CRON_TIME=25 20 * * *。所有依赖文件如 parsers 文件夹,config_template 文件夹、main.py、providers.json 全部打包到镜像。帮我做一个镜像。

项目文件清单(本地目录结构)

生成singbox-subscribe镜像的文件目录如下:

./项目根目录
├── Dockerfile
├── entrypoint.sh # 容器启动入口脚本
├── requirements.txt # Python依赖
├── main.py # 运行的主程序
├── providers.json # 内置默认配置打包进镜像
├── docker-compose.yml # 安装镜像
├── parsers/ # 完整解析器文件夹
│ ├── anytls.py
│ ├── app.py
│ ├── tool.py
│ ├── clash2base64.py
│ ├── http.py
│ ├── https.py
│ ├── hysteria.py
│ ├── hysteria2.py
│ ├── socks.py
│ ├── ss.py
│ ├── ssr.py
│ ├── tool.py
│ ├── trojan.py
│ ├── tuic.py
│ ├── vless.py
│ ├── vmess.py
│ ├── wg.py
│ └── gh_proxy_helper.py
└── config_template/ # 模板文件夹,内置到镜像
├── sb-config-1.12.json
├── sb-config-1.14.json
└── ...所有json模板

requirements.txt(Python 依赖)

根据 main.py 里 import 提取所有依赖:

requests
paramiko
scp
chardet
PyYAML
ruamel.yaml
Flask==2.3.2
cryptography
flask
flask-login
flask-wtf

Dockerfile(完整镜像构建文件)

使用 python:slim 轻量基础镜像,内置 cron,打包全部代码、模板、providers.json,挂载/config为持久化卷(仅生成 config.json)

FROM python:3.11-slim
# 镜像标签备注镜像维护信息,纯标识
LABEL maintainer="singbox-subscribePro"
# Python 环境变量日志实时打印,docker logs 即时看输出
ENV PYTHONUNBUFFERED=1
# Python 环境变量禁止生成 pyc 缓存文件,容器目录更干净
ENV PYTHONDONTWRITEBYTECODE=1
ENV TZ=Asia/Shanghai

# 安装系统依赖:cron、时区工具
RUN apt-get update && apt-get install -y --no-install-recommends \
cron tzdata \
&& rm -rf /var/lib/apt/lists/* && apt-get clean

# 工作目录
WORKDIR /app

# 复制依赖并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 复制全部业务代码、模板、默认providers.json
COPY main.py .
COPY providers.json .
COPY parsers ./parsers
COPY config_template ./config_template

# 复制启动脚本并授权
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# 持久化卷:/config 用于存放运行生成的config.json,用户可挂载宿主机目录
VOLUME ["/config"]

# 环境变量默认值
ENV TEMPLATE_INDEX=3
ENV CRON_TIME="59 10 * * *"

# 入口脚本
ENTRYPOINT ["/entrypoint.sh"]

docker-compose.yml

cd到singbox-subscribe目录下,然后执行docker compose up -d

services:
singbox-subscribe:
# build: .
image: singbox-subscribepro:latest
container_name: singbox-subscribepro
restart: on-failure
environment:
- CRON_TIME=59 10 * * *
# 容器启动时,不立刻执行**
- RUN_ON_START=false
- TEMPLATE_INDEX=3
# 设置订阅 URL 环境变量(支持多个)
- SUB_URL_1=http://192.168.50.1:8299/download/sub?target=sing-box
- SUB_URL_2=https://tv.*****.top:2053/sub
# 或者使用逗号分隔: - SUB_URLS=http://url1,https://url2
volumes:
- /etc/localtime:/etc/localtime:ro # 可选。用于与主机同步时间
- ./:/config
# 限制容器日志磁盘占用:**最多占用 3 × 10M = 30MB 存储空间**
logging:
driver: "json-file"
options:
max-size: "10M"
max-file: "3"

logging: 完整代码逐行解释

  1. driver: “json-file”
    Docker 默认日志驱动,容器所有标准输出 / 错误(print、脚本打印、报错)会以 JSON 格式持久化存储在宿主机磁盘。
  • 不配置时默认也是 json-file
  • 日志文件会保存在 Docker 数据目录(通常 /var/lib/docker/containers/容器ID/);
  • 执行 docker logs 容器名 底层就是读取该 JSON 日志文件。
  1. options.max-size: “10M”
    单日志文件最大上限 10 兆
    容器持续运行、定时每日打印日志,日志文件会不断膨胀;
    当日志文件达到 10MB 时,Docker 自动滚动分割日志,不再往这个文件追加内容。

  2. options.max-file: “3”
    最多保留 3 份滚动日志文件
    举个例子配合上面 max-size:10M
    初始日志:xxx-json.log(当前正在写入)
    填满 10M → 重命名为 xxx-json.log.1,新建空日志继续输出
    再次填满 10M → 生成 xxx-json.log.2
    第三次填满 10M → 生成 xxx-json.log.3
    第四次达到 10M 时:最旧的 .log.1 文件直接自动删除,只维持最多 3 个历史日志文件

  3. 整体作用总结

logging:
driver: "json-file"
options:
max-size: "10M"
max-file: "3"

限制容器日志磁盘占用:最多占用 3 × 10M = 30MB 存储空间,避免长期定时运行(每天 20:25 执行)日志无限堆积撑满服务器硬盘,是生产环境标准优化配置,推荐保留。

Docker 镜像标签(仓库名)强制要求全小写

  1. 镜像名称只能:小写字母、数字、横线 -、下划线 _、点 .
  2. 不能包含任意大写英文字母;
  3. 镜像名分隔建议用短横线 -,可读性更好。

entrypoint.sh

启动程序入口

#!/bin/bash
set -e
# 设置东八区时区
ln -sf /usr/share/zoneinfo/Asia/Shanghai
echo "Asia/Shanghai" > /etc/timezone

# 统一处理 RUN_ON_START 变量
RUN_ON_START_RAW="${RUN_ON_START:-true}"
RUN_ON_START=$(echo "$RUN_ON_START_RAW" | tr '[:upper:]' '[:lower:]')

# 生成独立执行脚本(仅负责运行python)
cat > /app/run_task.sh <<'EOF'
#!/bin/bash
cd /app
/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1
EOF
chmod +x /app/run_task.sh

# 启动时直接运行(完整继承容器环境)
if [ "$RUN_ON_START" = "true" ]; then
echo "RUN_ON_START=true,容器启动立即执行转换脚本"
/app/run_task.sh
fi

# 配置定时任务:命令行前置注入所有自定义环境变量,绕过cron环境隔离
rm -f /etc/cron.d/app-task
touch /etc/cron.d/app-task
if [ -n "${CRON_TIME}" ]; then
echo "配置定时任务:${CRON_TIME}"
# 全部环境变量写在cron执行命令最前面,cron执行时自带
CRON_FULL_CMD="${CRON_TIME} root TEMPLATE_INDEX=${TEMPLATE_INDEX} SUB_URL_1='${SUB_URL_1}' SUB_URL_2='${SUB_URL_2}' /app/run_task.sh"
echo "$CRON_FULL_CMD" >> /etc/cron.d/app-task
chmod 0644 /etc/cron.d/app-task
crontab /etc/cron.d/app-task
exec cron -f
else
echo "未设置CRON_TIME,执行完成后容器退出"
fi

以上代码的核心问题:cron 进程隔离环境变量

现象拆解
  1. 容器前台 exec /app/run_task.sh 正常读取 SUB_URL_1/SUB_URL_2/TEMPLATE_INDEX=3
  2. cron 定时执行 /app/run_task.sh丢失所有自定义环境变量,程序读空,回退 default-url
  3. 根本原理:Linux cron 运行在独立极简环境,不会继承容器启动时 -e 注入的环境变量,/app/run_task.sh 内部 export $VAR 拿不到父进程容器变量。
两种彻底解决方案(推荐方案 1)
方案 1:把所有环境变量写入 cron 任务行(一劳永逸,无需改 run_task.sh)

替换完整 entrypoint.sh,cron 命令行前置注入全部变量,cron 执行时自带,不受环境隔离限制:

#!/bin/bash
set -e
# 设置东八区时区
ln -sf /usr/share/zoneinfo/Asia/Shanghai
echo "Asia/Shanghai" > /etc/timezone

# 统一处理 RUN_ON_START 变量
RUN_ON_START_RAW="${RUN_ON_START:-true}"
RUN_ON_START=$(echo "$RUN_ON_START_RAW" | tr '[:upper:]' '[:lower:]')

# 生成独立执行脚本(仅负责运行python)
cat > /app/run_task.sh <<'EOF'
#!/bin/bash
cd /app
/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1
EOF
chmod +x /app/run_task.sh

# 启动时直接运行(完整继承容器环境)
if [ "$RUN_ON_START" = "true" ]; then
echo "RUN_ON_START=true,容器启动立即执行转换脚本"
/app/run_task.sh
fi

# 配置定时任务:命令行前置注入所有自定义环境变量,绕过cron环境隔离
rm -f /etc/cron.d/app-task
touch /etc/cron.d/app-task
if [ -n "${CRON_TIME}" ]; then
echo "配置定时任务:${CRON_TIME}"
# 全部环境变量写在cron执行命令最前面,cron执行时自带
CRON_FULL_CMD="${CRON_TIME} root TEMPLATE_INDEX=${TEMPLATE_INDEX} SUB_URLS='${SUB_URLS}' SUB_URL_1='${SUB_URL_1}' SUB_URL_2='${SUB_URL_2}' SUB_URL_3='${SUB_URL_3}' /app/run_task.sh"
echo "$CRON_FULL_CMD" >> /etc/cron.d/app-task
chmod 0644 /etc/cron.d/app-task
crontab /etc/cron.d/app-task
exec cron -f
else
echo "未设置CRON_TIME,执行完成后容器退出"
fi
关键改动说明

cron 任务不再只写 /app/run_task.sh,而是:

13 10 * * * root TEMPLATE_INDEX=3 SUB_URL_1="xxx" SUB_URL_2="xxx" /app/run_task.sh

cron 执行这条命令时,临时定义环境变量传递给脚本,不受 cron 隔离机制影响,和手动执行环境完全一致。

方案 2:全局持久环境文件(备选)

如果订阅链接含特殊引号导致方案 1 解析异常,可在 /etc/environment 写入变量,cron 默认读取该文件:

entrypoint 内增加:

# 写入全局环境,cron自动加载
cat > /etc/environment <<ENV
TEMPLATE_INDEX="${TEMPLATE_INDEX}"
SUB_URLS="${SUB_URLS}"
SUB_URL_1="${SUB_URL_1}"
SUB_URL_2="${SUB_URL_2}"
SUB_URL_3="${SUB_URL_3}"
ENV

run_task.sh 顶部加载全局环境:

#!/bin/bash
# 读取全局环境
while read line; do export "$line"; done < /etc/environment
cd /app
/usr/local/bin/python3 main.py >> /proc/1/fd/1 2>&1
部署生效步骤
  1. 替换本地 entrypoint.sh 为方案 1 完整代码
  2. 重新构建镜像
docker compose down
docker rmi singbox-subscribepro:latest
docker build -t singbox-subscribepro:latest .
docker compose up -d
  1. 校验 cron 任务内容
docker exec -it singbox-subscribepro cat /etc/cron.d/app-task

输出应看到变量拼接在命令最前方,示例:

13 10 * * root TEMPLATE_INDEX=3 SUB_URLS='' SUB_URL_1='http://192.168.50.1:8299/download/sub?target=sing-box' SUB_URL_2='https://tv.****.top:2053/sub' SUB_URL_3='' /app/run_task.sh
验证方式

手动模拟 cron 完整命令(复制 cat 输出的整行,去掉时间 + root 前缀执行):

docker exec -it singbox-subscribepro bash -c "TEMPLATE_INDEX=3 SUB_URL_1='http://192.168.50.1:8299/download/sub?target=sing-box' SUB_URL_2='https://tv.*****.top:2053/sub' /app/run_task.sh"

输出和你手动运行完全一致,不再出现未设置SUB_URL、TEMPLATE_INDEX无效报错。

补充原因总结

容器直接 exec:进程是容器主进程子进程,完整继承 -e 注入环境;

cron 定时任务:属于系统独立后台进程,不继承容器启动时的自定义环境变量,必须在 cron 命令行显式传入变量。

删除 main.py 中的 gh_proxy 代理

因为我是本地执行,不需要代理

一、确认要删除的 4 处 gh_proxy 代码,删除后程序完全正常运行

下面 4 段全部删掉,本地订阅逻辑不受任何影响,不会报错:

1. 顶部导入删除

原代码:

from parsers.gh_proxy_helper import set_gh_proxy

直接整行删掉。

2. argparse 参数删除
parser.add_argument('--gh_proxy_index', type=str, help='github加速链接')

整行删除。

3. 接收 gh_proxy_index 变量删除
gh_proxy_index = args.gh_proxy_index

整行删除。

4. github 加速逻辑整块删除

原完整块:

# 处理github加速
if hasattr(args, 'gh_proxy_index') and str(args.gh_proxy_index).isdigit():
gh_proxy_index = int(args.gh_proxy_index)
print(gh_proxy_index)
urls = [item["url"] for item in config["route"]["rule_set"]]
new_urls = set_gh_proxy(urls, gh_proxy_index)
for item, new_url in zip(config["route"]["rule_set"], new_urls):
item["url"] = new_url

全部删掉。

二、删除后对应修改片段展示(对照修改,避免改错)
修改后头部导入区
# 系统自带库(不用管来源,Python 自带)
import json, os, time, requests, sys, importlib, argparse, yaml, ruamel
import re
from datetime import datetime
from urllib.parse import urlparse
from collections import OrderedDict
# 项目内部文件(来自你项目里的文件)
from parsers.app import TEMP_DIR
from parsers import tool
from parsers.clash2base64 import clash2v2ray
# 下面一行gh_proxy导入已删除
修改后 argparse 区域
parser = argparse.ArgumentParser()
parser.add_argument('--temp_json_data', type=parse_json, help='临时内容')
parser.add_argument('--template_index', type=int, help='模板序号')
# --gh_proxy_index 参数已删除
args = parser.parse_args()
temp_json_data = args.temp_json_data
# gh_proxy_index = args.gh_proxy_index 已删除
修改后主逻辑(删除加速块)
nodes = process_subscribes(providers["subscribes"])
# ==========下面整块github加速代码已经删除==========
if providers.get('Only-nodes'):
combined_contents = []
for sub_tag, contents in nodes.items():
# 遍历每个机场的内容
for content in contents:
# 将内容添加到新列表中
combined_contents.append(content)
final_config = combined_contents # 只返回节点信息
else:
final_config = combin_to_config(config, nodes) # 节点信息添加到模板
三、为什么删除后程序 100% 正常?
  1. gh_proxy 仅作用于github 规则集链接加速,你本地内网订阅(192.168、tv.weito)完全不依赖这段代码;
  2. 删除后不会调用set_gh_proxy函数,不会出现导入缺失报错;
  3. --gh_proxy_index启动参数彻底移除,无多余参数干扰;
  4. 订阅拉取、节点解析、分流分组、定时、TEMPLATE_INDEX 全部原有逻辑保留;
  5. 不会影响providers.json、config 模板、/config 配置保存逻辑。
四、修改后验证步骤
  1. 保存修改后的 main.py
  2. 重建镜像
docker compose down
docker rmi singbox-subscribepro:latest
docker build -t singbox-subscribepro:latest .
docker compose up -d
  1. 手动执行测试
docker exec -it singbox-subscribepro /app/run_task.sh

输出正常加载 SUB_URL、zy_base 模板、正常生成 config.json,无任何报错即为修改成功。

五、额外清理(可选)

parsers 文件夹里的gh_proxy_helper.py文件可以直接删除,不再使用,不删也不影响运行。