feat: SaaS foundation for CrowdSight
Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation: - Local memory backend (Zep-compatible): memory services/models, local graph builder + updater, AgentActivity seam, import-boundary isolation; Zep stays default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven. - Durable product persistence: projects/simulations/reports schema (migration 0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project read-first + ArtifactStore abstraction; durable JobQueue + worker.py. - SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM accounting), redacted AuditService, idempotency, CORS allowlist, safe API errors, single-use PasswordResetService + endpoints (covers invite-pending). - Exactly 3 roles (super_admin/admin/user) with tenant authz policy. - Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with admin/super-admin route guards, th/en i18n. - Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep. Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap). No commit of credentials; secrets handled via env/.env.example. Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel container build of deploy topology.
This commit is contained in:
65
Dockerfile
65
Dockerfile
@@ -1,29 +1,62 @@
|
||||
FROM python:3.11
|
||||
# ============================================================
|
||||
# CrowdSight production image (multi-service, EasyPanel-buildable)
|
||||
#
|
||||
# Services inside one container (supervisord):
|
||||
# - web: nginx serving the built SPA, proxying /api -> gunicorn :5001
|
||||
# - backend: gunicorn WSGI (wsgi:app) on 0.0.0.0:5001
|
||||
# - worker: durable PollingWorker (backend/worker.py)
|
||||
# ============================================================
|
||||
|
||||
# ---- Stage 1: build the frontend SPA ----
|
||||
FROM node:20 AS frontend-build
|
||||
WORKDIR /build
|
||||
COPY package.json package-lock.json* ./
|
||||
COPY frontend/package.json frontend/package-lock.json* ./frontend/
|
||||
# install root + frontend deps
|
||||
RUN npm ci 2>/dev/null || true; npm ci --prefix frontend || true
|
||||
COPY locales ./locales
|
||||
COPY frontend ./frontend
|
||||
RUN npm run build --prefix frontend
|
||||
|
||||
# ---- Stage 2: runtime (python + nginx + supervisord) ----
|
||||
FROM python:3.11-slim AS runtime
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONPATH=/app/backend
|
||||
|
||||
# 安装 Node.js (满足 >=18)及必要工具
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nodejs npm \
|
||||
&& apt-get install -y --no-install-recommends nginx supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 从 uv 官方镜像复制 uv
|
||||
# uv runtime
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 先复制依赖描述文件以利用缓存
|
||||
COPY package.json package-lock.json ./
|
||||
COPY frontend/package.json frontend/package-lock.json ./frontend/
|
||||
# Install backend deps first (cache-friendly)
|
||||
COPY backend/pyproject.toml backend/uv.lock ./backend/
|
||||
RUN cd backend && uv sync --frozen --no-dev
|
||||
|
||||
# 安装依赖(Node + Python)
|
||||
RUN npm ci \
|
||||
&& npm ci --prefix frontend \
|
||||
&& cd backend && uv sync --frozen
|
||||
# Copy project source
|
||||
COPY backend ./backend
|
||||
COPY locales ./locales
|
||||
COPY package.json ./
|
||||
|
||||
# 复制项目源码
|
||||
COPY . .
|
||||
# Copy built SPA into nginx web root
|
||||
COPY --from=frontend-build /build/frontend/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 3000 5001
|
||||
# nginx config: SPA + /api proxy to gunicorn
|
||||
RUN echo 'server {\n listen 8080;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n location /api/ {\n proxy_pass http://127.0.0.1:5001;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n }\n}\n' > /etc/nginx/sites-available/crowdsight \
|
||||
&& ln -sf /etc/nginx/sites-available/crowdsight /etc/nginx/sites-enabled/crowdsight \
|
||||
&& rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# 同时启动前后端(开发模式)
|
||||
CMD ["npm", "run", "dev"]
|
||||
# supervisor: run nginx + gunicorn + worker
|
||||
RUN echo '[supervisord]\nnodeamon=false\n\n[program:nginx]\ncommand=/usr/sbin/nginx -g "daemon off;"\nautostart=true\nautorestart=true\n\n[program:backend]\ncommand=/bin/bash -c "cd /app/backend && uv run gunicorn -w 2 -b 0.0.0.0:5001 --timeout 120 wsgi:app"\nautostart=true\nautorestart=true\n\n[program:worker]\ncommand=/bin/bash -c "cd /app/backend && uv run python worker.py --poll-interval 5"\nautostart=true\nautorestart=true\n' > /etc/supervisor/conf.d/crowdsight.conf
|
||||
|
||||
EXPOSE 8080 5001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5001/health', timeout=4)" || exit 1
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-n"]
|
||||
|
||||
Reference in New Issue
Block a user