diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1652a2b..97bd836 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,10 +19,11 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install linter - run: | - python -m pip install --upgrade pip - pip install ruff + run: uv pip install --system ruff - name: Run linter run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 075b3e2..932bdeb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,10 +18,11 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + run: uv pip install --system -r requirements.txt - name: Run tests env: diff --git a/Makefile b/Makefile index 0bcb556..a473096 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean migrate migration +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev ENV_DEV = docker/.env.dev @@ -22,6 +22,8 @@ help: @echo "make build - Build Docker images" @echo "make up - Start all containers (detached)" @echo "make down - Stop all containers" + @echo "make sync - Fast-install new requirements.txt deps into running app (no rebuild)" + @echo "make status - Show compact container health summary" @echo "make logs - Stream all container logs" @echo "make logs-app - Stream app container logs" @echo "make logs-ollama - Stream Ollama container logs" @@ -46,30 +48,38 @@ init: *) echo "Run 'make fireform' when ready." ;; \ esac -fireform: build up - @printf "Waiting for Ollama to be ready..." - @until $(COMPOSE) exec -T ollama ollama list > /dev/null 2>&1; do \ - printf '.'; sleep 2; \ - done - @echo " ready." +fireform: + @$(COMPOSE) up -d --build @if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \ echo " Model $(OLLAMA_MODEL) already pulled."; \ else \ echo " Pulling $(OLLAMA_MODEL)..."; \ $(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \ fi - @echo "" - @echo "FireForm is ready!" - @echo " API: http://localhost:8000" - @echo " API Docs: http://localhost:8000/docs" - @echo "" - @echo "Run 'make logs' to view live logs, 'make down' to stop." + @$(MAKE) --no-print-directory ready-banner build: @$(COMPOSE) build up: @$(COMPOSE) up -d + @$(MAKE) --no-print-directory ready-banner + +# Fast path for "I added a package": install the delta into the running container +# (no image rebuild, no 1.6GB layer re-export). uv installs only what's missing in +sync: + @$(COMPOSE) exec -T app sh -c "UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt" + +status: + @$(COMPOSE) ps --format 'table {{.Service}}\t{{.Status}}' + +ready-banner: + @echo "" + @echo "FireForm is ready!" + @echo " API: http://localhost:8000" + @echo " API Docs: http://localhost:8000/docs" + @echo "" + @echo "Run 'make logs' to view live logs, 'make down' to stop." down: @$(COMPOSE) down --remove-orphans diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index ad4bfd4..a5a42c1 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.11-slim WORKDIR /app -# Use apt cache mount to speed up system package installation across builds +# apt cache mounts keep downloaded .debs across builds RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt \ @@ -11,13 +11,22 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ curl \ libgl1 \ libglib2.0-0 \ - libxcb1 + libxcb1 \ + libpq-dev \ + build-essential \ + g++ + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY requirements.txt . -# Use pip cache mount so it remembers downloaded wheels -RUN --mount=type=cache,target=/root/.cache/pip \ - pip install -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt + +# Bake a hash of the deps the image was built with. The entrypoint compares this +# against the live (bind-mounted) requirements.txt to decide whether to reinstall. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash ENV PYTHONPATH=/app diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 22f8a5d..820711d 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -17,6 +17,8 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s + start_interval: 1s ollama: image: ollama/ollama:latest @@ -33,6 +35,7 @@ services: timeout: 5s retries: 5 start_period: 30s + start_interval: 2s whisper: image: onerahmet/openai-whisper-asr-webservice:latest @@ -53,6 +56,7 @@ services: timeout: 5s retries: 5 start_period: 60s + start_interval: 3s redis: image: redis:7-alpine @@ -68,6 +72,8 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s + start_interval: 1s app: build: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 8694d9f..a4832be 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,10 +1,25 @@ #!/bin/sh set -e -# Ensure data directories exist (volumes may be empty on first run) mkdir -p /data/uploads -# Run DB migrations / init before starting the server +# Reinstall deps only when the live requirements.txt differs from what the image +# was built with. The image bakes the hash at /opt/req_hash; in dev the live file +# comes from the bind mount. Matching hash => deps already baked in => skip (instant). +BAKED_HASH=$(cat /opt/req_hash 2>/dev/null || echo "none") +LIVE_HASH=$(sha256sum requirements.txt 2>/dev/null | cut -d' ' -f1 || echo "unknown") + +if [ "$BAKED_HASH" = "$LIVE_HASH" ]; then + echo "[entrypoint] dependencies up to date — skipping install" +else + echo "[entrypoint] requirements.txt changed since image build — syncing deps..." + if command -v uv > /dev/null 2>&1; then + UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt + else + pip install -r requirements.txt + fi +fi + python3 -m app.db.init_db exec "$@" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index f97d6dd..ed65a28 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,32 +1,44 @@ +# ---- builder ---- FROM python:3.11-slim AS builder -WORKDIR /build +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential g++ libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +WORKDIR /build COPY requirements.txt . -RUN pip install --no-cache-dir --prefix=/install -r requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt +# ---- runtime ---- FROM python:3.11-slim WORKDIR /app -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ libgl1 \ libglib2.0-0 \ libxcb1 \ - && rm -rf /var/lib/apt/lists/* + libpq5 && \ + rm -rf /var/lib/apt/lists/* -COPY --from=builder /install /usr/local +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin -# Copy only app code, not data/ temp/ tests/ docs/ etc. COPY app/ ./app/ COPY requirements.txt . COPY docker/entrypoint.sh /entrypoint.sh +# Bake deps hash so the entrypoint can skip the redundant install when unchanged. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash + ENV PYTHONPATH=/app -# Data dirs created here; actual storage comes from mounted volumes at runtime. RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh EXPOSE 8000 diff --git a/scripts/setup-dockers-env.sh b/scripts/setup-dockers-env.sh index 90adb5c..17418ad 100644 --- a/scripts/setup-dockers-env.sh +++ b/scripts/setup-dockers-env.sh @@ -1,4 +1,8 @@ #!/bin/bash source venv/bin/activate -pip install -r requirements.txt +if command -v uv > /dev/null 2>&1; then + uv pip install -r requirements.txt +else + pip install -r requirements.txt +fi