# Salary Benchmark Tool FastAPI + React + PostgreSQL app with an AI research pipeline (Serper + Firecrawl + Cohere + Claude) for benchmarking salaries. --- ## Local development Requires Docker. ```bash cp .env.example .env # fill in API keys docker compose up -d ``` - Frontend: http://localhost:5179 - Backend: http://localhost:8009 - DB: 127.0.0.1:5436 (inside docker network: `db:5432`) **Login** (seeded by migration 004): `admin@oliver.agency` / `Oliver2026!` Migrations run automatically on app container start (`alembic upgrade head`). If your local DB drifts from the migration chain, reset it with `docker compose down -v && docker compose up -d`. --- ## Deploying to optical-dev The target server is `optical-dev.oliver.solutions`. The app is served at **https://optical-dev.oliver.solutions/salary-benchmark/**. ### Which deploy script? Two scripts ship with the repo — use one or the other, not both: - **`deploy/deploy-local.sh`** — run this **on the server**, from the cloned repo at `/opt/salary-benchmark/`. This is the path we use in practice. Requires being SSH'd in as root (or a sudoer). Builds the frontend in a node container so the server doesn't need npm. - **`deploy/deploy.sh`** — alternative: run on your laptop, it SSHes in and rsyncs. Needs `deploy/config.sh` with `SSH_TARGET` set. Handy if you don't want to commit-push-pull for every change, but not needed if you're already on the server. The rest of this section assumes **`deploy-local.sh`** (on-server). ### Architecture on the server - Code: `/opt/salary-benchmark/` (git checkout) - Built frontend: `/var/www/html/salary-benchmark/` (served directly by Apache) - Backend: Docker container bound to `127.0.0.1:`; Apache proxies `/salary-benchmark/api/` to it - Postgres: Docker container, no host port - Apache fragment: `/opt/salary-benchmark/deploy/apache-salary-benchmark.conf`, included from the main vhost ### First-time deploy (on the server as root) ```bash cd /opt git clone https://bitbucket.org/zlalani/salary-benchmark.git cd salary-benchmark cp .env.example .env # fill in SERPER / FIRECRAWL / COHERE / ANTHROPIC API keys ./deploy/deploy-local.sh ``` What `deploy-local.sh` does: 1. Scans `ss -tlnH` for a free port in **8100–8199** for the backend 2. Builds the frontend inside a `node:20-alpine` container with `VITE_BASE=/salary-benchmark/` — no npm on server required 3. Rsyncs `frontend/dist/` to `/var/www/html/salary-benchmark/` 4. Writes `deploy/.env.prod` (generates `JWT_SECRET` + `DB_PASSWORD` on first run; preserves them on re-runs; pulls API keys from `.env`) 5. Builds + starts the Docker stack (`docker compose -f deploy/docker-compose.prod.yml up -d`) 6. Alembic migrations run on app container start 7. Renders `deploy/apache-salary-benchmark.conf` with the chosen port 8. Adds `Include /opt/salary-benchmark/deploy/apache-salary-benchmark.conf` to the vhost (first run only; backs it up first), runs `apache2ctl configtest`, reloads Apache 9. Curls `/salary-benchmark/api/health` to verify ### Redeploys ```bash cd /opt/salary-benchmark git pull ./deploy/deploy-local.sh ``` Idempotent — safe to re-run. Existing `JWT_SECRET`, `DB_PASSWORD`, and DB data are preserved. Frontend is rebuilt; backend image is rebuilt; containers are recreated only if their config changes. ### Configuration overrides Environment variables you can set when calling `deploy-local.sh`: | Variable | Default | |-------------------|----------------------------------------------------------------| | `URL_SUBPATH` | `/salary-benchmark/` | | `WEB_DIR` | `/var/www/html/salary-benchmark` | | `VHOST_FILE` | `/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf` | | `PORT_SCAN_START` | `8100` | | `PORT_SCAN_END` | `8199` | Example: ```bash PORT_SCAN_START=8200 PORT_SCAN_END=8299 ./deploy/deploy-local.sh ``` ### Server prerequisites (already in place on optical-dev) - Docker + Docker Compose - Apache with `proxy`, `proxy_http`, `rewrite`, `headers` modules - Existing vhost at `/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf` - Outbound HTTPS (to pull node/python base images, npm/pip packages) ### Compose project name — important on shared servers On `optical-dev`, multiple apps each live at `/opt//deploy/` with their own `docker-compose.prod.yml`. Docker Compose derives the project name from the parent dir unless told otherwise — so **every** app would default to project `deploy`, sharing container names and volume namespaces. That's how another app on this box recently lost 2 days of data. We pin it two ways (belt-and-braces): 1. `name: salary-benchmark` at the top of `deploy/docker-compose.prod.yml` 2. `-p salary-benchmark` on every `docker compose` invocation in `deploy-local.sh` When running compose commands manually on the server, always include `-p salary-benchmark`: ```bash cd /opt/salary-benchmark/deploy docker compose -p salary-benchmark -f docker-compose.prod.yml --env-file .env.prod ps docker compose -p salary-benchmark -f docker-compose.prod.yml --env-file .env.prod logs app --tail 50 ``` **Never run `docker compose down -v`** — `-v` deletes volumes. If another app is (or was) sharing the `deploy_*` volume namespace you can destroy their data. If you genuinely need to wipe this app's DB, target the specific volume: `docker volume rm salary-benchmark_pgdata`. ### Rollback ```bash # restore the previous vhost (backup was created with a timestamp suffix) ls /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf.bak.* sudo cp /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf sudo apache2ctl configtest && sudo systemctl reload apache2 # stop containers cd /opt/salary-benchmark/deploy docker compose -f docker-compose.prod.yml --env-file .env.prod down ``` --- ## Troubleshooting **Backend container won't start** — check logs: ```bash cd /opt/salary-benchmark/deploy docker compose -f docker-compose.prod.yml --env-file .env.prod logs app --tail 50 ``` **Apache config test fails after the Include is added** — restore the backup: ```bash sudo cp /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf.bak. /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf sudo apache2ctl configtest ``` **404 on `/salary-benchmark/` but `/salary-benchmark/api/health` works** — frontend build didn't copy. Re-run the deploy script; check that `/var/www/html/salary-benchmark/index.html` exists. **Login fails with correct password** — migration 004 may not have run. Check: ```bash docker compose -f docker-compose.prod.yml --env-file .env.prod exec db \ psql -U salary_user -d salary_benchmark -c "SELECT email FROM users;" ``` **Port conflict** — the script scans 8100–8199. To see what's bound: `ss -tlnp`. Override the scan range via `PORT_SCAN_START`/`PORT_SCAN_END`. --- ## Later: Azure SSO When moving from local auth to Microsoft Azure SSO, the isolated swap points are: - `app/routers/auth.py` — replace `/login` with an OIDC callback that validates the Microsoft token and issues the same internal JWT - `frontend/src/pages/LoginPage.jsx` — replace the form with an MSAL "Sign in with Microsoft" button The `AuthContext`, `ProtectedRoute`, JWT middleware, and API Authorization header plumbing stay unchanged.