# Service environment reference

> Generated from the per-service env sources by `scripts/misc/env-builder.sh`. Do not edit by
> hand. Update the source at `~/.mountOS/env/<service>/.env` (the same source the `.env-sample`
> files are built from) and re-run the pre-commit hook.

Each service is configured from its environment. This lists every variable and what it means,
per service, taken from the env source comments. Values are deployment-specific; where the code
has a default it is noted in the description.

## appserv

| Variable | Notes |
| --- | --- |
| `HTTPS_PORT` | — |
| `TLS_CERT_FILE` | — |
| `TLS_KEY_FILE` | — |
| `REGION_CLUSTER_ID` | Region cluster UUID, for env-shape consistency with regional services. appserv is the hub: it resolves its own reserved hub region + "uno" cluster internally and does NOT consume this value at runtime. It references the reserved hub uno cluster (created with the hub region at first bootstrap). |
| `DB_URL` | — |
| `DB_PROVIDER_VERSION` | Database provider version for capability detection (e.g., SKIP LOCKED support) PostgreSQL: 18, 19 etc. MySQL: 8.0.32, etc. (Vault) |
| `DB_MAX_OPEN_CONNS` | — |
| `DB_CONN_MAX_LIFETIME_IN_MINS` | — |
| `DB_CONN_MAX_IDLE_TIME_IN_MINS` | — |
| `DB_POOL_MIN_CONNS` | Minimum connections to keep warm (avoids cold connection penalty) |
| `DB_POOL_HEALTH_CHECK_SECS` | Health check interval to replenish connections (seconds) |
| `PG_QUERY_STATEMENT_CACHE_SUPPORTED` | PG_QUERY_STATEMENT_CACHE_SUPPORTED applies to PostgreSQL only. It asserts that the connection path keeps named prepared statements usable, which removes per-query re-parsing and lowers CPU on both ends. Set true for a direct connection to PostgreSQL, or behind PgBouncer 1.21 and newer with max_prepared_statements above 0 (enabled by default at 200 since PgBouncer 1.24). Keep false (the default) for anything else. Behind PgBouncer older than 1.21, behind PgBouncer with max_prepared_statements=0, or behind any other transaction-pooling proxy without that support, it must be false. Before enabling behind a pooler, check max_prepared_statements against the workload. Some bulk paths vary their statement text with batch size, so the number of distinct statements can exceed the pooler cache, and thrashing it costs more than the setting saves. https://www.pgbouncer.org/config.html#max_prepared_statements |
| `VAULT_PROVIDER` | Secret store provider (supported: hashicorp, aws, gcp, azure) |
| `VAULT_HASHICORP_ADDRESS` | — |
| `VAULT_HASHICORP_ROLE_ID` | — |
| `VAULT_HASHICORP_SECRET_ID` | — |
| `ED25519_SIGNING_KEY` | — |
| `ED25519_VERIFICATION_KEY` | — |
| `PROVIDER_VERIFICATION_KEY` | Provider public key to verify provider/SDK-issued JWTs (required for appserv) |
| `DASHBOARD_USER_HMAC_KEY` | Dedicated HMAC secret for the X-MountOS-Dashboard-User header (>= 32 bytes, distinct from PROVIDER_VERIFICATION_KEY). Required at appserv startup even though dashboard-user auth is optional, so the dashboard can be enabled later without an appserv restart. Must match the value held by the dashboard backend. |
| `MOUNTOS_LICENSE_PATH` | License (OPTIONAL bootstrap seed; appserv does NOT require a license to start). If set, appserv loads this file into the admin licenses table on boot when not already present (deduped) — even if expired (expired licenses still grant extended read access). Admin DB is the source of truth; licenses are normally managed via the admin client / SDK. |
| `PROMETHEUS_ENDPOINT_SUFFIX` | Custom suffix for Prometheus metrics endpoint (default: auto-generated) Endpoint will be /metrics-{suffix} if provided, otherwise /metrics-442d89bf1692014faca82591a2133c29 |
| `ADVERTISE_ADDR` | Explicit advertise address (must be IPv4, not hostname/DNS) Localhost OK for local dev; must be public IP in production |

## blockserv

| Variable | Notes |
| --- | --- |
| `SERVICE_RPC_ADDR` | Appserv SRPC address for service registration, heartbeat, and discovery (required) e.g. discovery.mountos.io:9443 or 10.0.1.5:9443 |
| `REGION_CLUSTER_ID` | Region cluster UUID this service belongs to (required, strict in pre-prod) Must reference an existing, is_active=TRUE region_clusters row. The cluster carries its owning region, so the region shard is derived from it at startup (no separate REGION_ID). Resolved via appserv; a deactivated cluster raises a critical topology alert and the service refuses to register. e.g. 019577a0-0000-7000-8000-000000000001 or 019577a0000070008000000000000001 |
| `BLOCK_VOLUME_ID` | Block volume stable identity (required for blockserv). This block volume's UUID, the appserv fetch key used to resolve its storage at startup. Each block volume of a block storage (up to 3 active-active across distinct clusters) has its own. e.g. 019577a0-0000-7000-8000-000000000010 |
| `BLOCK_PORT` | Client byte I/O data-plane port for the custom, encrypted block protocol. Peer replication binds BLOCK_PORT+1. Default 9100. |
| `VAULT_PROVIDER` | Secret store provider (supported: hashicorp, aws, gcp, azure) |
| `VAULT_HASHICORP_ADDRESS` | — |
| `VAULT_HASHICORP_ROLE_ID` | AppRole is per-service; blockserv has none provisioned yet, unlike dataserv/gcserv/appserv. |
| `VAULT_HASHICORP_SECRET_ID` | — |
| `STORAGE_MOUNT_ROOT_PATH` | Block-device cache mount root (required): absolute, existing, writable directory; XFS preferred, mounted rw,async,noatime,nodev,noexec,nosuid. |
| `ED25519_SIGNING_KEY` | — |
| `ED25519_VERIFICATION_KEY` | — |
| `DELETE_MODE` | Controls how a deleted block's local file is reclaimed: - normal: standard unlink (default; residual bytes may be forensically recoverable) - secured / secured-immediate: zero-overwrite (with fsync) before unlink, for compliance Invalid or empty values fall back to normal; secure erase is opt-in. |
| `PROMETHEUS_ENDPOINT_SUFFIX` | — |
| `ADVERTISE_ADDR` | Explicit advertise address (must be IPv4, not hostname/DNS) Localhost OK for local dev; must be public IP in production |

## dataserv

| Variable | Notes |
| --- | --- |
| `SERVICE_RPC_ADDR` | Appserv SRPC address for service registration, heartbeat, and discovery (required) e.g. discovery.mountos.io:9443 or 10.0.1.5:9443 |
| `REGION_CLUSTER_ID` | Region cluster UUID this service belongs to (required, strict in pre-prod) Must reference an existing, is_active=TRUE region_clusters row. The cluster carries its owning region, so the region shard is derived from it at startup (no separate REGION_ID). Resolved via appserv; a deactivated cluster raises a critical topology alert and the service refuses to register. e.g. 019577a0-0000-7000-8000-000000000001 or 019577a0000070008000000000000001 |
| `APP_PORT` | TCP port (optional; defaults to 6464) |
| `RAFT_PORT` | Stable Raft transport port (optional; defaults to APP_PORT+1 = 6465). Must stay fixed across restarts. |
| `RPC_PORT` | Stable SRPC port for discovery and peer RPCs (optional; defaults to APP_PORT+2 = 6466). |
| `RAFT_DATA_DIR` | Raft data directory base path. Dataserv appends the generated raft node ID automatically, so each local instance gets its own directory. Default when unset: /var/lib/mountos/raft |
| `TCP_BACKPRESSURE_ENABLED` | Enable/disable backpressure control (default: true) |
| `TCP_MAX_CONNECTIONS` | Maximum concurrent TCP connections. 0 or unset = adaptive: the ceiling is derived from measured DB latency (1m/5m/15m EWMA gradient, scales down from 10000 under degradation, never below 1000). Past 105% of the ceiling new connections are rejected; existing connections are never touched. Set a positive value (range: 1-1000000) to pin a static ceiling instead -- also the escape hatch if adaptive misbehaves. |
| `TCP_RATE_LIMIT_REQUESTS_PER_SECOND` | Per-connection rate limit: requests per second. 0 or unset = adaptive, derived from the same latency gradient (scales down from 1000 under degradation). Positive value (range: 1-100000) pins a static rate. |
| `PORT` | — |
| `DB_URL` | — |
| `DB_PROVIDER_VERSION` | Database provider version for capability detection (e.g., SKIP LOCKED support) PostgreSQL: 18, 19 etc. MySQL: 8.0.32, etc. (Vault) |
| `DB_MAX_OPEN_CONNS` | — |
| `DB_CONN_MAX_LIFETIME_IN_MINS` | — |
| `DB_CONN_MAX_IDLE_TIME_IN_MINS` | — |
| `DB_POOL_MIN_CONNS` | Minimum connections to keep warm (avoids cold connection penalty) |
| `DB_POOL_HEALTH_CHECK_SECS` | Health check interval to replenish connections (seconds) |
| `PG_QUERY_STATEMENT_CACHE_SUPPORTED` | PG_QUERY_STATEMENT_CACHE_SUPPORTED applies to PostgreSQL only. It asserts that the connection path keeps named prepared statements usable, which removes per-query re-parsing and lowers CPU on both ends. Set true for a direct connection to PostgreSQL, or behind PgBouncer 1.21 and newer with max_prepared_statements above 0 (enabled by default at 200 since PgBouncer 1.24). Keep false (the default) for anything else. Behind PgBouncer older than 1.21, behind PgBouncer with max_prepared_statements=0, or behind any other transaction-pooling proxy without that support, it must be false. Before enabling behind a pooler, check max_prepared_statements against the workload. Some bulk paths vary their statement text with batch size, so the number of distinct statements can exceed the pooler cache, and thrashing it costs more than the setting saves. https://www.pgbouncer.org/config.html#max_prepared_statements |
| `VAULT_PROVIDER` | Secret store provider (supported: hashicorp, aws, gcp, azure) |
| `VAULT_HASHICORP_ADDRESS` | — |
| `VAULT_HASHICORP_ROLE_ID` | — |
| `VAULT_HASHICORP_SECRET_ID` | — |
| `CYCLE_DETECTION` | POSIX requires rename(2) to return EINVAL when moving a directory into its own subtree (e.g. mv /a/b /a/b/c). Detecting this requires walking the parent chain from newParent to root, which costs O(depth) DB reads. false (default): only check direct self-reference and immediate parent                  Cost: 1 extra DB read per cross-directory rename. true:            walk the full parent chain up to root (max 1024 levels)                  for strict POSIX compliance.                  Cost: O(depth) sequential DB reads per cross-directory rename.                  Typical trees (depth < 20) add ~2-5ms latency per rename.                  Deep trees (depth > 100) can add 20-50ms+ depending on DB RTT. |
| `PARENT_CMTIME_SKIP_TTL_MS` | Skips redundant parent dir timestamp UPDATEs during burst operations (rm -rf, mass create). TTL in milliseconds (default: 999, max: 5000, <=0 disables) PARENT_CMTIME_SKIP_TTL_MS=999 |
| `ED25519_SIGNING_KEY` | — |
| `ED25519_VERIFICATION_KEY` | — |
| `PROMETHEUS_ENDPOINT_SUFFIX` | Custom suffix for Prometheus metrics endpoint (default: auto-generated) Endpoint will be /metrics-{suffix} if provided, otherwise /metrics-442d89bf1692014faca82591a2133c29 |
| `METAENGINE_ARENA_SIZE` | Total mmap'd memory for the in-memory metadata cache. All shards share this single region as an elastic extent budget (region_size / 192 bytes_per_slot total slots): shards start empty and grow on demand, a single hot (volume, fork) may consume the whole budget, and extents rebalance from cold shards under pressure. Eviction is hotness-driven (coldest directories first). The raft coordinator uses each node's arena capacity and occupancy to balance partition assignments across the cluster, minimizing cache evictions. Format: <number><unit> where unit is MB or GB (case-insensitive) Minimum: 128MB |
| `ADVERTISE_ADDR` | Explicit advertise address (must be IPv4, not hostname/DNS) Localhost OK for local dev; must be public IP in production |

## gcserv

| Variable | Notes |
| --- | --- |
| `SERVICE_RPC_ADDR` | Appserv SRPC address for service registration, heartbeat, and discovery (required) e.g. discovery.mountos.io:9443 or 10.0.1.5:9443 |
| `REGION_CLUSTER_ID` | Region cluster UUID this service belongs to (required, strict in pre-prod) Must reference an existing, is_active=TRUE region_clusters row. The cluster carries its owning region, so the region shard is derived from it at startup (no separate REGION_ID). Resolved via appserv; a deactivated cluster raises a critical topology alert and the service refuses to register. e.g. 019577a0-0000-7000-8000-000000000001 or 019577a0000070008000000000000001 |
| `PORT` | — |
| `VAULT_PROVIDER` | Secret store provider (supported: hashicorp, aws, gcp, azure) |
| `VAULT_HASHICORP_ADDRESS` | — |
| `VAULT_HASHICORP_ROLE_ID` | — |
| `VAULT_HASHICORP_SECRET_ID` | — |
| `DB_URL` | — |
| `DB_PROVIDER_VERSION` | Database provider version for capability detection (e.g., SKIP LOCKED support) PostgreSQL: 18, 19 etc. MySQL: 8.0.32, etc. (Vault) |
| `DB_MAX_OPEN_CONNS` | — |
| `DB_CONN_MAX_LIFETIME_IN_MINS` | — |
| `DB_CONN_MAX_IDLE_TIME_IN_MINS` | — |
| `DB_POOL_MIN_CONNS` | Minimum connections to keep warm (avoids cold connection penalty) |
| `DB_POOL_HEALTH_CHECK_SECS` | Health check interval to replenish connections (seconds) |
| `PG_QUERY_STATEMENT_CACHE_SUPPORTED` | PG_QUERY_STATEMENT_CACHE_SUPPORTED applies to PostgreSQL only. It asserts that the connection path keeps named prepared statements usable, which removes per-query re-parsing and lowers CPU on both ends. Set true for a direct connection to PostgreSQL, or behind PgBouncer 1.21 and newer with max_prepared_statements above 0 (enabled by default at 200 since PgBouncer 1.24). Keep false (the default) for anything else. Behind PgBouncer older than 1.21, behind PgBouncer with max_prepared_statements=0, or behind any other transaction-pooling proxy without that support, it must be false. Before enabling behind a pooler, check max_prepared_statements against the workload. Some bulk paths vary their statement text with batch size, so the number of distinct statements can exceed the pooler cache, and thrashing it costs more than the setting saves. https://www.pgbouncer.org/config.html#max_prepared_statements |
| `ED25519_SIGNING_KEY` | — |
| `ED25519_VERIFICATION_KEY` | — |
| `PROMETHEUS_ENDPOINT_SUFFIX` | Custom suffix for Prometheus metrics endpoint (default: auto-generated) Endpoint will be /metrics-{suffix} if provided, otherwise /metrics-442d89bf1692014faca82591a2133c29 |
| `GC_SCRATCH_DIR` | Scratch directory for temporary, non-durable intermediate data (e.g. iceberg compaction spills downloaded data files here instead of buffering them in RAM). Point it at a fast local (ideally non-durable) volume. Files are always removed after use. Unset/empty or unwritable -> falls back to /tmp. |
| `API_KEY_ROTATION_INTERVAL_DAYS` | API Key Master Key Rotation Interval How many days between API key master key rotations When the master key rotates, all existing API key secrets are re-encrypted with the new master key. User-facing secrets remain unchanged. Default: 7 days, Minimum: 3 days, Maximum: 30 days |
| `ADVERTISE_ADDR` | Explicit advertise address (must be IPv4, not hostname/DNS) Localhost OK for local dev; must be public IP in production |
| `MOUNTOS_CLOUD_SCAN_ENABLED` | Cloud object scan (orphan reclaim by LISTing the object store) — OFF by default. A full store LIST can cost more (request charges + time) than leaving rare orphans; anchored orphans are reclaimed by RefSync without it. Enable only as a deep-scan backstop. Object stores only (block volumes are skipped). |
