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 |
|
| Layer | Tech |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend | FastAPI (async), SQLAlchemy 2, Alembic, PostgreSQL 16 |
|
| 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 |
|
| Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query |
|
||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
- User registration and login (JWT auth)
|
- User registration and login (JWT RS256 auth, 8-hour expiry)
|
||||||
- Protected dashboard with nav bar (Dashboard | Profile | Logout)
|
- Protected dashboard with nav bar
|
||||||
- `/api/users/me` — authenticated user info
|
- `/api/users/me` — authenticated user info
|
||||||
- `/api/profile/me` — GET/PUT personal profile (position, phone, date of birth, address)
|
- `/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-only user management at `/admin`: list, add, delete, toggle active
|
||||||
- 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
|
|
||||||
- All input sanitized before reaching the DB (null-byte rejection, length caps, format validation)
|
- All input sanitized before reaching the DB (null-byte rejection, length caps, format validation)
|
||||||
- 3 separate Docker containers: `db` (PostgreSQL), `backend` (FastAPI), `frontend` (nginx)
|
- **PDF Documents app** (`/apps/documents`): upload PDFs, async text extraction (pdfplumber), AI classification via Anthropic / Ollama / LM Studio, per-user categories, file download
|
||||||
- All containers run as non-root users (UID 1001 for backend and frontend, UID 70 for db)
|
- 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
|
||||||
- Network-isolated: only the frontend exposes a host port (80/5173); db and backend are unreachable from outside Docker
|
- `/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!`)
|
- 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
|
- 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
|
- 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 |
|
| Container | Image | Host port | Network | User (UID:GID) | Description |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `db` | postgres:16-alpine | none | backend-net | 70:70 | PostgreSQL database |
|
| `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 |
|
| `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy |
|
||||||
|
|
||||||
**Networks:**
|
**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
|
- `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
|
## Installation
|
||||||
|
|
||||||
@@ -58,7 +65,9 @@ docker compose up --build -d
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Frontend: http://localhost
|
- 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)
|
### Development (hot reload)
|
||||||
|
|
||||||
@@ -81,14 +90,24 @@ docker compose up db -d
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
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]"
|
pip install -e ".[dev]"
|
||||||
cp ../.env.example .env
|
cp ../.env.example .env
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
uvicorn app.main:app --reload
|
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
|
```bash
|
||||||
cd frontend && npm install && npm run dev
|
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_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`) |
|
| `JWT_PUBLIC_KEY` | — | RS256 public key PEM (generate with `scripts/generate_jwt_keys.py`) |
|
||||||
| `CORS_ORIGINS` | `["http://localhost:5173"]` | Allowed frontend origins |
|
| `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
|
## Development
|
||||||
|
|
||||||
@@ -117,7 +140,11 @@ cd backend && pytest
|
|||||||
# Frontend type check + lint
|
# Frontend type check + lint
|
||||||
cd frontend && npm run typecheck && npm run 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 revision --autogenerate -m "describe change"
|
||||||
cd backend && alembic upgrade head
|
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 app ./app
|
||||||
COPY --chown=appuser:appuser alembic ./alembic
|
COPY --chown=appuser:appuser alembic ./alembic
|
||||||
COPY --chown=appuser:appuser alembic.ini .
|
COPY --chown=appuser:appuser alembic.ini .
|
||||||
|
COPY --chown=appuser:appuser scripts ./scripts
|
||||||
|
RUN chmod +x scripts/start.sh
|
||||||
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
EXPOSE 8000
|
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
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
@router.api_route("", methods=["GET", "POST"])
|
||||||
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||||
async def proxy_documents(
|
async def proxy_documents(
|
||||||
path: str,
|
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
|
path: str = "",
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
url = f"/documents/{path}" if path else "/documents"
|
url = f"/documents/{path}" if path else "/documents"
|
||||||
headers = _forward_headers(request, str(current_user.id))
|
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