文件结构与说明
. ├── Dockerfile ├── entrypoint.sh ├── main.py # 修改后的主程序(支持环境变量及绝对路径导入) ├── requirements.txt # Python 依赖(保持原样) ├── parsers/ # 原有解析器文件夹(保持原样) ├── config_template/ # 默认配置文件(构建时复制到镜像) │ ├── config_template.json/ # 示例配置模板(至少包含一个 .json 模板) │ └── sb.base.json # 示例提供方配置 |__ providers.json └── docker-compose.yml # 示例编排文件
|
模板打包到镜像自动释放到主机的映射卷中
我们理解用户的需求:模板文件(config_template/)应该打包进镜像,在容器运行时自动释放到主机的映射卷中(如果主机目录为空),这样用户无需手动准备模板文件。用户当前遇到错误是因为 entrypoint.sh 检查 config_template 目录不存在,但没有自动复制逻辑。
需要修改 entrypoint.sh 和 Dockerfile,实现:
- 在镜像内预置一个默认的 config_template 文件夹(包含至少一个模板文件,如 default.json)。
- 容器启动时,检查 /config/config_template 是否存在且非空,若为空则从镜像内复制默认模板到该目录。
这样用户首次运行容器时,卷映射目录会自动获得默认模板,之后用户可以修改主机上的模板。
1. 修改 Dockerfile:将 config_template 复制到镜像内作为默认模板
假设您的项目根目录下有一个 config_template 文件夹(包含至少一个 .json 模板文件),则在 Dockerfile 中添加:
COPY config_template /app/default_config_template
|
同时保留原有的 COPY providers.json /app/providers.json。
2. 修改 entrypoint.sh:自动初始化 config_template 目录
在检查 config_template 是否存在之前,先判断主机卷中的 /config/config_template 是否为空,若为空则从镜像默认模板复制。
完整的 entrypoint.sh:
#!/bin/bash set -e
CONFIG_DIR="/config" RUN_LOG="$CONFIG_DIR/run.log"
if [ ! -d "$CONFIG_DIR/config_template" ] || [ -z "$(ls -A $CONFIG_DIR/config_template 2>/dev/null)" ]; then echo "config_template 目录不存在或为空,从镜像复制默认模板..." mkdir -p "$CONFIG_DIR/config_template" cp -r /app/default_config_template/* "$CONFIG_DIR/config_template/" echo "默认模板已复制到 $CONFIG_DIR/config_template" fi
if [ ! -d "$CONFIG_DIR/config_template" ]; then echo "错误:$CONFIG_DIR/config_template 目录不存在" exit 1 fi
CRON_SCHEDULE="${CRON_TIME}" CRON_ENV="TEMPLATE_INDEX=${TEMPLATE_INDEX}" CRON_JOB="$CRON_SCHEDULE cd /app && $CRON_ENV python3 main.py >> $RUN_LOG 2>&1"
(crontab -l 2>/dev/null | grep -v "main.py" ; echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; echo "$CRON_JOB") | crontab -
if [ "$RUN_ON_START" = "true" ]; then echo "立即运行一次 main.py ..." cd /app TEMPLATE_INDEX="$TEMPLATE_INDEX" python3 main.py fi
/usr/sbin/cron -f
|
3. 确保主机卷映射正确(如 ./date:/config)
您的 docker-compose.yml 已经正确映射。首次启动时,./date 目录可能为空,容器会自动将镜像内的默认 config_template 复制进去,之后您可以在主机上修改模板文件。
4. 重新构建镜像并运行
cd /avmb/singbox-subscribe docker-compose down -v docker rmi singbox-subscribe docker build -t singbox-subscribe . docker-compose up -d
|
补充说明
providers.json 已经完全打包在镜像内(/app/providers.json),并通过环境变量 SUB_URL_1、SUB_URL_2 等传递订阅 URL,无需主机挂载。
config_template 的自动释放使得用户不必手动准备模板,实现了“安装时从镜像释放”的需求。
- 如果用户已经自己准备了模板(即主机目录非空),则不会覆盖,保证自定义配置不丢失。
这样,您的容器实现了“所有依赖打包进镜像,主机卷只存放需要持久化的配置文件(模板和生成的 config.json)”,并且首次运行自动初始化,完全符合预期。
导出/导入镜像文件(适合无镜像仓库的场景)
- 导出镜像:docker save -o singbox-subscribe.tar singbox-subscribe
- 传输到目标机器
- 导入镜像:docker load -i singbox-subscribe.tar
- 准备配置文件(config_template 和 docker-compose.yml 等)
您的容器时区是 UTC(世界协调时),而您设置的 CRON_TIME=52 18 * * * 通常是指北京时间(UTC+8)的 18:52。由于容器使用 UTC 时间,cron 会在 UTC 18:52 执行,也就是北京时间的次日 02:52,所以您在当前时间(北京时间 19:02?)看不到执行是正常的。
解决方案(二选一)
方案一:修改容器时区为北京时间(推荐)
这样您就可以直接用北京时间设置 cron 表达式。
方法1:在 docker-compose.yml 中挂载宿主机时区文件
services: subconverter: volumes: - ./date:/config - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro
|
方法2:在 Dockerfile 中设置 TZ 环境变量
ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
修改后的 docker-compose.yml 示例:
services: subconverter: build: . container_name: singbox-subscriber restart: on-failure environment: - CRON_TIME=52 18 * * * - RUN_ON_START=true - TEMPLATE_INDEX=1 - SUB_URL_1=http://192.168.50.1:8299/download/sub?target=sing-box - SUB_URL_2=https://tv.weito.top:2053/sub volumes: - ./date:/config - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro logging: driver: "json-file" options: max-size: "10M" max-file: "3"
|
操作步骤:
修改 docker-compose.yml(如上增加两行 volume 挂载)。
重新创建容器:
bash
docker-compose down docker-compose up -d
|
验证时区:
docker exec singbox-subscriber date
|
方法二:设置环境变量 TZ
environment: - TZ=Asia/Shanghai
|
1. 首先确认容器时区是否已变为北京时间
docker exec singbox-subscriber date
|
请将 entrypoint.sh 替换为以下精简且健壮的版本:
#!/bin/bash set -e
CONFIG_DIR="/config" RUN_LOG="$CONFIG_DIR/run.log"
if [ ! -d "$CONFIG_DIR/config_template" ]; then echo "错误:$CONFIG_DIR/config_template 目录不存在,请挂载正确目录" exit 1 fi
ENV_EXPORTS=""
ENV_EXPORTS="$ENV_EXPORTS export TEMPLATE_INDEX='$TEMPLATE_INDEX';"
for var in $(env | grep '^SUB_URL_' | cut -d= -f1); do ENV_EXPORTS="$ENV_EXPORTS export $var='${!var}';" done
if [ -n "$SUB_URLS" ]; then ENV_EXPORTS="$ENV_EXPORTS export SUB_URLS='$SUB_URLS';" fi
CRON_SCHEDULE="${CRON_TIME}"
CRON_JOB="$CRON_SCHEDULE /app/cron_wrapper.sh >> $RUN_LOG 2>&1"
{ crontab -l 2>/dev/null || echo ""; } | grep -v "main.py" | { cat; echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; echo "$CRON_JOB"; } | crontab -
if [ "$RUN_ON_START" = "true" ]; then echo "立即运行一次 main.py ..." cd /app TEMPLATE_INDEX="$TEMPLATE_INDEX" python3 main.py || true fi
echo "启动 cron 服务,定时任务: $CRON_SCHEDULE" /usr/sbin/cron -f
|
说明:
- 移除了自动复制模板的代码,因为您已经通过卷挂载了
config_template。
- 在 crontab 行中显式导出
TEMPLATE_INDEX 和所有 SUB_URL_* 环境变量,确保 cron 执行时能正确获取订阅地址。
- 保留了立即运行和 cron 前台启动。
1. 修改 Dockerfile,创建包装脚本
FROM python:3.9-slim
RUN apt-get update && apt-get install -y cron && rm -rf /var/lib/apt/lists/*
ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
COPY config_template /app/default_config_template COPY parsers ./parsers RUN find /app/parsers -type d -exec touch {}/__init__.py \;
COPY main.py requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt paramiko scp
COPY providers.json /app/providers.json COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh
RUN echo '#!/bin/bash\n\ export $(cat /proc/1/environ | tr "\\0" "\\n" | grep -E "^(SUB_URL_|TEMPLATE_INDEX)")\n\ cd /app\n\ python3 main.py\n' > /app/cron_wrapper.sh && chmod +x /app/cron_wrapper.sh
RUN mkdir -p /config
ENV CRON_TIME="0 4 * * *" \ RUN_ON_START="true" \ TEMPLATE_INDEX=""
ENTRYPOINT ["/entrypoint.sh"]
|
这将在 /app/cron_wrapper.sh 中生成一个脚本,它从 PID 1 的环境变量中提取所有 SUB_URL_ 和 TEMPLATE_INDEX 变量,然后执行 main.py。
2. 修改 entrypoint.sh,让 crontab 调用包装脚本
将 CRON_JOB 的定义改为:
CRON_JOB="$CRON_SCHEDULE /app/cron_wrapper.sh >> $RUN_LOG 2>&1"
|
同时可以移除之前复杂的 ENV_EXPORTS 收集逻辑。
完整的 entrypoint.sh(精简版)
#!/bin/bash set -e
CONFIG_DIR="/config" RUN_LOG="$CONFIG_DIR/run.log"
if [ ! -d "$CONFIG_DIR/config_template" ] || [ -z "$(ls -A $CONFIG_DIR/config_template 2>/dev/null)" ]; then echo "config_template 目录不存在或为空,从镜像复制默认模板..." mkdir -p "$CONFIG_DIR/config_template" if [ -d "/app/default_config_template" ]; then cp -r /app/default_config_template/* "$CONFIG_DIR/config_template/" echo "默认模板已复制到 $CONFIG_DIR/config_template" else echo "警告:镜像中没有默认模板目录 /app/default_config_template,请自行提供 config_template" fi fi
if [ ! -d "$CONFIG_DIR/config_template" ]; then echo "错误:$CONFIG_DIR/config_template 目录不存在,且无法自动创建" exit 1 fi
CRON_SCHEDULE="${CRON_TIME}"
CRON_JOB="$CRON_SCHEDULE /app/cron_wrapper.sh >> $RUN_LOG 2>&1"
{ crontab -l 2>/dev/null || echo ""; } | grep -v "cron_wrapper.sh" | { cat; echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; echo "$CRON_JOB"; } | crontab -
if [ "$RUN_ON_START" = "true" ]; then echo "立即运行一次 main.py ..." cd /app TEMPLATE_INDEX="$TEMPLATE_INDEX" python3 main.py || true fi
echo "启动 cron 服务,定时任务: $CRON_SCHEDULE" /usr/sbin/cron -f
|
3. 重新构建并运行
cd /avmb/singbox-subscribe docker-compose down docker rmi singbox-subscribe:latest docker build -t singbox-subscribe:latest . docker-compose up -d
|
4. 验证
docker exec singbox-subscribe ps aux | grep cron docker exec singbox-subscribe crontab -l
docker exec singbox-subscribe /app/cron_wrapper.sh
|
说明
- 该
RUN 命令创建了 /app/cron_wrapper.sh 脚本,用于从容器主进程(PID 1)的环境变量中提取所有 SUB_URL_ 开头的变量以及 TEMPLATE_INDEX,然后执行 main.py。
- 确保脚本有执行权限(
chmod +x)。
- 之后您需要修改
entrypoint.sh 中的 CRON_JOB 为 /app/cron_wrapper.sh(如之前所述)。
如果不希望修改 entrypoint.sh(虽然推荐修改),也可以直接在 crontab 中调用这个脚本。但请按照之前的回答同步更新 entrypoint.sh 中的 CRON_JOB 定义。