Configuration reference¶
Repod is configured through two files: .env (Docker Compose variables) and
backend.env (backend application settings). This page documents every available
option.
.env — Docker Compose variables¶
These variables control how the containers are built and exposed. Place a .env
file next to docker-compose.yaml (cp .env.example .env).
| Variable | Default | Description |
|---|---|---|
BIND_HOST |
0.0.0.0 |
IP address Docker binds container ports to. Set to 127.0.0.1 if behind a reverse proxy on the same host. |
APT_PORT |
80 |
Host port for depot-apt — serves .deb (APT) and .apk (Alpine) repositories. |
RPM_REPO_PORT |
8080 |
Host port for depot-rpm — serves .rpm repositories. |
BACKEND_PORT |
8000 |
Host port for the FastAPI backend. |
FRONTEND_PORT |
3003 |
Host port for the React web UI. |
REACT_APP_API_URL |
(empty) | URL the browser uses to call the backend API. Must stay empty so calls remain relative (/api/v1/...) and are proxied by the frontend's Nginx — see Frontend. |
REACT_APP_REPO_URL |
http://localhost |
URL the browser uses to display APT/APK repository URLs to users. |
REACT_APP_RPM_REPO_URL |
http://localhost:8080 |
URL the browser uses to display RPM repository URLs to users. |
POSTGRES_PASSWORD |
repod_dev_password |
Password for the repod PostgreSQL user. Change this in production and keep it in sync with DATABASE_URL in backend.env. |
APT_TLS_PORT |
8085 |
Used by docker-compose.tls.yml — depot-apt falls back to this port when nginx-proxy takes over port 80 for HTTP→HTTPS redirection. |
BIND_HOST in production
If you expose Repod directly (no reverse proxy), keep BIND_HOST=0.0.0.0. If
you run a reverse proxy on the same host, set BIND_HOST=127.0.0.1 to
prevent direct access to backend ports. See Reverse proxy guide.
backend.env — Application settings¶
Database¶
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string: postgresql://repod:<password>@db:5432/repod. db/engine.py raises RuntimeError at startup if unset. The db service (PostgreSQL 16) is defined in docker-compose.yaml; the password must match POSTGRES_PASSWORD in .env. Schema is managed by Alembic migrations in backend/alembic/versions/. |
Repository format¶
| Variable | Default | Description |
|---|---|---|
REPO_FORMAT |
apt |
Read once at startup by format_router.py. Controls which package format(s) this instance manages: |
| Value | .deb (reprepro) |
.rpm (createrepo_c) |
.apk (apk index) |
|---|---|---|---|
apt (default) |
✅ | ||
rpm |
✅ | ||
apk |
✅ | ||
both |
✅ | ✅ | |
all |
✅ | ✅ | ✅ |
Any unrecognized value is rejected (warning logged, falls back to apt).
Authentication¶
| Variable | Required | Description |
|---|---|---|
JWT_SECRET_KEY |
✅ | HS256 signing secret for JWT tokens. Generate with openssl rand -hex 32. The application refuses to start in production if this is the default value. |
JWT_ALGORITHM |
— | JWT algorithm. Default: HS256. Do not change unless you know what you're doing. |
JWT_EXPIRE_MINUTES |
— | JWT token lifetime in minutes. Default: 60. |
SETTINGS_ENCRYPTION_KEY |
recommended | Dedicated key encrypting secrets stored in settings.json (SMTP/LDAP password, OIDC client_secret). Generate with openssl rand -hex 32. Falls back to JWT_SECRET_KEY if unset (a startup warning is logged) — keeping it independent means rotating JWT_SECRET_KEY doesn't break decryption of existing secrets. |
WEBHOOK_SECRET |
✅ | HMAC secret for /webhooks/github and /webhooks/kev. Generate with openssl rand -hex 32. The application refuses to start in production if this is empty or a known default. |
Initial admin account¶
By default no admin account is created — use the first-run setup wizard
(POST /api/v1/setup, exposed in the web UI on first access) to create it
interactively.
| Variable | Required | Description |
|---|---|---|
ADMIN_USERNAME |
— | Username of an admin account to pre-provision at startup (e.g. for automated deployments). Only takes effect together with a valid ADMIN_PASSWORD_HASH. |
ADMIN_PASSWORD_HASH |
— | bcrypt hash of the admin password ($2a$/$2b$/$2y$). Generate: docker run --rm python:3.12-slim python -c "from passlib.hash import bcrypt; print(bcrypt.hash('YourPass1!'))". Escape $ as $$ in .env files. If empty or invalid, no admin is seeded and the setup wizard remains available. |
SETUP_TOKEN |
— | Optional. If set, POST /api/v1/setup requires a matching X-Setup-Token header — closes the bootstrap race window before the first admin is created. |
Environment mode¶
| Variable | Default | Description |
|---|---|---|
ENV |
production |
Set to development to enable Swagger UI (/docs), hot-reload, and relaxed proxy trust. Never set to development in production. |
APP_VERSION |
— | Version string returned by /health. |
Paths (pre-configured via Docker volumes — do not change)¶
| Variable | Value | Description |
|---|---|---|
POOL_DIR |
/repos/pool |
Canonical package store — .deb/.rpm files land here on upload |
MANIFEST_DIR |
/repos/manifests |
Package manifest JSONs + index.json |
STAGING_INCOMING |
/repos/staging/incoming |
Upload landing zone |
STAGING_QUARANTINE |
/repos/staging/quarantine |
Quarantined packages |
AUDIT_DIR |
/repos/audit |
Audit log JSONL files |
INDEX_PATH |
/repos/manifests/index.json |
Aggregated package index file |
INDEX_DIR |
/repos/package-index |
Full-text search working files |
IMPORTS_DIR |
/repos/imports |
Working directory for sync/mirror imports |
SETTINGS_PATH |
/repos/settings.json |
Application settings file |
SECURITY_DIR |
/repos/security |
CVE decisions, CISA KEV and EPSS caches |
GNUPG_HOME |
/repos/gnupg |
GPG keyring shared with depot-apt / depot-rpm |
NGINX_LOGS_DIR |
/repos/logs |
Nginx access logs (for download stats) |
CLAMAV_DB_DIR |
/var/lib/clamav |
ClamAV signature database |
GRYPE_DB_CACHE_DIR |
/repos/grype-db |
Grype vulnerability database cache |
REPREPRO_BASE |
/repos |
-b path for reprepro — contains conf/, dists/, db/, pool/ (APT mode) |
DISTS_DIR |
/repos/dists |
reprepro-managed Release + Packages trees (APT mode) |
CONF_DIR |
/repos/conf |
reprepro distribution configuration (APT mode) |
REPO_BASE |
/repos/rpm |
createrepo_c directories per <distro>/<arch>/ (RPM mode) |
ADD_RPM_SCRIPT |
/scripts/add-rpm.sh |
Script called by backend to index new .rpm packages |
APK_REPO_BASE |
/repos/apk |
Alpine repositories — APKINDEX.tar.gz + .apk files (APK mode) |
SSH_KEY_PATH |
/home/appuser/.ssh/id_ed25519 |
Ed25519 key for SSH-based inventory scanning |
Network & proxy¶
| Variable | Default | Description |
|---|---|---|
TRUSTED_PROXIES |
127.0.0.1,172.16.0.0/12,192.168.0.0/16 |
Comma-separated list of trusted reverse proxy IPs or CIDR ranges. Used by uvicorn --forwarded-allow-ips to trust X-Forwarded-For headers. |
CORS_ORIGINS |
— | Comma-separated allowed origins (e.g. https://repod.example.com). Must include every host/IP+port used to reach the frontend. |
SMTP (email notifications)¶
Configured via the web UI (Settings → Email) and stored encrypted in
settings.json. Not set via environment variables.
LDAP¶
Configured via the web UI (Settings → LDAP) and stored in settings.json. See
Configure LDAP.
settings.json — Runtime configuration¶
The file at /repos/settings.json (mounted volume) stores runtime settings
modified through the web UI. It is read at startup and updated when you save
changes in the UI. Secrets inside it (SMTP/LDAP password, OIDC client_secret) are
encrypted with SETTINGS_ENCRYPTION_KEY.
Info
You can edit settings.json directly as a text file if the web UI is
unavailable, but encrypted fields must be set through the API/UI (plaintext
values written by hand will not decrypt). Restart the backend container after
manual edits: docker compose restart backend.
Full default structure¶
{
"app_url": "http://localhost:3003",
"sync": {
"enabled": true,
"hour": 3,
"minute": 0
},
"sources": {
"ubuntu-jammy": true,
"ubuntu-jammy-updates": true,
"ubuntu-noble": true,
"ubuntu-focal": true,
"debian-bookworm": true,
"ubuntu-jammy-security": true,
"ubuntu-noble-security": true,
"ubuntu-focal-security": true,
"debian-bookworm-security": true
},
"mirror": {
"enabled": false,
"hour": 4,
"minute": 30,
"max_packages_per_run": 200,
"max_runtime_minutes": 60,
"min_free_disk_gb": 5,
"sources": {}
},
"notifications": {
"webhook_url": "",
"webhook_enabled": false,
"webhook_min_packages": 1
},
"email": {
"enabled": false,
"smtp_host": "",
"smtp_port": 587,
"smtp_user": "",
"smtp_password": "",
"from_address": "",
"to_addresses": "",
"use_tls": true
},
"ldap": {
"enabled": false,
"host": "",
"port": 389,
"use_ssl": false,
"use_starttls": false,
"bind_dn": "",
"bind_password": "",
"base_dn": "",
"user_filter": "(sAMAccountName={username})",
"attr_username": "sAMAccountName",
"attr_email": "mail",
"attr_fullname": "displayName",
"attr_groups": "memberOf",
"group_admin": "",
"group_maintainer": "",
"group_uploader": "",
"group_auditor": "",
"group_reader": "",
"default_role": "reader",
"auto_provision": true,
"verify_cert": true,
"ca_bundle_path": ""
},
"retention": {
"audit_days": 90,
"import_cleanup_days": 30
},
"backup": {
"enabled": false,
"hour": 4,
"minute": 30
},
"validation": {
"sha256_check": true,
"clamav_scan": true,
"grype_scan": true,
"grype_fail_on": "critical",
"max_upload_size_mb": 500
},
"cve_policy": {
"critical": "block",
"high": "review",
"medium": "warn",
"low": "allow",
"negligible": "allow",
"sla_critical_days": 0,
"sla_high_days": 30,
"sla_medium_days": 90,
"auto_enrich": true
}
}
Section reference¶
| Section | Key | Default | Description |
|---|---|---|---|
sync |
enabled |
true |
Whether the daily security sync cron runs (Enterprise only) |
sync |
hour |
3 |
Hour of day for the sync cron (Europe/Paris timezone) |
sync |
minute |
0 |
Minute for the sync cron |
sources |
<source-id> |
true |
Toggle each upstream APT source on/off |
mirror |
enabled |
false |
Whether the daily bulk mirror cron runs (Enterprise only) |
mirror |
sources |
{} |
Per-source bulk mirror configuration, opt-in |
notifications |
webhook_url |
"" |
Slack/Teams/Mattermost incoming webhook URL — validated by the SSRF guard before use |
notifications |
webhook_enabled |
false |
Enable webhook notifications |
notifications |
webhook_min_packages |
1 |
Minimum packages changed to trigger a webhook |
email |
enabled |
false |
Enable SMTP email notifications |
email |
smtp_host |
"" |
SMTP server hostname |
email |
smtp_port |
587 |
SMTP port (465 → implicit TLS, 587/25 → STARTTLS) |
email |
use_tls |
true |
Use STARTTLS |
ldap |
enabled |
false |
Enable LDAP/AD authentication |
ldap |
user_filter |
(sAMAccountName={username}) |
LDAP search filter; {username} is replaced at login |
ldap |
default_role |
reader |
Role assigned when no group mapping matches |
ldap |
auto_provision |
true |
Create a local user record on first LDAP login |
ldap |
verify_cert |
true |
Validate the LDAP server TLS certificate |
retention |
audit_days |
90 |
Days to keep audit log files (Enterprise only) |
retention |
import_cleanup_days |
30 |
Days to keep import staging files (Enterprise only) |
backup |
enabled |
false |
Whether the daily backup cron runs (Enterprise only) |
validation |
clamav_scan |
true |
Enable antivirus scan on upload |
validation |
grype_scan |
true |
Enable CVE scan on upload |
validation |
max_upload_size_mb |
500 |
Maximum package file size accepted |
cve_policy |
critical |
block |
Action for Critical CVEs: block, review, warn, allow |
cve_policy |
high |
review |
Action for High CVEs |
cve_policy |
medium |
warn |
Action for Medium CVEs |
cve_policy |
low |
allow |
Action for Low CVEs |
cve_policy |
negligible |
allow |
Action for Negligible CVEs |
cve_policy |
sla_critical_days |
0 |
SLA days for Critical CVE decisions (0 = immediate) |
cve_policy |
sla_high_days |
30 |
SLA days for High CVE decisions |
cve_policy |
sla_medium_days |
90 |
SLA days for Medium CVE decisions |
cve_policy |
auto_enrich |
true |
Fetch EPSS scores and CISA KEV flags at upload time |
Resource limits (Docker Compose)¶
The backend container has default resource limits:
The memory limit is 2.5g because the clamd daemon requires approximately
800 MB to load the ClamAV signature database into memory, on top of the Python
process and Grype's working set. Increase further for large repositories or high
upload throughput. Grype and ClamAV scans are CPU-intensive.
ClamAV daemon configuration¶
ClamAV scanning uses the clamd daemon, which is started by entrypoint.sh at
container boot. Key details:
appuseris added to theclamavgroup so it can write to/var/lib/clamav.- The Unix socket is at
/var/run/clamav/clamd.ctl. - Signatures (~700 MB) are loaded once at container start;
clamscanis used as a fallback only if the daemon is unavailable. - Manual signature updates are available via Settings → ClamAV in the web UI.
GPG configuration¶
GPG key generation uses --pinentry-mode loopback and requires gpg-agent, which
is included in the Docker image.
GNUPG_HOME defaults to /repos/gnupg — a volume shared between the backend and
the repository containers (depot-apt, depot-rpm).
Community Edition — excluded modules¶
The Community Edition does not include the following service modules; the code is
simply absent and REPOD_EDITION has no effect:
| Module | Description |
|---|---|
cve_enrichment |
EPSS/KEV enrichment at upload time |
security_decisions |
CVE review workflow (pending/approve/reject) |
sla_alerts |
SLA deadline tracking and alerts |
sbom |
SBOM export endpoints (returns HTTP 402) |
ldap_auth |
LDAP/AD authentication (returns HTTP 401) |
email_notifications |
SMTP email alerts |
security_sync / mirror |
Scheduled and manual CVE database sync / bulk mirroring |
retention |
Audit log and import cleanup retention policy |
inventory |
SSH-based machine inventory and remote install |
leader_election (HA) |
Multi-replica active-passive scheduling |
Changing REPOD_EDITION in your environment file has no effect — the modules
above are not compiled into the Community image.
Environment: production vs. development¶
| Behaviour | ENV=production |
ENV=development |
|---|---|---|
Swagger UI (/docs, /redoc, /openapi.json) |
Disabled (404) | Enabled |
| JWT secret validation | Refuses to start if missing or default | Warning log only |
| Source code mount | Not mounted (image is immutable) | ./backend:/app mounted via docker-compose.dev.yml |
| Docker socket | Never mounted | Not mounted (GPG uses shared volume in all modes) |
To run in development mode: