feat(service): dockerize — Dockerfile + docker-compose + 统一入口

把项目打包成可部署的数据同步服务:

- Dockerfile (multi-stage builder/runtime, slim base, 含 healthcheck)
- docker-compose.yml (postgres:16-alpine + market_sync, 持久化 pgdata + logs)
- .dockerignore (减 build context)
- bin/service_run.sh (单进程跑 uvicorn + scheduler 线程)
- app/entrypoints/worker.py 抽 start_scheduler_thread() (幂等)

部署:
  docker compose up -d
  # 然后访问 http://localhost:8100/docs 看 FastAPI

设计选择:
  - 单容器一服务(uvicorn 主 + scheduler daemon thread),不用 supervisord
  - 原 systemd timer 由进程内 scheduler 取代(配置已存在于 config 表)
  - PG 用 docker volume 持久化;同步日志单独 volume
This commit is contained in:
gao
2026-07-07 16:30:56 +08:00
parent a8ea132924
commit fbc3cc84f0
5 changed files with 299 additions and 1 deletions
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# service_run.sh — Docker 入口
#
# 单进程跑:FastAPI dashboarduvicorn+ 进程内 schedulerworker
# 用 uvicorn 主进程 + 一个 daemon thread 跑 worker 的 scheduler 循环。
#
# 设计原因:docker-compose 一容器一服务最简单,systemd timer 的事交给
# 进程内 scheduler(避免 docker 里跑 systemd)。原来 .env 里的 schedule
# 表就是为这个准备的。
set -eu
# 等 PG 就绪(避免 racemarket_sync 比 postgres 早启)
if [ -n "${PG_HOST:-}" ]; then
echo "[service] wait for PG ${PG_HOST}:${PG_PORT:-5432}..."
for i in $(seq 1 30); do
if python -c "
import socket, sys
s = socket.socket()
s.settimeout(2)
try:
s.connect(('${PG_HOST}', ${PG_PORT:-5432}))
print('pg ready')
except Exception as e:
sys.exit(1)
" >/dev/null 2>&1; then
echo "[service] PG up after ${i}s"
break
fi
sleep 1
done
fi
# seed datasource + sync registry + schedule(幂等)
python -c "
from app.core.datasource.registry import build_default_registry, seed_datasource_configs
from app.core.sync.registry import seed_sync_registry, recover_interrupted_syncs
from app.core.scheduler.scheduler import seed_schedule_configs
build_default_registry()
seed_datasource_configs()
seed_sync_registry()
recover_interrupted_syncs()
seed_schedule_configs()
print('[service] seed 完成')
"
# 后台启动 scheduler(独立线程)
python -c "
from app.entrypoints.worker import start_scheduler_thread
start_scheduler_thread()
print('[service] scheduler thread started')
import time
while True:
time.sleep(60)
" &
SCHEDULER_PID=$!
# trap SIGTERM 优雅退出
trap "echo '[service] stopping scheduler'; kill ${SCHEDULER_PID} 2>/dev/null || true; wait" TERM INT
# 前台跑 uvicorn(主进程)
echo "[service] starting uvicorn on ${API_HOST:-0.0.0.0}:${API_PORT:-8100}"
exec uvicorn app.api.main:app \
--host "${API_HOST:-0.0.0.0}" \
--port "${API_PORT:-8100}" \
--workers 1 \
--log-level "${LOG_LEVEL:-info}" \
--no-access-log