Add Docker Compose one-click deployment

This commit is contained in:
Rixuan Shao
2026-05-30 00:39:14 +08:00
parent 11a6c949db
commit 132f799d3c
12 changed files with 634 additions and 49 deletions
+19
View File
@@ -0,0 +1,19 @@
APP_ROOT=/opt/douyin-sparkflow
TZ=Asia/Shanghai
WEB_PORT=8787
LOGIN_DESKTOP_WEB_PORT=8788
PROXY_HTTP_PORT=7890
PROXY_CONTROLLER_PORT=9090
# Optional: paste a Mihomo/Clash subscription URL here, then run ./refresh_proxy.sh.
PROXY_SUB_URL=
PROXY_USER_AGENT=clash-verge/1.7.7
# Optional build-time proxy. Leave empty unless your Docker build needs a proxy.
HTTP_PROXY_BUILD=
HTTPS_PROXY_BUILD=
ALL_PROXY_BUILD=
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
+1
View File
@@ -1,6 +1,7 @@
.env
state/
logs/
proxy/config.yaml
*.bak-*
*.tar.gz
+5 -3
View File
@@ -25,7 +25,8 @@ ENV PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST}
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
RUN set -eux; \
sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone \
@@ -39,13 +40,14 @@ RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
websockify \
x11vnc \
xfonts-intl-chinese \
&& curl -fsSL -x http://127.0.0.1:7890 https://download.docker.com/linux/static/stable/x86_64/docker-25.0.3.tgz -o docker.tgz \
&& download() { url="$1"; output="$2"; proxy_url="${HTTPS_PROXY:-${https_proxy:-${HTTP_PROXY:-${http_proxy:-}}}}"; if [ -n "$proxy_url" ]; then curl -fsSL -x "$proxy_url" "$url" -o "$output"; else curl -fsSL "$url" -o "$output"; fi; } \
&& download https://download.docker.com/linux/static/stable/x86_64/docker-25.0.3.tgz docker.tgz \
&& tar xzvf docker.tgz \
&& mv docker/docker /usr/bin/docker \
&& chmod +x /usr/bin/docker \
&& rm -rf docker docker.tgz \
&& mkdir -p /usr/local/lib/docker/cli-plugins \
&& curl -SL -x http://127.0.0.1:7890 https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose \
&& download https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 /usr/local/lib/docker/cli-plugins/docker-compose \
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-compose \
&& rm -rf /var/lib/apt/lists/*
+88
View File
@@ -0,0 +1,88 @@
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
def expand_field(field, minimum, maximum, current):
values = set()
for part in str(field).split(","):
part = part.strip()
if not part:
continue
step = 1
if "/" in part:
part, raw_step = part.split("/", 1)
step = max(1, int(raw_step))
if part == "*":
start, end = minimum, maximum
elif "-" in part:
raw_start, raw_end = part.split("-", 1)
start, end = int(raw_start), int(raw_end)
else:
start = end = int(part)
values.update(range(max(minimum, start), min(maximum, end) + 1, step))
return current in values
def cron_dow(now):
return (now.weekday() + 1) % 7
def field_matches(field, minimum, maximum, current, *, allow_sunday_alias=False):
if allow_sunday_alias and current == 0:
return expand_field(field, minimum, maximum, 0) or expand_field(field, minimum, maximum, 7)
return expand_field(field, minimum, maximum, current)
def line_should_run(line, now):
parts = line.split(maxsplit=5)
if len(parts) < 6:
return False, ""
minute, hour, day, month, weekday, command = parts
try:
matched = (
field_matches(minute, 0, 59, now.minute)
and field_matches(hour, 0, 23, now.hour)
and field_matches(day, 1, 31, now.day)
and field_matches(month, 1, 12, now.month)
and field_matches(weekday, 0, 7, cron_dow(now), allow_sunday_alias=True)
)
except ValueError:
return False, ""
return matched, command
def read_crontab(path):
if not path.exists():
return []
lines = []
for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" in line.split(maxsplit=1)[0]:
continue
lines.append(line)
return lines
def run_loop(crontab_path):
crontab_path.parent.mkdir(parents=True, exist_ok=True)
last_minute_key = None
print(f"[cron_runner] watching {crontab_path}", flush=True)
while True:
now = datetime.now()
minute_key = now.strftime("%Y-%m-%d %H:%M")
if minute_key != last_minute_key:
last_minute_key = minute_key
for line in read_crontab(crontab_path):
matched, command = line_should_run(line, now)
if matched and command:
print(f"[cron_runner] {now.isoformat(timespec='seconds')} run: {command}", flush=True)
subprocess.Popen(["/bin/bash", "-lc", command])
time.sleep(15)
if __name__ == "__main__":
target = Path(sys.argv[1] if len(sys.argv) > 1 else "/host-spool-cron/root")
run_loop(target)
+4 -2
View File
@@ -1,5 +1,6 @@
import json
import logging
import os
import traceback
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -132,13 +133,14 @@ def _target_sent_today(account, target_name):
def login_desktop_api_url():
settings = get_app_settings(force_reload=True)
return str(settings.get("login_desktop_api_url") or "http://127.0.0.1:18090").rstrip("/")
return str(os.getenv("SPARKFLOW_LOGIN_DESKTOP_API_URL") or settings.get("login_desktop_api_url") or "http://127.0.0.1:18090").rstrip("/")
def login_desktop_public_url(request: Request) -> str:
host = request.url.hostname or "127.0.0.1"
scheme = request.url.scheme or "http"
return f"{scheme}://{host}:8788/vnc.html?autoconnect=1&resize=scale&view_only=0"
port = str(os.getenv("LOGIN_DESKTOP_PUBLIC_PORT") or "8788").strip() or "8788"
return f"{scheme}://{host}:{port}/vnc.html?autoconnect=1&resize=scale&view_only=0"
def call_login_desktop(path: str, *, method: str = "GET", payload: dict | None = None, timeout: int = 20) -> dict:
+79 -24
View File
@@ -1,41 +1,96 @@
# douyin-sparkflow
这个仓库是一个围绕 `DouYinSparkFlow/` 组织的部署仓库,用于保存核心应用源码、代理配置和容器编排文件,方便在本地或服务器上统一维护。当前整理目标是“私有内用、可放入 GitHub、避免提交运行态和敏感数据”
Douyin SparkFlow 是一个用于自动续火花的 Web 管理版部署包。推荐用 Docker Compose 部署,启动后在浏览器里完成管理员密码、扫码登录、目标好友勾选和发送窗口设置
## 仓库结构
## 一键部署到服务器
- `DouYinSparkFlow/`: 核心应用源码,包含 Web UI、任务调度、账号登录与消息发送逻辑。
- `proxy/`: 代理容器配置目录,当前包含 `mihomo` 配置文件。
- `docker-compose.yml`: 统一的容器编排入口。
- `refresh_proxy.sh`: 代理刷新脚本。
适合 Ubuntu/Debian/CentOS 类服务器:
## 运行方式概览
```bash
curl -fsSL https://raw.githubusercontent.com/halfwaystudent/douyin-sparkflow/main/deploy/install-server.sh | bash
```
- 支持本地运行核心应用,也支持通过 `docker-compose.yml` 在服务器上部署。
- Web 管理入口、交互式登录桌面和定时任务都由仓库内现有脚本与配置驱动。
- 定时发送、账号状态和消息模板属于运行时行为,不在本仓库中直接携带账号数据。
也可以指定安装目录或代理订阅:
## 配置与敏感文件
```bash
curl -fsSL https://raw.githubusercontent.com/halfwaystudent/douyin-sparkflow/main/deploy/install-server.sh | APP_ROOT=/opt/douyin-sparkflow PROXY_SUB_URL='你的 Mihomo/Clash 订阅链接' bash
```
以下内容不会进入当前 Git 仓库,需要在实际部署环境中自行补齐
部署完成后打开
- Web 面板:`http://服务器IP:8787`
- 扫码登录桌面:Web 面板里的“登录桌面”入口,默认端口 `8788`
首次使用流程:
1. 创建管理员账号密码。
2. 打开登录桌面,扫码登录抖音。
3. 保存登录态。
4. 刷新好友列表,勾选要续火花的目标好友。
5. 设置发送窗口,例如 `10:00-18:00/10m`
## 本地部署
Windows 本地需要先启动 Docker Desktop,然后运行:
```powershell
.\deploy\install-local.ps1
```
带代理订阅:
```powershell
.\deploy\install-local.ps1 -ProxySubUrl "你的 Mihomo/Clash 订阅链接"
```
脚本会创建 `.env`、运行态目录和默认代理配置,然后执行 `docker compose up -d --build` 并打开 Web 面板。
Linux/macOS 本地可以运行:
```bash
./deploy/install-local.sh
```
## 常用命令
```bash
docker compose ps
docker compose logs -f web
docker compose logs -f scheduler
docker compose up -d --build
docker compose down
```
刷新代理订阅:
```bash
./refresh_proxy.sh
docker compose restart proxy
```
如果 `.env` 里的 `PROXY_SUB_URL` 为空,系统会使用 `proxy/config.example.yaml` 生成一个直连配置。
## 目录结构
- `DouYinSparkFlow/`:核心应用、Web UI、登录桌面和发送任务。
- `docker-compose.yml`:统一容器编排入口,包含 Web、登录桌面、定时器、任务和代理服务。
- `deploy/`:服务器和本地一键部署脚本。
- `proxy/config.example.yaml`:安全的代理配置模板。
- `.env.example`:部署环境变量模板。
## 不提交的运行态文件
这些内容包含账号、登录态、日志或本机配置,不应提交到 GitHub:
- `.env`
- `state/`
- `logs/`
- `proxy/config.yaml`
- `DouYinSparkFlow/logs/`
- `DouYinSparkFlow/usersData.json`
- `DouYinSparkFlow/webui_settings.json`
- `DouYinSparkFlow/.im_sdk_cache/`
如果需要复现运行环境,建议在目标机器上重新生成这些文件,而不是从仓库恢复。
## 许可
## 使用边界
- 本仓库按内部项目资料整理,主要用于源码管理、部署维护和环境迁移。
- 使用者需要自行评估平台规则、账号风险和运行后果。
- 不建议把真实账号数据、浏览器登录态、日志或运行缓存提交到仓库。
## 许可证
核心应用当前采用 MIT 协议,许可证文件位于 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。
如需查看源码级说明,请优先阅读 [DouYinSparkFlow/README.md](DouYinSparkFlow/README.md)。
核心应用采用 MIT 协议,详见 [DouYinSparkFlow/LICENSE](DouYinSparkFlow/LICENSE)。
+124
View File
@@ -0,0 +1,124 @@
param(
[string]$ProxySubUrl = "",
[switch]$NoOpen
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
function Require-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name is required. Please install Docker Desktop and make sure Docker Compose is available."
}
}
function Set-EnvValue {
param(
[string]$Path,
[string]$Key,
[string]$Value
)
$line = "$Key=$Value"
if (-not (Test-Path $Path)) {
Set-Content -Path $Path -Value $line -Encoding utf8
return
}
$content = Get-Content -Path $Path -ErrorAction SilentlyContinue
$found = $false
$escapedKey = [regex]::Escape($Key)
$next = foreach ($item in $content) {
if ($item -match "^$escapedKey=") {
$found = $true
$line
} else {
$item
}
}
if (-not $found) {
$next = @($next) + $line
}
Set-Content -Path $Path -Value $next -Encoding utf8
}
function Get-EnvValue {
param(
[string]$Path,
[string]$Key,
[string]$DefaultValue
)
if (Test-Path $Path) {
$escapedKey = [regex]::Escape($Key)
$match = Get-Content -Path $Path | Where-Object { $_ -match "^$escapedKey=" } | Select-Object -First 1
if ($match) {
return ($match -replace "^$escapedKey=", "")
}
}
return $DefaultValue
}
function Set-ProxyConfigLine {
param(
[string]$Path,
[string]$Key,
[string]$Value
)
$line = "${Key}: $Value"
$content = Get-Content -Path $Path -ErrorAction SilentlyContinue
$escapedKey = [regex]::Escape($Key)
$found = $false
$next = foreach ($item in $content) {
if ($item -match "^${escapedKey}:") {
$found = $true
$line
} else {
$item
}
}
if (-not $found) {
$next = @($next) + $line
}
Set-Content -Path $Path -Value $next -Encoding utf8
}
function Refresh-ProxyConfig {
param([string]$Url)
if (-not $Url) {
return
}
$userAgent = Get-EnvValue -Path ".env" -Key "PROXY_USER_AGENT" -DefaultValue "clash-verge/1.7.7"
Invoke-WebRequest -Uri $Url -Headers @{ "User-Agent" = $userAgent } -OutFile "proxy/config.yaml"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "mixed-port" -Value "7890"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "allow-lan" -Value "true"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "bind-address" -Value "'*'"
Set-ProxyConfigLine -Path "proxy/config.yaml" -Key "external-controller" -Value "'0.0.0.0:9090'"
}
Require-Command docker
docker compose version | Out-Null
if (-not (Test-Path ".env")) {
Copy-Item ".env.example" ".env"
}
if ($ProxySubUrl) {
Set-EnvValue -Path ".env" -Key "PROXY_SUB_URL" -Value $ProxySubUrl
}
New-Item -ItemType Directory -Force -Path "proxy", "state/cron", "state/login-profile", "DouYinSparkFlow/logs" | Out-Null
if (-not (Test-Path "proxy/config.yaml")) {
Copy-Item "proxy/config.example.yaml" "proxy/config.yaml"
}
Refresh-ProxyConfig -Url $ProxySubUrl
docker compose up -d --build
$webPort = Get-EnvValue -Path ".env" -Key "WEB_PORT" -DefaultValue "8787"
$url = "http://localhost:$webPort"
Write-Host "Douyin SparkFlow is running: $url"
Write-Host "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
if (-not $NoOpen) {
Start-Process $url
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
echo "Docker with the Compose plugin is required." >&2
exit 1
fi
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file"; then
local tmp_file
tmp_file="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" { print key "=" value; replaced = 1; next }
{ print }
END { if (!replaced) print key "=" value }
' "$file" > "$tmp_file"
mv "$tmp_file" "$file"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
if [ ! -f ".env" ]; then
cp ".env.example" ".env"
fi
proxy_sub_url="${PROXY_SUB_URL:-${1:-}}"
if [ -n "$proxy_sub_url" ]; then
set_env_value ".env" "PROXY_SUB_URL" "$proxy_sub_url"
fi
mkdir -p proxy state/cron state/login-profile DouYinSparkFlow/logs
if [ ! -f "proxy/config.yaml" ]; then
cp "proxy/config.example.yaml" "proxy/config.yaml"
fi
bash ./refresh_proxy.sh
docker compose up -d --build
web_port="$(grep '^WEB_PORT=' .env | sed 's/^WEB_PORT=//')"
web_port="${web_port:-8787}"
url="http://localhost:${web_port}"
echo "Douyin SparkFlow is running: $url"
echo "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$url" >/dev/null 2>&1 || true
elif command -v open >/dev/null 2>&1; then
open "$url" >/dev/null 2>&1 || true
fi
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="${REPO_URL:-https://github.com/halfwaystudent/douyin-sparkflow.git}"
BRANCH="${BRANCH:-main}"
APP_ROOT="${APP_ROOT:-/opt/douyin-sparkflow}"
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
else
SUDO=""
fi
run_root() {
if [ -n "$SUDO" ]; then
sudo "$@"
else
"$@"
fi
}
install_base_tools() {
if command -v apt-get >/dev/null 2>&1; then
run_root apt-get update
run_root apt-get install -y ca-certificates curl git gnupg
elif command -v yum >/dev/null 2>&1; then
run_root yum install -y ca-certificates curl git
fi
}
install_docker_debian() {
. /etc/os-release
local docker_id="${ID}"
if [ "$docker_id" = "debian" ] || [ "$docker_id" = "ubuntu" ]; then
run_root install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/${docker_id}/gpg" | run_root tee /etc/apt/keyrings/docker.asc >/dev/null
run_root chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${docker_id} ${VERSION_CODENAME} stable" | run_root tee /etc/apt/sources.list.d/docker.list >/dev/null
run_root apt-get update
run_root apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else
run_root apt-get install -y docker.io docker-compose-plugin
fi
}
ensure_docker() {
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
return
fi
if command -v apt-get >/dev/null 2>&1; then
install_docker_debian
elif command -v yum >/dev/null 2>&1; then
run_root yum install -y docker docker-compose-plugin
else
echo "Docker is not installed. Please install Docker with the Compose plugin first." >&2
exit 1
fi
run_root systemctl enable --now docker || true
}
prepare_repo() {
run_root mkdir -p "$(dirname "$APP_ROOT")"
if [ -d "$APP_ROOT/.git" ]; then
run_root git -C "$APP_ROOT" fetch origin "$BRANCH"
run_root git -C "$APP_ROOT" checkout "$BRANCH"
run_root git -C "$APP_ROOT" pull --ff-only origin "$BRANCH"
else
run_root git clone --branch "$BRANCH" "$REPO_URL" "$APP_ROOT"
fi
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file"; then
local tmp_file
tmp_file="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" { print key "=" value; replaced = 1; next }
{ print }
END { if (!replaced) print key "=" value }
' "$file" > "$tmp_file"
run_root cp "$tmp_file" "$file"
rm -f "$tmp_file"
else
printf '%s=%s\n' "$key" "$value" | run_root tee -a "$file" >/dev/null
fi
}
prepare_runtime_files() {
if [ ! -f "$APP_ROOT/.env" ]; then
run_root cp "$APP_ROOT/.env.example" "$APP_ROOT/.env"
fi
set_env_value "$APP_ROOT/.env" "APP_ROOT" "$APP_ROOT"
for key in TZ WEB_PORT LOGIN_DESKTOP_WEB_PORT PROXY_HTTP_PORT PROXY_CONTROLLER_PORT PROXY_SUB_URL HTTP_PROXY_BUILD HTTPS_PROXY_BUILD ALL_PROXY_BUILD; do
if [ -n "${!key:-}" ]; then
set_env_value "$APP_ROOT/.env" "$key" "${!key}"
fi
done
local current_sub
current_sub="$(grep '^PROXY_SUB_URL=' "$APP_ROOT/.env" | sed 's/^PROXY_SUB_URL=//' || true)"
if [ -z "$current_sub" ] && [ -t 0 ]; then
printf 'Proxy subscription URL, optional and hidden: '
read -r -s input_sub || true
printf '\n'
if [ -n "${input_sub:-}" ]; then
set_env_value "$APP_ROOT/.env" "PROXY_SUB_URL" "$input_sub"
fi
fi
run_root mkdir -p "$APP_ROOT/proxy" "$APP_ROOT/state/cron" "$APP_ROOT/state/login-profile" "$APP_ROOT/DouYinSparkFlow/logs"
if [ ! -f "$APP_ROOT/proxy/config.yaml" ]; then
run_root cp "$APP_ROOT/proxy/config.example.yaml" "$APP_ROOT/proxy/config.yaml"
fi
}
main() {
install_base_tools
ensure_docker
prepare_repo
prepare_runtime_files
cd "$APP_ROOT"
run_root bash "$APP_ROOT/refresh_proxy.sh"
run_root docker compose up -d --build
local web_port login_port host_ip
web_port="$(grep '^WEB_PORT=' "$APP_ROOT/.env" | sed 's/^WEB_PORT=//')"
login_port="$(grep '^LOGIN_DESKTOP_WEB_PORT=' "$APP_ROOT/.env" | sed 's/^LOGIN_DESKTOP_WEB_PORT=//')"
host_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
host_ip="${host_ip:-127.0.0.1}"
echo
echo "Douyin SparkFlow is running."
echo "Web UI: http://${host_ip}:${web_port:-8787}"
echo "Login desktop: http://${host_ip}:${login_port:-8788}/vnc.html?autoconnect=1&resize=scale&view_only=0"
echo "Next: create the admin password, open the login desktop, scan the QR code, select target friends, and set the send window."
}
main "$@"
+38 -13
View File
@@ -9,10 +9,10 @@ services:
max-size: "20m"
max-file: "3"
ports:
- "7890:7890"
- "9090:9090"
- "${PROXY_HTTP_PORT:-7890}:7890"
- "${PROXY_CONTROLLER_PORT:-9090}:9090"
volumes:
- ./proxy/config.yaml:/root/.config/mihomo/config.yaml
- ./proxy/config.yaml:/root/.config/mihomo/config.yaml:ro
web:
build:
@@ -20,9 +20,14 @@ services:
dockerfile: Dockerfile.server
network: host
args:
HTTP_PROXY: http://127.0.0.1:7890
HTTPS_PROXY: http://127.0.0.1:7890
ALL_PROXY: socks5://127.0.0.1:7890
HTTP_PROXY: ${HTTP_PROXY_BUILD:-}
HTTPS_PROXY: ${HTTPS_PROXY_BUILD:-}
ALL_PROXY: ${ALL_PROXY_BUILD:-}
http_proxy: ${HTTP_PROXY_BUILD:-}
https_proxy: ${HTTPS_PROXY_BUILD:-}
all_proxy: ${ALL_PROXY_BUILD:-}
PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}
PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn}
image: douyin-sparkflow:local
container_name: douyin-web
restart: unless-stopped
@@ -30,20 +35,22 @@ services:
- proxy
- login-desktop
environment:
TZ: Asia/Shanghai
TZ: ${TZ:-Asia/Shanghai}
HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890
NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com
SPARKFLOW_LOGIN_DESKTOP_API_URL: http://login-desktop:18090
LOGIN_DESKTOP_PUBLIC_PORT: ${LOGIN_DESKTOP_WEB_PORT:-8788}
ports:
- "8787:8787"
- "${WEB_PORT:-8787}:8787"
command: python main.py --web --host 0.0.0.0 --port 8787
volumes:
- ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs
- ./state/cron:/host-spool-cron
- /var/run/docker.sock:/var/run/docker.sock
- /opt/douyin-sparkflow:/opt/douyin-sparkflow
- /var/spool/cron/root:/var/spool/cron/crontabs/root
- .:/opt/douyin-sparkflow
login-desktop:
image: douyin-sparkflow:local
@@ -52,27 +59,45 @@ services:
depends_on:
- proxy
environment:
TZ: Asia/Shanghai
TZ: ${TZ:-Asia/Shanghai}
DISPLAY: :99
LOGIN_DESKTOP_API_PORT: "18090"
LOGIN_DESKTOP_WEB_PORT: "6080"
HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890
NO_PROXY: localhost,127.0.0.1,login-desktop,douyin.com,amemv.com,snssdk.com,bytedance.com,pstatp.com,volccdn.com,bytescm.com,byted.net,douyinstatic.com,bytecdn.cn,byteimg.com,bytegoofy.com,toutiaostatic.com
ports:
- "8788:6080"
- "${LOGIN_DESKTOP_WEB_PORT:-8788}:6080"
command: bash /app/scripts/start_login_desktop.sh
volumes:
- ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs
- ./state/login-profile:/data/login-profile
scheduler:
image: douyin-sparkflow:local
container_name: douyin-scheduler
restart: unless-stopped
depends_on:
- web
environment:
TZ: ${TZ:-Asia/Shanghai}
PYTHONUNBUFFERED: "1"
command: python /app/scripts/cron_runner.py /host-spool-cron/root
volumes:
- ./DouYinSparkFlow:/app
- ./DouYinSparkFlow/logs:/app/logs
- ./state/cron:/host-spool-cron
- /var/run/docker.sock:/var/run/docker.sock
task:
image: douyin-sparkflow:local
container_name: douyin-task
depends_on:
- proxy
environment:
TZ: Asia/Shanghai
TZ: ${TZ:-Asia/Shanghai}
HTTP_PROXY: http://proxy:7890
HTTPS_PROXY: http://proxy:7890
ALL_PROXY: socks5://proxy:7890
+9
View File
@@ -0,0 +1,9 @@
mixed-port: 7890
allow-lan: true
bind-address: '*'
mode: rule
log-level: info
external-controller: '0.0.0.0:9090'
rules:
- MATCH,DIRECT
Regular → Executable
+67 -7
View File
@@ -1,9 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail
APP_ROOT=/opt/douyin-sparkflow
SUB_URL='https://liangxin.xyz/api/v1/liangxin?OwO=6981c5a8452d44e8521c78f9f7bf1eea'
curl -fsSL -A 'clash-verge/1.7.7' "$SUB_URL" -o "$APP_ROOT/proxy/config.yaml"
if grep -q '^allow-lan:' "$APP_ROOT/proxy/config.yaml"; then sed -i 's/^allow-lan:.*/allow-lan: true/' "$APP_ROOT/proxy/config.yaml"; else echo 'allow-lan: true' >> "$APP_ROOT/proxy/config.yaml"; fi
if grep -q '^bind-address:' "$APP_ROOT/proxy/config.yaml"; then sed -i "s#^bind-address:.*#bind-address: '*'#" "$APP_ROOT/proxy/config.yaml"; else echo "bind-address: '*'" >> "$APP_ROOT/proxy/config.yaml"; fi
if grep -q '^external-controller:' "$APP_ROOT/proxy/config.yaml"; then sed -i "s#^external-controller:.*#external-controller: '0.0.0.0:9090'#" "$APP_ROOT/proxy/config.yaml"; else echo "external-controller: '0.0.0.0:9090'" >> "$APP_ROOT/proxy/config.yaml"; fi
docker compose -f "$APP_ROOT/docker-compose.yml" restart proxy
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_ROOT_OVERRIDE="${APP_ROOT:-}"
ENV_FILE="$SCRIPT_DIR/.env"
read_env_value() {
local key="$1"
if [ ! -f "$ENV_FILE" ]; then
return 0
fi
local raw
raw="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d= -f2- || true)"
raw="${raw%\"}"
raw="${raw#\"}"
raw="${raw%\'}"
raw="${raw#\'}"
printf '%s' "$raw"
}
APP_ROOT="${APP_ROOT_OVERRIDE:-$SCRIPT_DIR}"
PROXY_SUB_URL="${PROXY_SUB_URL:-$(read_env_value PROXY_SUB_URL)}"
PROXY_USER_AGENT="${PROXY_USER_AGENT:-$(read_env_value PROXY_USER_AGENT)}"
CONFIG_DIR="$APP_ROOT/proxy"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
EXAMPLE_CONFIG="$CONFIG_DIR/config.example.yaml"
USER_AGENT="${PROXY_USER_AGENT:-clash-verge/1.7.7}"
mkdir -p "$CONFIG_DIR"
if [ -n "${PROXY_SUB_URL:-}" ]; then
tmp_file="$(mktemp)"
curl -fsSL -A "$USER_AGENT" "$PROXY_SUB_URL" -o "$tmp_file"
mv "$tmp_file" "$CONFIG_FILE"
echo "Proxy subscription refreshed: $CONFIG_FILE"
elif [ ! -f "$CONFIG_FILE" ]; then
if [ ! -f "$EXAMPLE_CONFIG" ]; then
echo "Missing $CONFIG_FILE and $EXAMPLE_CONFIG. Set PROXY_SUB_URL or provide a Mihomo config." >&2
exit 1
fi
cp "$EXAMPLE_CONFIG" "$CONFIG_FILE"
echo "PROXY_SUB_URL is empty. Created a DIRECT-only proxy config from config.example.yaml."
else
echo "PROXY_SUB_URL is empty. Keeping existing proxy/config.yaml."
fi
ensure_line() {
local key="$1"
local value="$2"
if grep -q "^${key}:" "$CONFIG_FILE"; then
sed -i "s#^${key}:.*#${key}: ${value}#" "$CONFIG_FILE"
else
printf '%s: %s\n' "$key" "$value" >> "$CONFIG_FILE"
fi
}
ensure_line "mixed-port" "7890"
ensure_line "allow-lan" "true"
ensure_line "bind-address" "'*'"
ensure_line "external-controller" "'0.0.0.0:9090'"
if command -v docker >/dev/null 2>&1 && [ -f "$APP_ROOT/docker-compose.yml" ]; then
proxy_id="$(docker compose -f "$APP_ROOT/docker-compose.yml" ps -q proxy 2>/dev/null || true)"
if [ -n "$proxy_id" ]; then
docker compose -f "$APP_ROOT/docker-compose.yml" restart proxy
fi
fi