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:
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# service_run.sh — Docker 入口
|
||||
#
|
||||
# 单进程跑:FastAPI dashboard(uvicorn)+ 进程内 scheduler(worker)
|
||||
# 用 uvicorn 主进程 + 一个 daemon thread 跑 worker 的 scheduler 循环。
|
||||
#
|
||||
# 设计原因:docker-compose 一容器一服务最简单,systemd timer 的事交给
|
||||
# 进程内 scheduler(避免 docker 里跑 systemd)。原来 .env 里的 schedule
|
||||
# 表就是为这个准备的。
|
||||
|
||||
set -eu
|
||||
|
||||
# 等 PG 就绪(避免 race:market_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
|
||||
Reference in New Issue
Block a user