From b8238e03eacb15ae3e037a4347012bd65284ba4a Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 14 Apr 2026 05:32:43 +0200 Subject: [PATCH] Fix prod startup: add start.sh for backend, fix documents proxy base route - backend/Dockerfile: run migrations via start.sh before uvicorn instead of launching uvicorn directly (prod was skipping Alembic) - backend/scripts/start.sh: alembic upgrade head + uvicorn exec - documents_proxy.py: add explicit "" route so GET /api/documents (no trailing slash) returns 200 instead of 307 redirect - README.md: update Containers table, volumes section, and Current State to reflect the new 4-container architecture with doc-service Co-Authored-By: Claude Sonnet 4.6 --- README.md | 59 +++++++++++++++++++------- backend/Dockerfile | 4 +- backend/app/routers/documents_proxy.py | 3 +- backend/scripts/start.sh | 8 ++++ 4 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 backend/scripts/start.sh diff --git a/README.md b/README.md index 53ee506..87bb741 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,23 @@ A fullstack SaaS web application built with FastAPI, React, and PostgreSQL. | Layer | Tech | |---|---| | Backend | FastAPI (async), SQLAlchemy 2, Alembic, PostgreSQL 16 | -| Auth | JWT bearer tokens, bcrypt password hashing | +| Auth | JWT bearer tokens (RS256), bcrypt password hashing | | Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query | ## Current State -- User registration and login (JWT auth) -- Protected dashboard with nav bar (Dashboard | Profile | Logout) +- User registration and login (JWT RS256 auth, 8-hour expiry) +- Protected dashboard with nav bar - `/api/users/me` — authenticated user info - `/api/profile/me` — GET/PUT personal profile (position, phone, date of birth, address) -- Profile data stored in a dedicated `profiles` table; auto-created on first access -- Admin role flag (`is_superuser`) stored in `users` table; exposed as `is_admin` in API (false for regular users, true for admins) -- Admin-only user management at `/admin`: list all users, add users, delete users, toggle active status +- Admin-only user management at `/admin`: list, add, delete, toggle active - All input sanitized before reaching the DB (null-byte rejection, length caps, format validation) -- 3 separate Docker containers: `db` (PostgreSQL), `backend` (FastAPI), `frontend` (nginx) -- All containers run as non-root users (UID 1001 for backend and frontend, UID 70 for db) -- Network-isolated: only the frontend exposes a host port (80/5173); db and backend are unreachable from outside Docker +- **PDF Documents app** (`/apps/documents`): upload PDFs, async text extraction (pdfplumber), AI classification via Anthropic / Ollama / LM Studio, per-user categories, file download +- Admin settings per app at `/apps/documents/settings/admin`: AI provider (cloud or local), upload limits; config stored in `/config/doc_service_config.json` on a shared Docker volume +- `/apps` launcher hub — one card per installed app with Open + Settings links +- 4 separate Docker containers: `db`, `backend`, `doc-service`, `frontend` +- All containers run as non-root users (UID 1001 for app containers, UID 70 for db) +- Network-isolated: only the frontend exposes a host port (80/5173); all backend services are unreachable from outside Docker - Dev environment seeds a test user automatically on startup (`test@example.com` / `Test123!`) - Password policy: min 8 chars, upper + lowercase, digit, special character, no common words - Pre-commit security hook (`scripts/security_check.py`) runs inside Docker on every commit @@ -32,14 +33,20 @@ A fullstack SaaS web application built with FastAPI, React, and PostgreSQL. | Container | Image | Host port | Network | User (UID:GID) | Description | |---|---|---|---|---|---| | `db` | postgres:16-alpine | none | backend-net | 70:70 | PostgreSQL database | -| `backend` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | FastAPI management API | +| `backend` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | FastAPI management API + proxy to doc-service | +| `doc-service` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | PDF extraction microservice | | `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy | **Networks:** -- `backend-net` (`internal: true`) — db, backend, and frontend reverse proxy communicate here; no host routing +- `backend-net` — db, backend, doc-service, and frontend reverse proxy; no host ports bound; outbound internet access allowed (needed for cloud AI API calls) - `frontend-net` — frontend only; this is where the single host port (80/5173) is bound -The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. No backend or database port is ever exposed to the host. +**Volumes:** +- `postgres_data` — PostgreSQL data files +- `doc_data` — uploaded PDF files (mounted into doc-service at `/data/documents`) +- `app_config` — per-service runtime config JSON files (mounted into backend and doc-service at `/config`) + +The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. The backend proxies `/api/documents/*` and `/api/documents/categories/*` to `doc-service:8001`. No backend, doc-service, or database port is ever exposed to the host. ## Installation @@ -58,7 +65,9 @@ docker compose up --build -d ``` - Frontend: http://localhost -- API docs: not directly accessible from host (backend port not exposed); access via `docker compose exec backend` or add a dev-only port mapping +- API docs: not directly accessible from host (backend port not exposed) + +After first start, configure the AI provider at `/apps/documents/settings/admin` (admin login required). ### Development (hot reload) @@ -81,14 +90,24 @@ docker compose up db -d ```bash cd backend -python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate +python -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" cp ../.env.example .env alembic upgrade head uvicorn app.main:app --reload ``` -**3. Frontend** +**3. doc-service** + +```bash +cd features/doc-service +python -m venv .venv && source .venv/bin/activate +pip install -e . +alembic upgrade head +uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload +``` + +**4. Frontend** ```bash cd frontend && npm install && npm run dev @@ -104,6 +123,10 @@ Copy `.env.example` to `backend/.env` and adjust: | `JWT_PRIVATE_KEY` | — | RS256 private key PEM (generate with `scripts/generate_jwt_keys.py`) | | `JWT_PUBLIC_KEY` | — | RS256 public key PEM (generate with `scripts/generate_jwt_keys.py`) | | `CORS_ORIGINS` | `["http://localhost:5173"]` | Allowed frontend origins | +| `APP_CONFIG_DIR` | `/config` | Directory for per-service runtime config JSON files | +| `DOC_SERVICE_URL` | `http://doc-service:8001` | Internal URL of the doc-service (set by docker-compose) | + +`doc-service` reads `DATABASE_URL`, `DATA_DIR`, and `CONFIG_PATH` from its own environment (set in `docker-compose.yml`). ## Development @@ -117,7 +140,11 @@ cd backend && pytest # Frontend type check + lint cd frontend && npm run typecheck && npm run lint -# New DB migration (after changing models) +# New DB migration — main backend cd backend && alembic revision --autogenerate -m "describe change" cd backend && alembic upgrade head + +# New DB migration — doc-service +cd features/doc-service && alembic revision --autogenerate -m "describe change" +cd features/doc-service && alembic upgrade head ``` diff --git a/backend/Dockerfile b/backend/Dockerfile index 5e37208..08a06e7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -24,9 +24,11 @@ COPY --from=builder /install /usr/local COPY --chown=appuser:appuser app ./app COPY --chown=appuser:appuser alembic ./alembic COPY --chown=appuser:appuser alembic.ini . +COPY --chown=appuser:appuser scripts ./scripts +RUN chmod +x scripts/start.sh USER appuser EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["sh", "scripts/start.sh"] diff --git a/backend/app/routers/documents_proxy.py b/backend/app/routers/documents_proxy.py index 4d8f697..2dbc0d4 100644 --- a/backend/app/routers/documents_proxy.py +++ b/backend/app/routers/documents_proxy.py @@ -46,11 +46,12 @@ def _forward_headers(request: Request, user_id: str) -> dict: return headers +@router.api_route("", methods=["GET", "POST"]) @router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def proxy_documents( - path: str, request: Request, current_user: User = Depends(get_current_user), + path: str = "", ) -> StreamingResponse: url = f"/documents/{path}" if path else "/documents" headers = _forward_headers(request, str(current_user.id)) diff --git a/backend/scripts/start.sh b/backend/scripts/start.sh new file mode 100644 index 0000000..f58c8c9 --- /dev/null +++ b/backend/scripts/start.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +echo "[start] running migrations..." +alembic upgrade head + +echo "[start] starting uvicorn..." +exec uvicorn app.main:app --host 0.0.0.0 --port 8000