Compose derives project name from parent dir by default — every app on this server landed on project "deploy", sharing container names and volume namespaces. Another app on the box just lost 2 days of data from this exact issue. Fix: name: salary-benchmark in docker-compose.prod.yml, plus -p on every docker compose call in deploy-local.sh and deploy.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.4 KiB
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
- 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. Needsdeploy/config.shwithSSH_TARGETset. 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:
- Scans
ss -tlnHfor a free port in 8100–8199 for the backend - Builds the frontend inside a
node:20-alpinecontainer withVITE_BASE=/salary-benchmark/— no npm on server required - Rsyncs
frontend/dist/to/var/www/html/salary-benchmark/ - Writes
deploy/.env.prod(generatesJWT_SECRET+DB_PASSWORDon first run; preserves them on re-runs; pulls API keys from.env) - Builds + starts the Docker stack (
docker compose -f deploy/docker-compose.prod.yml up -d) - Alembic migrations run on app container start
- Renders
deploy/apache-salary-benchmark.confwith the chosen port - Adds
Include /opt/salary-benchmark/deploy/apache-salary-benchmark.confto the vhost (first run only; backs it up first), runsapache2ctl configtest, reloads Apache - Curls
/salary-benchmark/api/healthto 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,headersmodules - 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):
name: salary-benchmarkat the top ofdeploy/docker-compose.prod.yml-p salary-benchmarkon everydocker composeinvocation indeploy-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 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/loginwith an OIDC callback that validates the Microsoft token and issues the same internal JWTfrontend/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.