Bert is a self-hosted job-search workspace for collecting vacancies, filtering and ranking results, tracking applications, and comparing jobs with candidate CVs.
It supports separate user workspaces, scheduled searches, profile-specific rules, notifications, and an administrator area for system management.
- Search Profiles and independently scheduled Search Jobs
- Job Fit, Language Fit, Overall Fit, and evidence-based CV Match
- German, English, mixed, and unknown job-ad language detection
- Per-search preferred or strict working-time handling for full-time, part-time, Werkstudent, and Minijob searches
- Bilingual role-first query planning, structural role relevance, and Strong / Match / Stretch result tiers
- Candidate Profiles with encrypted CV text
- Optional local Ollama context for CV analysis
- Application workspace with Kanban/list views, follow-up dates, contacts, activity history, and source conversion
- Manual vacancy capture with profile-aware Job Fit and Language Fit scoring
- Explicit Markdown handoff of selected applications to career-ops
- User-specific learning, notifications, searches, applications, and candidate data
- Admin-managed registration, account activation, sessions, logs, backups, and updates
- Responsive desktop, tablet, and mobile interface
- Persistent SQLite storage in the Docker
/datavolume
Stable integrations:
- Arbeitnow
- Adzuna
- Jooble
- Greenhouse
- Lever
- SmartRecruiters
- RSS and Atom feeds
Experimental integrations:
- JobSpy for LinkedIn, Indeed, Google Jobs, and Glassdoor
- StepStone Germany
- Kleinanzeigen Jobs
Experimental sources can be affected by rate limits, anti-bot controls, or upstream page changes. Bert does not bypass CAPTCHAs or other access controls.
Bert treats provider search results as discovery candidates, not as confirmed matches. Each vacancy passes through separate stages so a high language, location, or skill score cannot rescue an unrelated occupation:
- Query planning removes location and working-time constraints from base role queries, distributes the first provider queries across requested role families, and adds English/German variants before narrower schedule variants. Custom Search Job queries remain isolated from the profile's other provider queries.
- Role relevance requires a requested phrase or role family in the title. A generic professional title such as
Engineer IIis accepted only when its description supplies both role-family and industrial-domain evidence. Conflicting software/data occupations are rejected unless supported by multiple manufacturing signals. - Independent dimensions rank Job Fit, Language Fit, working-time fit, learned preferences, and optional CV Match.
- Result tiers expose
Strong,Match, andStretchvacancies in Job Review. Rows without structural role evidence—or rows that fail an explicitly strict eligibility rule—are marked excluded and are not shown, even when their old Overall Fit would have crossed the threshold.
The CV Match threshold becomes a hard gate only when the provider supplied a sufficiently complete description.
Title-relevant vacancies with a short card/snippet remain visible as Stretch with a deferred-CV explanation instead
of being rejected for evidence the source did not provide.
Search Jobs default to Prefer profile hours; keep stretch roles. This keeps a strong technical vacancy visible when it is full-time or its hours are missing, while labeling the constraint for review; it can still be notified when the other configured thresholds pass. Select Strictly exclude other/unknown hours when the working arrangement is a hard requirement; those vacancies are excluded from that Search Job's review queue and notifications. Student-only vacancies are always excluded unless the profile explicitly targets enrolled-student work.
During the database migration, existing profile scores are re-evaluated by the role gate. Run each Search Job once after updating to fetch fresh source data and populate the new match tiers. For a profile with four target role families, six provider queries cover every base role plus two translated variants; use eight when provider capacity permits to cover both English and German for all four families.
Requirements:
- Docker with Docker Compose
- Port
8080, or a custom port mapping - A stable
APP_SECRET_KEY
Clone the repository and prepare the environment:
git clone https://github.com/emnl51/bert.git
cd bert
cp .env.example .env
python3 -c "import secrets; print(secrets.token_urlsafe(48))"Set at least these values in .env:
ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-a-long-unique-password
APP_SECRET_KEY=replace-with-the-generated-secretDo not set APP_VERSION in .env. Release versions are embedded into the
container image at build time; a runtime override can otherwise make the UI
report a stale version.
Start Bert:
docker compose up -d --buildOpen:
http://SERVER_IP:8080
Check the deployment:
docker compose ps
curl http://127.0.0.1:8080/healthThe GHCR Compose file uses:
ghcr.io/whojan/bert
Run the latest image:
git clone https://github.com/emnl51/bert.git
cd bert
cp .env.example .env
docker compose -f docker-compose.ghcr.yml pull
docker compose -f docker-compose.ghcr.yml up -dTo pin a release, set BERT_IMAGE_TAG before running Compose:
BERT_IMAGE_TAG=<release-version> docker compose -f docker-compose.ghcr.yml up -dBoth deployment methods use the persistent bert_data volume.
After signing in as the administrator:
- Create or review Search Profiles.
- Configure job sources under Sources → Source Catalog.
- Create Search Jobs and assign profiles, schedules, and optional Candidate Profiles.
- Configure personal Telegram or email notifications in the user workspace.
- Configure account-activation email under Administration → System Email.
The Search Profile editor keeps the everyday choices in Profile essentials: target role level, role families, working arrangements, German and English ability, preferred weekly hours, availability, and provider-query languages. Saving automatically adds generated bilingual suggestions while preserving existing custom terms. Raw thresholds, exact query lists, allowlists, blocklists, and scoring JSON remain available under the collapsed Advanced matching settings section.
Role level is an eligibility boundary rather than a score boost. A technician profile rejects engineering, management, and student-only titles; an engineering profile rejects technician-only and student titles; a working- student profile requires an explicit student-role signal. Weekly hours and availability act as labeled preferences for Search Jobs in preference mode and as exclusions in strict working-time mode.
Part-time and working-student profiles always require a confirmed work type after Bert inspects the job title, available description, hours/workload text, and provider metadata. A vacancy that still shows Work time unknown is excluded from review and notifications even when its Search Job uses preference mode.
Profiles referenced by Search Jobs cannot be deleted. Bert reports the linked Search Job names so they can be reassigned or removed first; profile-specific scores are deleted only after those references are resolved.
System email and job-notification email are separate. System email settings can be managed in the admin UI; the SYSTEM_SMTP_* values in .env remain available as fallback defaults.
Bert extracts job requirements and links each result to CV evidence using match, partial, or missing states.
The default hybrid score combines:
- 70% deterministic evidence
- 30% optional Ollama context
Ollama is disabled by default. When it is enabled, AI can add evidence-linked context but cannot change deterministic requirement states or invent CV evidence. If Ollama fails or times out, Bert falls back to deterministic scoring.
Configure it under Intelligence → Hybrid CV Match Engine. The default host endpoint is:
http://host.docker.internal:11434
The model must already exist in the configured Ollama installation.
Applications can be managed as a Kanban board or list. Each application stores its stage, application date, next action, due date, contact, notes, and an owner-scoped activity timeline. The workspace also shows overdue actions, stage totals, and source progression.
Use Add job to capture a public vacancy that was found outside Bert's providers. Bert scores the pasted vacancy against the selected Search Profile and adds it to To Apply. Review the extracted fields before saving and do not paste private correspondence or credentials into the public vacancy description.
The Export for career-ops action downloads a Markdown handoff containing the vacancy, Bert's match evidence, and the current application state. Nothing is sent automatically. Review the file before sharing it with an AI CLI or provider; CV tailoring, cover letters, and interview preparation remain explicit career-ops tasks.
The workspace uses a responsive, minimalist layout with keyboard-visible focus states, touch-friendly controls, and high-contrast text and surfaces in both color modes. Open Settings → Interface to choose English or Turkish and select System, Light, or Dark. Preferences are stored only in the browser; System follows the operating-system color scheme.
The Fredy-inspired workspace uses five stable top-level destinations: Control Panel, Jobs, Job Review, Settings, and Administration. The neutral 220 px sidebar collapses to a centered 60 px icon rail. Jobs and Job Review keep compact workflow submenus, while Settings and Administration expose their pages as horizontal content tabs instead of repeating long lists in the sidebar. Navigation search, keyboard focus, mobile behavior, Light/Dark themes, restrained surfaces, and compact 8 px cards remain available. The shell mounts navigation only after both the sidebar and main workspace are available.
Job Review cards show only the role, company, location, Overall Fit, work arrangement, freshness, and review actions. Select a card—or focus it and press Enter—to open the complete detail view. The original job site opens only from that view, so scanning the queue does not unexpectedly leave Bert.
Preserve the existing .env, Docker volume, and especially APP_SECRET_KEY.
Source deployment:
cd ~/bert
git pull --ff-only origin main
docker compose up -d --buildGHCR deployment:
cd ~/bert
docker compose -f docker-compose.ghcr.yml pull
docker compose -f docker-compose.ghcr.yml up -dDo not run docker compose down -v unless you intentionally want to delete persistent data.
The optional web updater performs guarded fast-forward updates through a restricted host service. Setup instructions are in deploy/README.md.
- Keep
APP_SECRET_KEYstable; changing it can make encrypted credentials and CV data unreadable. - SMTP passwords and Candidate CV text are encrypted at rest.
- System SMTP passwords are never returned through the API.
- Registration links are single-use and time-limited.
- User-owned records are isolated by account.
- Destructive database operations create a SQLite snapshot under
/data/backups. - Do not expose Bert publicly without HTTPS and appropriate network controls.
- Never mount the Docker socket directly into the web container.
- Keep
.env, database files, and backups out of Git.
Bert uses Python 3.12. The stable ASGI entry point is app.application:app.
Every pull request must review and update this README so user-visible behavior, setup steps, and operational
guidance stay aligned with the code. Release-impacting changes must also be recorded under Unreleased in
CHANGELOG.md.
When a GitHub Release is published, the Sync release changelog workflow converts its release notes into an
exact-tag section in CHANGELOG.md and opens an automated pull request. The operation is idempotent, so rerunning
the workflow will not create duplicate release entries.
Run the test suite:
python -m pytest --cov --cov-report=term-missing -qAdditional CI checks include Python compilation, Ruff lint/format validation, JavaScript syntax checks, Docker image builds, and CodeQL analysis.
See CHANGELOG.md for release history and LICENSE for the Apache 2.0 license.