camel-oasis (dep of camel-ai/sentence-transformers) pulls in torch on linux-x86_64, which drags several GB of nvidia-cuda-* / cudnn / triton packages into the production image even though this deployment never runs an LLM locally — all LLM calls go through an API (OpenAI-compatible) and the server has no GPU. The nvidia-* packages are pure bloat. After 'uv sync', uninstall all nvidia-* runtime libs + triton (torch itself stays as a CPU runtime). Then verify the stripped env still imports psycopg, torch, sentence-transformers and the app, so the build fails loudly if the strip breaks anything instead of failing silently at container runtime. Verified locally (no CUDA libs present): psycopg/torch/sentence_transformers/ create_app all import fine.
83 lines
4.8 KiB
Docker
83 lines
4.8 KiB
Docker
# ============================================================
|
|
# 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
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends nginx supervisor \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# uv runtime
|
|
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy project source FIRST so the installed deps match the shipped
|
|
# pyproject.toml/uv.lock exactly (avoids `uv run` re-syncing at container
|
|
# runtime, which previously rebuilt the project every time the entrypoint
|
|
# ran and missed the psycopg Postgres driver -> "NoSuchModuleError:
|
|
# sqlalchemy.dialects:postgres").
|
|
COPY backend ./backend
|
|
COPY locales ./locales
|
|
COPY package.json ./
|
|
|
|
# Install backend deps against the final pyproject.toml/uv.lock, then strip the
|
|
# NVIDIA CUDA runtime libraries that torch drags in (via camel-oasis ->
|
|
# sentence-transformers). The server has no GPU and all LLM calls go through an
|
|
# API, so CUDA libs are pure bloat (many GB). torch itself stays (CPU runtime)
|
|
# but without the nvidia-* packages the image is dramatically smaller. We then
|
|
# verify the stripped env still imports torch/sentence-transformers and the app
|
|
# so the build fails loudly instead of a silent runtime break.
|
|
RUN cd backend && uv sync --frozen --no-dev \
|
|
&& uv pip uninstall \
|
|
nvidia-cublas-cu12 nvidia-cuda-cupti-cu12 nvidia-cuda-nvrtc-cu12 \
|
|
nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 nvidia-cufft-cu12 \
|
|
nvidia-cufile-cu12 nvidia-curand-cu12 nvidia-cusolver-cu12 \
|
|
nvidia-cusparse-cu12 nvidia-cusparselt-cu12 nvidia-nccl-cu12 \
|
|
nvidia-nvjitlink-cu12 nvidia-nvshmem-cu12 nvidia-nvtx-cu12 triton \
|
|
&& uv run --frozen python -c "import psycopg, torch; import sentence_transformers; from app import create_app; print('deps-after-strip-ok')" \
|
|
|| (echo "FATAL: dependency import failed after CUDA strip (torch/sentence-transformers/app). Revisit the nvidia-* uninstall list." >&2 && exit 1)
|
|
|
|
# Make the migration-runner entrypoint executable
|
|
RUN chmod +x /app/backend/docker_entrypoint.sh
|
|
|
|
# Copy built SPA into nginx web root
|
|
COPY --from=frontend-build /build/frontend/dist /usr/share/nginx/html
|
|
|
|
# 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
|
|
|
|
# supervisor: run nginx + gunicorn + worker (migrations already run by entrypoint)
|
|
RUN echo '[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:nginx]\ncommand=/usr/sbin/nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:backend]\ncommand=/bin/bash -c "cd /app/backend && uv run --frozen gunicorn -w 2 -b 0.0.0.0:5001 --timeout 120 wsgi:app"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:worker]\ncommand=/bin/bash -c "cd /app/backend && uv run --frozen python worker.py --poll-interval 5"\nautostart=true\nautorestart=true\nstartsecs=2\nstartretries=5\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\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 ["/app/backend/docker_entrypoint.sh"]
|