// daemon
Environment variables
All configuration is environment-based — no config file for the daemon itself.
| Variable | Default | Description |
|---|---|---|
OXID_DATA_DIR | /data | State directory: audit.sqlite, git-cache/, secret.key. |
OXID_ADDR | 0.0.0.0:8080 | Bind address. |
OXID_GC_INTERVAL_SECS | 30 | Scheduler tick for GC / deploy-queue retries. |
OXID_MASTER_KEY | generated | 64-hex master key for secret encryption; generated + persisted to secret.key (0600) when unset. |
OXID_API_TOKEN | unset | Bearer token required on /api/v1/*. Unset = open API (fine on localhost) — or set OXID_AUTO_TOKEN=1 to auto-generate and persist it (printed once to logs). |
OXID_ALLOW_OPEN_API | 0 | 1 to let a non-loopback daemon start without OXID_API_TOKEN (explicit opt-in to an unauthenticated API). |
OXID_AUTO_TOKEN | 0 | 1 for zero-config starts: any of OXID_API_TOKEN / OXID_WEBHOOK_SECRET not set is generated (64 hex), persisted under {data}/ (0600) and printed once — the shipped docker-compose.yml uses this so docker compose up -d works with no .env. |
OXID_WEBHOOK_SECRET | unset | HMAC-SHA256 secret for GitHub/GitLab/Gitea/Gogs webhooks; webhooks rejected while unset (or auto-generated with OXID_AUTO_TOKEN=1). |
OXID_DOCKER_NETWORK | unset | Shared network with Traefik: containers get subdomains instead of host ports, and scale-to-zero wake-on-request activates. The wizard's infra step bootstraps this with one click (POST /api/v1/infra/bootstrap). |
OXID_DAEMON_URL | http://oxid-daemon:8080 | The daemon's own address inside OXID_DOCKER_NETWORK (Traefik forwardAuth labels). |
OXID_POSTGRES_URL | unset | Admin connection string for a shared Postgres — per-branch logical databases for projects declaring the dependency. |
OXID_REDIS_URL | unset | Base URL of a shared Redis; per-branch logical databases. |
OXID_REDIS_POOL_SIZE | 16 | Leasable Redis logical databases. |
OXID_DEFAULT_MEMORY_LIMIT_MB | 512 | Fallback container memory limit; 0 disables. |
OXID_DEFAULT_CPU_LIMIT_MILLICORES | 1000 | Fallback container CPU limit; 0 disables. |
OXID_RESERVED_MEMORY_MB | 1024 | Host memory reserved before admission control starts queueing deploys; 0 disables queueing. |
OXID_TLS_CERT / OXID_TLS_KEY | unset | PEM paths — when both are set the daemon serves HTTPS directly. |
OXID_ALLOW_RESTORE | 0 | 1 to accept POST /api/v1/backup/restore uploads at all (staged, applied on next restart). |
OXID_BACKUP_INTERVAL_SECS | unset | When set (e.g. 300), periodic VACUUM INTO snapshots to {data}/backups/. |
OXID_BACKUP_KEEP | 7 | Newest snapshots to keep when backups are enabled. |
OXID_RATE_LIMIT_PER_SECOND / _BURST | unset | When both are set, token-bucket rate limit on protected routes (per client IP). |
OXID_BOOTSTRAP_TOKEN_ACCESS | loopback | Who GET /api/v1/setup/token hands the auto-generated master token to, pre-auth: loopback (only the daemon's own host), any, or off. A containerized daemon is always bound to 0.0.0.0 and sees every caller as the bridge gateway, so it cannot tell a private publish from a public one — the operator does. The shipped compose sets any because it publishes on 127.0.0.1; widen that publish and you must remove it. |
OXID_DEPLOY_CONCURRENCY | per fleet | Queued deploys run at once per drain wave. Leave it unset and Oxid derives it from the fleet — four per node that accepts placements, capped at 32 — so registering a node raises throughput without touching configuration. Setting it pins that number instead. Builds spend nearly all their time waiting on Docker, so overlapping them is what stops a burst of pushes finishing one after another. Measured on 15 simultaneous pushes to a single host: 7.1s at 4, 4.2s at 16. |
OXID_DB_MAX_CONNECTIONS | 8 | SQLite pool size. WAL lets readers and the writer run at once, so the read-heavy paths overlap instead of queueing. Writes still serialize — that is SQLite. See the benchmarks. |
OXID_TRAEFIK_HTTP_PORT | 80 | Host port the built-in Traefik publishes on. The proxy always listens on 80 inside its container; this only moves where that surfaces, for a host whose 80 is taken. |
OXID_ALLOW_INSECURE_NODES | 0 | 1 to let a remote node be registered with no TLS material. A Docker socket over plain TCP is root on that machine for anyone who can route to it — this is the explicit opt-in, following OXID_ALLOW_OPEN_API. |
OXID_NODE_STATUS_TIMEOUT_SECS | 5 | How long a status query to a fleet node may take before this daemon gives up on it for the decision in hand. A partitioned machine sends no RST, so without a deadline a deploy aimed at a healthy node waits out the kernel — measured at 121 s. Raise it for a high-latency link: a deploy takes 3.1 s at no added RTT, 9.5 s at 200 ms and 17.7 s at 400 ms. |
OXID_TRAEFIK_POLL_INTERVAL | 5s | How often Traefik re-polls /api/v1/traefik/config for the fleet's routers. Not the wake latency — a sleeping branch keeps its router, so the request that wakes it arrives immediately — only the delay before a newly created branch becomes routable. |
OXID_LANG | system locale | Language for the CLI's own messages (en/es). The daemon answers API errors per-request from Accept-Language instead. |
RUST_LOG | info | Tracing filter, e.g. oxid_daemon=debug,info. |
OXID_LOG_FORMAT | pretty | pretty or json (one object per line — use in production). |
On startup the daemon reconciles its database against Docker's actual state before
serving any request, and drains in-flight requests for up to 10s on
SIGTERM/Ctrl+C instead of dying mid-request.
Data layout
Everything lives under OXID_DATA_DIR — back this up (or use oxid backup) and you have the whole system.
/data ├── audit.sqlite # all state + audit trail (WAL mode) ├── secret.key # AES-GCM master key, mode 0600 ├── api-token # auto-generated when OXID_AUTO_TOKEN=1 (0600) ├── webhook-secret # auto-generated when OXID_AUTO_TOKEN=1 (0600) ├── git-cache/ # cached clones, one dir per project └── backups/ # VACUUM INTO snapshots when OXID_BACKUP_INTERVAL_SECS is set
A restore never touches the live database in place — an upload is staged and applied on the next daemon startup.