No description
Find a file
DJP e7e177082d Mark pgdata volume external so compose stops warning about pre-existing volume
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:20:50 -04:00
alembic Migration 005: reseed benchmarks with canonical 67-entry NYC dataset 2026-04-17 20:11:12 -04:00
app Add login (JWT + local admin user) and deploy script for optical-dev 2026-04-17 19:34:15 -04:00
deploy Mark pgdata volume external so compose stops warning about pre-existing volume 2026-04-20 12:20:50 -04:00
frontend Move URL subpath from /opt/ to /salary-benchmark/ 2026-04-17 20:06:56 -04:00
.env.example Add login (JWT + local admin user) and deploy script for optical-dev 2026-04-17 19:34:15 -04:00
.gitignore Add login (JWT + local admin user) and deploy script for optical-dev 2026-04-17 19:34:15 -04:00
alembic.ini Initial commit: Salary Benchmark Tool 2026-04-02 22:47:32 -04:00
docker-compose.yml Remap host ports to avoid collisions (frontend 5179, app 8009) 2026-04-17 17:04:48 -04:00
Dockerfile Initial commit: Salary Benchmark Tool 2026-04-02 22:47:32 -04:00
index (7).html Initial commit: Salary Benchmark Tool 2026-04-02 22:47:32 -04:00
README.md Pin compose project name to avoid collision with other /opt/<app>/deploy/ apps 2026-04-20 11:39:49 -04:00
requirements.txt Add login (JWT + local admin user) and deploy script for optical-dev 2026-04-17 19:34:15 -04:00

Salary Benchmark Tool

FastAPI + React + PostgreSQL app with an AI research pipeline (Serper + Firecrawl + Cohere + Claude) for benchmarking salaries.


Local development

Requires Docker.

cp .env.example .env          # fill in API keys
docker compose up -d

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:<free-port>; 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)

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 81008199 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

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:

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/<app>/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:

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

# restore the previous vhost (backup was created with a timestamp suffix)
ls /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf.bak.*
sudo cp <that-file> /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:

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:

sudo cp /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf.bak.<timestamp> /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:

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 81008199. 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.