Ganek reads its settings from environment variables, in the compose stack via the .env file next to docker-compose.yml. .env.example in the repository is the source of truth; this page groups the same settings with their defaults. Commented-out lines in the example are optional.
| Variable |
Default |
What it does |
GANEK_MODE |
single |
single for a self-hosted instance with one company; multi for the hosted product (public signup, quotas, captcha). |
GANEK_SECRET_KEY |
change it |
Signs session tokens and the capability links in candidate emails. openssl rand -hex 32. Rotating it signs everyone out. |
GANEK_ENCRYPTION_KEY |
derived from the secret key |
Encrypts stored 2FA secrets and webhook signing secrets. Set it separately (openssl rand -hex 32) so the two keys rotate independently; rotating this one means recruiters re-enrol their authenticator. |
POSTGRES_PASSWORD |
ganek |
Password of the bundled Postgres. Change it before exposing anything. |
GANEK_DATABASE_URL |
bundled Postgres |
postgresql+asyncpg://user:pass@host:5432/ganek for an external server. Use a database dedicated to Ganek. |
GANEK_REDIS_URL |
redis://localhost:6379/0 (compose: redis://redis:6379/0) |
Redis for rate limits, quotas and the worker queue. |
GANEK_QUESTIONS_DIR |
auto-detected |
Explicit path of the question bank. Unset finds the repo’s questions/ directory or /srv/questions in the image. |
| Variable |
Default |
What it does |
GANEK_S3_ENDPOINT_URL |
http://localhost:9000 (compose: http://minio:9000) |
S3 endpoint the backend talks to. |
GANEK_S3_BUCKET |
ganek-cvs |
Bucket for uploaded CVs. |
GANEK_S3_ACCESS_KEY, GANEK_S3_SECRET_KEY |
minioadmin |
Credentials the backend uses. Change both. |
MINIO_ROOT_USER, MINIO_ROOT_PASSWORD |
minioadmin |
Root credentials of the bundled MinIO. Change both. |
GANEK_S3_PUBLIC_ENDPOINT_URL |
unset |
Public origin candidates upload to (presigned POST). Must be reachable from the browser. It is baked into the frontend’s Content Security Policy at build time, so rebuild the frontend image after changing it. |
GANEK_CV_MAX_SIZE_MB |
10 |
Upload size limit; only PDF files are accepted. |
GANEK_CLAMAV_HOST |
unset |
Host of a ClamAV daemon that scans every new CV in the worker. With compose, start the optional service with docker compose --profile clamav up -d and set this to clamav. An infected CV cannot be downloaded. Unset = CVs are not scanned and the admin says scanning is not configured. The first start downloads the signature database (a few minutes) and clamd needs about 1.5 GB of RAM. |
GANEK_CLAMAV_PORT |
3310 |
clamd TCP port. |
GANEK_CLAMAV_TIMEOUT_SECONDS |
60 |
How long one scan may take before the worker retries it. After 4 failed tries the CV is marked as not scannable and stays downloadable. |
| Variable |
Default |
What it does |
GANEK_PUBLIC_BASE_URL |
http://localhost:3000 |
The candidate-facing origin, for example https://jobs.example.com. Quiz, status and interview links in emails use it; the frontend builds the sitemap and Open Graph URLs from it. Set it as soon as the instance has a hostname. |
GANEK_GOOGLE_CLIENT_ID, GANEK_GOOGLE_CLIENT_SECRET |
unset |
Google sign-in. Create an OAuth web client in Google Cloud Console; only the openid email scope is requested. |
GANEK_GOOGLE_REDIRECT_URL |
${GANEK_PUBLIC_BASE_URL}/api/v1/auth/google/callback |
Override the redirect URI registered with Google. |
GANEK_OIDC_ISSUER, GANEK_OIDC_CLIENT_ID, GANEK_OIDC_CLIENT_SECRET |
unset |
Generic OpenID Connect sign-in (Keycloak, Authentik, Okta, Entra ID and others). The issuer must serve /.well-known/openid-configuration; only the openid email scope is requested. Signs in existing accounts, and a new team member can accept their invite with it when the IdP confirms the invited email as verified. No self-service signup through it. |
GANEK_OIDC_REDIRECT_URL |
${GANEK_PUBLIC_BASE_URL}/api/v1/auth/oidc/callback |
Override the redirect URI registered with the identity provider. |
GANEK_OIDC_DISPLAY_NAME |
SSO |
Button label on the login page: “Continue with ”. |
GANEK_OIDC_EMAIL_DOMAINS |
empty |
Comma-separated domains the identity provider is trusted for. A first OIDC sign-in links to the account with the same email only when the IdP marks the email verified and its domain is listed here; otherwise the person is asked to sign in with their password. |
Emails are skipped with a warning while no provider is configured. The recruiter checklist shows the state.
| Variable |
Default |
What it does |
GANEK_EMAIL_PROVIDER |
smtp |
smtp for any relay, or mailjet for the HTTPS API (EU-hosted; its bounce and complaint events feed the suppression list). |
GANEK_SMTP_HOST, GANEK_SMTP_PORT, GANEK_SMTP_USER, GANEK_SMTP_PASSWORD |
unset, port 587 |
The SMTP relay. |
GANEK_SMTP_STARTTLS |
true |
Disable only for a local plaintext catcher such as Mailhog. |
GANEK_EMAIL_FROM |
no-reply@localhost |
Sender address, for example jobs@example.com. Set up SPF, DKIM and DMARC for its domain (see Self-hosting). |
GANEK_MAILJET_API_KEY, GANEK_MAILJET_SECRET_KEY |
unset |
Mailjet only: the REST API key pair. |
Interview invites travel as a calendar file in the booking, reschedule and cancellation emails. SMTP sends it as a proper calendar part, so Outlook, Gmail and Apple Calendar update the event in place on a reschedule or cancel. Mailjet carries it as a plain attachment: recipients add it to their calendar, and a later change arrives as a new file.
| Variable |
Default |
What it does |
GANEK_EMAIL_WEBHOOK_SECRET |
unset |
Basic-auth password the provider presents on its bounce and complaint webhook (https://ganek:<secret>@host/api/v1/email/webhooks/mailjet). Unset means webhooks are off. |
| Variable |
Default |
What it does |
GANEK_CAPTCHA_ENABLED |
false |
Self-hosted proof-of-work challenge (ALTCHA, no third party) on signup, apply and request-new-link. Only shown in multi mode. |
GANEK_CAPTCHA_SECRET |
unset |
HMAC key for the challenge; any long random string. Required when enabled. |
GANEK_CAPTCHA_MAX_NUMBER |
100000 |
Proof-of-work ceiling the widget brute-forces; lower it for slower phones. |
GANEK_CAPTCHA_TTL_SECONDS |
600 |
How long a solved challenge stays valid. |
| Variable |
Default |
Meaning |
GANEK_SSR_SECRET |
unset |
Optional. Page views are counted when the frontend’s server render says a load is one (no cookie, no script, no identifier; see the GDPR guide). Those headers can be sent by anyone who calls the API directly, so the counters are advisory. Set the same long random string on the backend and the frontend and only loads that carry it count; anything else is served as usual and not counted. |
| Variable |
Default |
What it does |
GANEK_LOG_FORMAT |
text |
json writes one object per line for log shippers. Every line carries the request id, also returned as the X-Request-ID header (an inbound one is honoured). |
GANEK_LOG_LEVEL |
INFO |
Standard Python levels. |
GANEK_SENTRY_DSN |
unset |
Optional error reporting to Sentry or a self-hosted Sentry/GlitchTip. Unset means nothing leaves the box. Events are scrubbed (emails, capability tokens, CV file names, cookies). Setting this makes your Sentry host a processor: list it in your records (see the GDPR guide). |
GANEK_SENTRY_ENVIRONMENT |
unset |
Environment tag; the Sentry SDK assumes production when it is unset. |
GANEK_SENTRY_TRACES_SAMPLE_RATE |
0 |
Performance tracing stays off by default. |
| Variable |
Default |
What it does |
GANEK_COOKIE_SECURE |
false |
Session cookies are sent over HTTPS only. Set it to true in production behind TLS; the default only suits plain-HTTP local development. |
GANEK_TRUST_PROXY_HEADERS |
true |
Read the client IP from X-Forwarded-For. Keep the backend behind a trusted proxy, or clients can spoof their IP and dodge the rate limits. |
Per-IP, per-minute limits are Redis-backed with an in-memory fallback.
| Variable |
Default |
GANEK_RATE_LIMIT_ENABLED |
true |
GANEK_RATE_LIMIT_AUTH_PER_MINUTE |
10 |
GANEK_RATE_LIMIT_APPLY_PER_MINUTE |
5 |
GANEK_RATE_LIMIT_UPLOAD_PER_MINUTE |
10 |
GANEK_RATE_LIMIT_QUIZ_PER_MINUTE |
60 |
GANEK_RATE_LIMIT_PUBLIC_PER_MINUTE |
120 |
Login lockout and quiz timing:
| Variable |
Default |
What it does |
GANEK_LOCKOUT_ENABLED |
true |
After GANEK_LOCKOUT_MAX_ATTEMPTS failed logins or 2FA codes for one account inside the window, further attempts are refused until the window passes. |
GANEK_LOCKOUT_MAX_ATTEMPTS |
10 |
Failed attempts allowed per window. |
GANEK_LOCKOUT_WINDOW_SECONDS |
900 |
Lockout window, 15 minutes. |
GANEK_QUIZ_NETWORK_GRACE_SECONDS |
2 |
Answers that arrive this long after a question’s deadline still count, to absorb network jitter. |
GANEK_QUIZ_START_TTL_HOURS |
24 |
A quiz link must be started within this many hours of being issued. |
Per-tenant abuse guards (0 turns one off). Admins see the counters on the usage endpoint and as a banner from 80% of a limit.
| Variable |
Default |
What it limits |
GANEK_QUOTA_EMAILS_PER_DAY |
500 |
Emails queued per day. |
GANEK_QUOTA_APPLICATIONS_PER_HOUR |
100 |
Application attempts per hour; the public apply endpoint answers 429 past it. |
GANEK_QUOTA_ACTIVE_JOBS |
50 |
Jobs published at once; publishing past it answers 409. |
| Variable |
Default |
What it does |
GANEK_SUPERADMIN_USER_IDS |
empty |
Comma-separated user ids allowed on /api/v1/ops: list tenants, suspend one with a reason, unsuspend it, export one as JSON (users, jobs and one DSAR-shaped bundle per candidate, CV links presigned), delete one (DELETE /api/v1/ops/tenants/{id} with {"confirm_slug": "<slug>"}; the tenant must be suspended first and cannot be the operator’s own workspace; CVs and the logo are removed first, and blind erasure tombstones make scripts/restore.sh delete it again after restoring an older backup). Every call is logged. Empty, or a caller not on the list, gets a 404. A suspended tenant’s careers pages answer 503 without saying why; its users see the reason when they sign in, and its sessions and API keys are refused until it is unsuspended. The tenant’s activity feed records suspensions and exports, without naming the operator. |