Skip to content

Hot reload and dynamic config

BatleHub can reload its configuration at runtime without restarting the process. The following components are hot-swappable:

  • Registry list (add, remove, or update a registry)
  • Per-registry RBAC (anonymous, user, admin, group-based access)
  • Per-registry policy rules (age gate, deny latest)
  • Per-registry versioning, signing, and beta-channel configuration
  • Artifact size limit
  • Host-based routing (hosts, path_routing, [subdomain_routing]) and the trusted_proxies policy that governs it — the two swap together, so a reload that turns host routing on never runs under the old trust policy

The following components require a process restart:

  • Server host / port
  • Database URL or connection pool size
  • Auth providers ([[auth]])
  • Storage backends

9.1 File Watcher

When the config file changes on disk, BatleHub automatically validates the new config (schema check + connectivity probes) and stores a pending reload. The admin then confirms or discards it via the UI or API. Pending reloads expire after 10 minutes.

The file watcher is enabled by default. Disable it with:

sh
BATLEHUB_DISABLE_HOT_RELOAD=1 batlehub --config config.toml

Use this when config.toml is mounted as a read-only Kubernetes ConfigMap.

9.2 API Endpoints

MethodPathDescription
POST/api/v1/admin/config/reloadImmediate reload: validate + apply atomically
GET/api/v1/admin/config/pendingGet pending reload diff (404 if none)
POST/api/v1/admin/config/pending/applyApply the pending reload
DELETE/api/v1/admin/config/pendingDiscard the pending reload
GET/api/v1/admin/config/changesPaginated audit history (?page=0&per_page=50)
GET/api/v1/admin/config/warningsNon-fatal problems with the config currently in force
sh
# CI/CD: apply a new config atomically
curl -s -X POST \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://localhost:8080/api/v1/admin/config/reload

# Two-step flow: let the file watcher load a pending, then apply from CI
curl -s -X POST \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://localhost:8080/api/v1/admin/config/pending/apply

All reloads (applied or rejected) are written to the config_changes table with the diff, trigger source, and operator identity.

Config warnings

Some config states are wrong enough to tell an operator about but not wrong enough to refuse to start — a registry name that cannot become a DNS label, a deprecated key being shadowed, a permissive security default left in place. These are logged at startup and on every reload, and served from GET /api/v1/admin/config/warnings so they can be seen without grepping logs:

json
{
  "warnings": [
    {
      "code": "proxy-trust.unconfigured",
      "path": "server.trusted_proxies",
      "message": "no trusted-proxy list is configured, so Forwarded / X-Forwarded-Host / …"
    }
  ]
}

code is a stable slug, safe to match on; path points at the offending config location verbatim, so it can be searched for in the TOML.

POST /api/v1/admin/config/validate and POST /api/v1/admin/config/from-content return the same shape inline under warnings, describing the candidate config — so an admin sees them before applying a pending reload rather than after. The Config Reload admin page renders both.

CodeMeaning
proxy-trust.unconfiguredNo trusted_proxies list anywhere; forwarded host/scheme are believed from any client
proxy-trust.deprecated-key-onlyProxy trust comes from the deprecated [ip_blocking].trusted_proxies
proxy-trust.invalid-deprecated-entryAn entry of the deprecated [ip_blocking].trusted_proxies is not an IP or CIDR range and was dropped
proxy-trust.shadowed-deprecated-keyBoth keys are set; [server] wins and the deprecated list is ignored entirely
subdomain.invalid-dns-label[subdomain_routing] is on but a registry name cannot be a DNS label, so no wildcard host is derived for it

9.3 Global Admin Banner

Administrators can broadcast a message to all website visitors:

sh
# Set a warning banner
curl -s -X PUT \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message":"Maintenance window in 30 min","level":"warning"}' \
  http://localhost:8080/api/v1/admin/banner

# Clear it
curl -s -X DELETE \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://localhost:8080/api/v1/admin/banner

The frontend polls GET /api/v1/banner (no auth required) every 30 seconds. The banner backend uses the same infrastructure as the metadata cache:

[cache] typeBanner storage
"memory"In-process — not shared across replicas
"redis"Redis — shared across all HA replicas
"postgres"system_kv table — shared across all HA replicas

Released under the Apache 2.0 License. Made with ❤️ and too much ☕.