#!/usr/bin/env bash # The one command to run after any change to this repo (compose file, # litellm-config.yaml, .env, or a git pull) to bring the running stack in # sync. Ensures secrets/keys exist, pulls, validates, rebuilds/re-pulls # images, and recreates only what changed — safe to run any time, including # with nothing to do. # # ponytail: no rollback/backup logic — this is a single-user homelab box, # not a fleet. If a bad config lands, `git revert` + re-run is the recovery # path, not this script. set -euo pipefail cd "$(dirname "$0")/.." [ -f .env ] || cp .env.example .env # Handles all three cases: the KEY=value line is missing entirely (.env # predates that var being added to .env.example — sed can't fix what isn't # there, so this appends it), present but blank, or already set. set_if_blank() { local key="$1" value="$2" if grep -qE "^${key}=.*[^[:space:]]" .env; then echo "${key}: already set, skipping." elif grep -qE "^${key}=" .env; then sed -i "s|^${key}=.*|${key}=${value}|" .env echo "${key}: set." else echo "${key}=${value}" >> .env echo "${key}: added (was missing from .env)." fi } echo "==> filling in missing secrets" # Random values — safe to re-run, never overwrites what's already set. # LITELLM_SALT_KEY especially: never change it after first run, existing # encrypted data becomes unreadable if you do. set_if_blank LITELLM_MASTER_KEY "$(openssl rand -hex 32)" set_if_blank LITELLM_SALT_KEY "$(openssl rand -hex 32)" set_if_blank LITELLM_DB_PASSWORD "$(openssl rand -hex 32)" set_if_blank REDIS_PASSWORD "$(openssl rand -hex 32)" set_if_blank UI_PASSWORD "$(openssl rand -hex 16)" set_if_blank PGVECTOR_DB_PASSWORD "$(openssl rand -hex 32)" set_if_blank LITELLM_PGVECTOR_API_KEY "$(openssl rand -hex 32)" echo "==> resolving SEARXNG_LAN_IP" # search.home is a LAN mDNS/local-DNS name — resolvable from this host, just # not from inside the litellm container (see docs/research/litellm-searxng-search.md). searxng_ip="$(getent hosts search.home 2>/dev/null | awk '{print $1}' | head -1)" if [ -n "$searxng_ip" ]; then set_if_blank SEARXNG_LAN_IP "$searxng_ip" else echo "SEARXNG_LAN_IP: couldn't resolve search.home from this host, set it manually if still blank." fi echo "==> git pull" git pull --ff-only echo "==> validating compose config" docker compose config -q echo "==> pulling images" docker compose pull --ignore-buildable echo "==> rebuilding local-build services" docker compose build --pull echo "==> ensuring models are downloaded (skips already-present files)" docker compose --profile tools run --rm downloader docker compose --profile tools run --rm downloader-embedding echo "==> bringing up litellm (needed to mint virtual keys below)" docker compose up -d --wait litellm-db litellm # OPENWEBUI_LITELLM_KEY / LITELLM_PGVECTOR_EMBEDDING_KEY are per-workload # virtual keys, not random secrets — minted via LiteLLM's own API # (docs/proxy-key-onboarding.md documents the manual Admin UI route; this is # the same thing over the REST endpoint LITELLM_MASTER_KEY already # authenticates against). set -a && . ./.env && set +a mint_key_if_blank() { local key="$1" alias="$2" if grep -qE "^${key}=.*[^[:space:]]" .env; then echo "${key}: already set, skipping." return fi local minted minted=$(curl -sf -X POST "http://localhost:${LITELLM_PORT:-4000}/key/generate" \ -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \ -H "Content-Type: application/json" \ -d "{\"key_alias\": \"${alias}\"}" | jq -r '.key') if [ -n "$minted" ] && [ "$minted" != "null" ]; then # Same missing-line-vs-blank-line handling as set_if_blank above. if grep -qE "^${key}=" .env; then sed -i "s|^${key}=.*|${key}=${minted}|" .env else echo "${key}=${minted}" >> .env fi echo "${key}: minted." else echo "${key}: mint failed, create it by hand per docs/proxy-key-onboarding.md." fi } mint_key_if_blank OPENWEBUI_LITELLM_KEY openwebui mint_key_if_blank LITELLM_PGVECTOR_EMBEDDING_KEY litellm-pgvector set -a && . ./.env && set +a echo "==> recreating changed services" docker compose up -d --remove-orphans # litellm-pgvector's Dockerfile only runs `prisma generate` (codegen) at # build time — nothing ever applied the schema to pgvector-db itself, so the # vector_stores/embeddings tables plain didn't exist until this was added # (see issue #24). --accept-data-loss is the same "no rollback/backup logic, # git revert is the recovery path" tradeoff as the rest of this script — a # schema-incompatible change here would need a manual look regardless. echo "==> syncing litellm-pgvector's database schema" docker compose up -d --wait pgvector-db litellm-pgvector docker compose exec -T litellm-pgvector prisma db push --accept-data-loss # Registers memory-and-notes in litellm's own DB (LiteLLM_ManagedVectorStoresTable), # not just litellm-config.yaml's vector_store_registry block. Both matter for # different reasons: config.yaml seeds it into memory at boot (works even # before this script has ever run against a fresh DB); the DB row is what # /vector_store/list — and so the Admin UI's Vector Stores page — actually # shows, since that endpoint only auto-syncs a config-only entry into the DB # view once a DB row with the same id exists (see issue #24 follow-up). # # Must pass the real key, not the os.environ/... form used in # litellm-config.yaml — this hits the live management API, not the # config.yaml loader, so there's no env-substitution pass over the request # body. Ignores failure if the row already exists (no update-in-place: see # below). # # No update-if-changed path — the DB row is otherwise never touched once # created (/vector_store/update in this litellm version can't set # litellm_params at all — VectorStoreUpdateRequest has no such field, so an # update silently no-ops on it). If LITELLM_PGVECTOR_API_KEY ever rotates, # fix this row by hand: /vector_store/delete then re-run this script. echo "==> registering memory-and-notes vector store with litellm (for the Admin UI)" curl -sf -X POST "http://localhost:${LITELLM_PORT:-4000}/vector_store/new" \ -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg key "${LITELLM_PGVECTOR_API_KEY}" '{ vector_store_id: "memory-and-notes", custom_llm_provider: "pg_vector", vector_store_name: "memory-and-notes", litellm_params: {api_base: "http://litellm-pgvector:8000", api_key: $key} }')" > /dev/null 2>&1 || echo "memory-and-notes: already registered (or registration failed — check by hand if this is a fresh deploy)." echo "==> status" docker compose ps