Skip to main content

Configuration

Host settings live in .env.shared (rendered once from .env.exist.shared, gitignored). ./existential.sh merges it into the root .env. The file has one comment line per key; this page has the detail.

What happens when you git pull​

Rendered files are never overwritten. Instead, each run appends any key your .env file is missing, with its comment, and lists it:

updated: .env.shared — 2 new key(s) from the template
+ EXIST_NTFY_USER
+ EXIST_PUBLIC_DOMAIN (needs a value)

git pull && ./existential.sh is the upgrade. It never:

  • changes a value you set
  • fills a blank (blank means something: "not asked yet", or "use the global URL")
  • removes a key (./existential.sh validate drift lists extras)
  • asks a question: a key that needs input arrives blank, marked (needs a value)

New services arrive with their flag false.

Images work differently: when a template names a different image than your rendered compose file, the run lists it and asks before updating the file. Each run then pulls and builds every enabled image without touching running containers; docker compose up -d switches to them.

Service enablement​

EXIST_IS_<CATEGORY>_<SLUG>=true|false, one per service folder (Services). Set by hand or by ./existential.sh flow on <name>.

Models​

Every model is named once, in the Model Selection block; everything else reads it. The block is filled from a tier table when the first GPU service is enabled (GPU vendor, then VRAM). To change it:

./existential.sh run models

After editing anything in the block by hand:

./existential.sh run ollama pull-models
docker compose up -d # recreates what reads the models

The VRAM budget​

On one card, at all times:

chat model + KV cache < your VRAM

Download sizes:

TagSizeCapabilitiesTier
qwen3-vl:2b1.9 GBtools, visionCPU-only
qwen3-vl:4b3.3 GBtools, vision6 GB
gemma4:e2b-it-qat4.3 GBtools, vision8 GB (default)
gemma4:e4b-it-qat6.1 GBtools, vision12 GB
gemma4:12b-it-qat7.2 GBtools, vision16 GB
gemma4:26b-a4b-it-qat16 GBtools, vision24 GB
gemma4:31b-it-bf1663 GBtools, vision96 GB (bf16)
  • Up to 24 GB is quantised; 96 GB runs full precision.
  • Every tier runs 64k context (an agent's tool loop needs it). Below about 12 GB the KV cache spills to RAM and runs slower.
  • The table is src/utils/model-tiers.sh. Edit it there; a unit test checks it matches.

Traps​

  • gemma4 "E" tags count effective parameters, not size: gemma4:e4b downloads at 9.6 GB. Use the -qat tags above.
  • Mixture-of-experts models keep every expert resident: a 30B model with 3B active is still 25 GB.

Rules for any model you pick​

  • It must have ollama's tools capability, or pi can't act.
  • Prefer multimodal, so images don't evict it for a separate vision model.
  • EXIST_MODEL_CHAT_NUM_CTX is a floor of 65536. Below it ollama silently truncates an agent's prompt. Raising it costs VRAM.
  • EXIST_MODEL_VISION ships as the chat tag, so one model stays loaded. Blank EXIST_MODEL_VISION disables image handling.
  • Speech runs on CPU (wyoming-whisper, wyoming-piper):
    • EXIST_MODEL_STT: tiny | base | small | medium | large-v3
    • EXIST_MODEL_TTS_VOICE: a piper voice, <lang>_<REGION>-<name>-<quality> (samples)
    • EXIST_MODEL_STT_LANGUAGE: the language you speak (en by default); short commands need it
    • WhisperX is separate: GPU, long recordings, speaker labels.

Endpoints — putting roles on different hosts​

EXIST_MODEL_URL (default http://ollama:11434/v1) is the OpenAI-compatible /v1 URL every role uses unless it has its own: local ollama, ollama elsewhere, llama.cpp (http://192.168.1.20:8080/v1), anything that answers /v1/chat/completions.

  • Every caller speaks only /v1, so the backend doesn't matter.
  • Ollama-only steps (pull, run ollama unload, /api/show) run only with EXIST_IS_AI_OLLAMA=true.
  • The vision server must accept images (llama.cpp: --mmproj).
  • Models elsewhere: set EXIST_IS_AI_OLLAMA=false.

The Model Endpoints block sends each role somewhere different. Every key ships blank, meaning "use EXIST_MODEL_URL".

KeyRoleWho reads it
EXIST_MODEL_URL_CHATChat / reasoningpi, open-webui, Home Assistant, decree scripts, the newsletter
EXIST_MODEL_URL_VISIONVision / OCRimage-ocr

A worked split, with a 24 GB card and an 8 GB one:

EXIST_MODEL_URL=http://bigbox:8080/v1 # chat lands here
EXIST_MODEL_URL_VISION=http://littlebox:11434/v1

Chat keeps the big card; OCR stops evicting it. Then ./existential.sh and docker compose up -d.

  • ./existential.sh run models prints where each role resolves. Migrations pull each model to the host that serves it.
  • The old EXIST_OLLAMA_URL* keys are carried over on upgrade, /v1 added.
  • A Separate AI Box: llama.cpp presets, ComfyUI, on-demand TTS.

ComfyUI moves the same way, with EXIST_COMFYUI_URL (default http://comfyui:8188):

EXIST_COMFYUI_URL=http://bigbox:8188
EXIST_IS_AI_COMFYUI=false # nothing local to start

One thing doesn't follow the block: Open WebUI's compose file is rendered once. Edit ai/open-webui/docker-compose.yml too, or ./existential.sh reset.

Speech has no endpoint keys​

They run on CPU, and Home Assistant addresses them by host and port in its own settings. To move one: publish its port on the other host (the commented ports: block), disable it here, and point Home Assistant at it.

GPU vendor​

EXIST_GPU_VENDOR: nvidia, amd, none or external. Asked once, when the first GPU service is enabled (never for Core). Ships blank, meaning not yet asked. Change it with ./existential.sh run models.

What it changes, in src/generate-compose.ts:

ValueEffect
nvidiaNothing. The templates already declare the nvidia device reservation.
amdThe nvidia reservation is stripped, and each service's x-exist-gpu.amd block is merged in instead (Vulkan/ROCm).
noneThe nvidia reservation is stripped, and x-exist-gpu.none merged.
externalSame treatment as none — this host has no card to reserve. Ollama is not run locally at all; the models come from EXIST_MODEL_URL on another host.
  • One nvidia reservation on a host without the runtime fails docker compose up for the whole stack, which is why it's stripped.
  • Blank falls back to EXIST_VRAM_GB (0 → none, a number → nvidia); both blank → none.
  • external turns off local Ollama and asks for a model URL. That's how a GPU-less machine runs the agent path.
  • e2e runs a tiny model set on CPU (src/test/fixtures/env.shared), so it needs no card.

EXIST_VRAM_GB is the tier picked; it also ships blank.

  • Custom (last entry) takes a model name, sets the EXIST_MODEL_* keys, and checks nothing. The model needs tool calling, vision and 64k context.
  • 0 is CPU-only, set by answering No GPU. Expect seconds per token.

Identity and permissions​

  • EXIST_USERNAME / EXIST_PASSWORD: your login for ntfy and every admin account a service creates. Blank until a service that needs them is first turned on (Core does), then asked once. Enter gives admin and a generated 24-character password, which is never printed. A typed password must be 8+ characters of A-Za-z0-9._- (it's written unquoted into many files).
  • EXIST_EMAIL: blank until an add-on that needs one (Open WebUI, NocoDB, Appsmith) is first enabled, then asked once.
  • EXIST_PUID / EXIST_PGID: the user and group every container runs as, so files in volumes stay yours. Detected from id -u / id -g on the first run.

Hostnames, DNS and TLS​

  • EXIST_TAILNET_NAME: Core's address (box.tail1234.ts.net), set once tailscale serve works. ntfy and the digest's links use it. See Core over Tailscale.
  • EXIST_CONTROLLER: with the worker flow on, the controller this host reports to, its tailnet name or 100.x address. Blank, this host is the controller. See Controller and Workers.
  • EXIST_REPORTING_URL: on the controller, the Loki every host reports to (http://<host>:3100). Blank means this host's own Loki, when it is on.

The rest are for Caddy (Hostnames that just work):

  • EXIST_LOCAL_HOST_IP: detected (LAN address, or the tailnet address with Tailscale up). Keep it above EXIST_DOMAIN in the file; placeholders resolve top-down.
  • EXIST_DOMAIN: defaults to <ip-with-dashes>.nip.io. Never a MagicDNS name: there's no wildcard under one.
  • EXIST_PUBLIC_DOMAIN: adds Let's Encrypt certs. Needs a domain you own, <domain> and *.<domain> pointing at your public IP, and ports 80 and 443 forwarded. Then ./existential.sh run caddy public-domain.

docker compose up creates the exist network.

Storage​

  • Data is host bind mounts under <repo>/volumes/<name>, never Docker volumes.
  • EXIST_NFS_ADDRESS: the NAS's NFS address, asked the first run a service with an nfs: true volume is on. Blank is not yet asked; none keeps every volume local.
  • EXIST_NFS_PATH_<VOLUME>: one per nfs: true volume of an enabled service (e.g. EXIST_NFS_PATH_NEXTCLOUD_DATA), appended when asked: that volume's folder on the NAS, or none (Enter) to keep it on this host. Compose mounts it over NFS.
  • EXIST_NFS_SHARE_<NAME>: the same for a volume that holds your own files rather than a container's data: EXIST_NFS_SHARE_NEXTCLOUD (shown in Nextcloud at /nas) and EXIST_NFS_SHARE_WORKSPACE (where workspace/ syncs to). See Storage.
  • EXIST_BACKUP_RCLONE_REMOTE: an rclone remote (<remote>:<path>), set by ./existential.sh run rclone. Blank disables scheduled DB backups.

Shared service credentials​

Generated on render, shared between services:

  • EXIST_NTFY_URL: ntfy, by container name. EXIST_NTFY_USER / EXIST_NTFY_PASSWORD are the publisher, created by ntfy on first boot.
  • EXIST_DECREE_FILES_WEBHOOK_AUTH_TOKEN: what Nextcloud's file-event webhooks send to /files. See File Processor.
  • EXIST_NEXTCLOUD_DOMAIN, _USER, _APP_PASSWORD, _WORKSPACE_DIR: the Nextcloud workspace/ syncs with. See Syncing with a Nextcloud.