dockerfile构建sing-box-subscribe镜像的方式方法

文件结构与说明

.
├── 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,实现:

  1. 在镜像内预置一个默认的 config_template 文件夹(包含至少一个模板文件,如 default.json)。
  2. 容器启动时,检查 /config/config_template 是否存在且非空,若为空则从镜像内复制默认模板到该目录。

这样用户首次运行容器时,卷映射目录会自动获得默认模板,之后用户可以修改主机上的模板。

1. 修改 Dockerfile:将 config_template 复制到镜像内作为默认模板

假设您的项目根目录下有一个 config_template 文件夹(包含至少一个 .json 模板文件),则在 Dockerfile 中添加:

# 复制默认配置模板到镜像内的 /app/default_config_template
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"

# 初始化 config_template 目录(如果主机目录为空)
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 任务
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,设置 PATH 避免 cron 找不到 python3
(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

# 启动 cron(Debian 使用 /usr/sbin/cron -f)
/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_1SUB_URL_2 等传递订阅 URL,无需主机挂载。
  • config_template 的自动释放使得用户不必手动准备模板,实现了“安装时从镜像释放”的需求。
  • 如果用户已经自己准备了模板(即主机目录非空),则不会覆盖,保证自定义配置不丢失。

这样,您的容器实现了“所有依赖打包进镜像,主机卷只存放需要持久化的配置文件(模板和生成的 config.json)”,并且首次运行自动初始化,完全符合预期。

导出/导入镜像文件(适合无镜像仓库的场景)

  1. 导出镜像:docker save -o singbox-subscribe.tar singbox-subscribe
  2. 传输到目标机器
  3. 导入镜像:docker load -i singbox-subscribe.tar
  4. 准备配置文件(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 * * * # 保持原样,表示北京时间 18:52
- 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"

操作步骤:

  1. 修改 docker-compose.yml(如上增加两行 volume 挂载)。

  2. 重新创建容器

    bash

    docker-compose down
    docker-compose up -d
  3. 验证时区

    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"

# 只检查 config_template 目录是否存在(由主机卷提供,不自动复制)
if [ ! -d "$CONFIG_DIR/config_template" ]; then
echo "错误:$CONFIG_DIR/config_template 目录不存在,请挂载正确目录"
exit 1
fi

# 收集所有需要的环境变量,供 cron 使用
ENV_EXPORTS=""
# 传递 TEMPLATE_INDEX
ENV_EXPORTS="$ENV_EXPORTS export TEMPLATE_INDEX='$TEMPLATE_INDEX';"
# 传递所有 SUB_URL_ 开头的变量
for var in $(env | grep '^SUB_URL_' | cut -d= -f1); do
ENV_EXPORTS="$ENV_EXPORTS export $var='${!var}';"
done
# 也可传递 SUB_URLS 如果使用
if [ -n "$SUB_URLS" ]; then
ENV_EXPORTS="$ENV_EXPORTS export SUB_URLS='$SUB_URLS';"
fi

CRON_SCHEDULE="${CRON_TIME}"
# crontab 执行命令:先设置环境,再 cd 到 /app 执行 python3
CRON_JOB="$CRON_SCHEDULE /app/cron_wrapper.sh >> $RUN_LOG 2>&1"

# 写入 crontab(兼容无已有 crontab 的情况)
{ 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
# 直接在当前 shell 执行,环境变量天然存在
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

# 创建 cron 包装脚本
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"

# 如果 config_template 目录不存在或为空,则从镜像默认模板复制
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"
# 假设 Dockerfile 已将默认模板复制到 /app/default_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}"
# crontab 执行命令:调用包装脚本(从主进程继承环境变量)
CRON_JOB="$CRON_SCHEDULE /app/cron_wrapper.sh >> $RUN_LOG 2>&1"

# 写入 crontab(清除旧的与 cron_wrapper.sh 相关的任务)
{ 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. 验证

# 检查 cron 进程和 crontab
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 定义。