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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
+3
-1
@@ -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"]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user