Panella docs · Self-hosting

Self Hosting Panella

Start a box from a checkout

The released path is the one-command panella up (see "Zero-clone bootstrap" below). From a checkout, the equivalent is compose plus the one-shot init — without panella init the stack serves read-only and unprovisioned (no owner/approval credentials, no governance overlay):

echo "PANELLA_API_KEY=$(openssl rand -hex 32)" > .env
mkdir -m 0700 .panella      # create it yourself first — see the native-Linux note below for why
docker compose up --wait
panella init --yes          # mints both credentials, writes the overlay, restarts write-capable

The compose stack builds two targets from the same Dockerfile: store runs the pinned local SQLite Panella store, and app runs the governed facade. The facade mounts the store volume read-only for startup coherence checks; all writes go through the HTTP store adapter.

Embedding model: baked, offline by default

The store image bakes sentence-transformers/all-MiniLM-L6-v2 at revision 1110a243fdf4706b3f48f1d95db1a4f5529b4d41. Its ten required model files are SHA-256-manifested at build time, and released images are cosign-signed. Runtime starts with HF_HUB_OFFLINE=1, TRANSFORMERS_OFFLINE=1, and HF_HUB_DISABLE_TELEMETRY=1: ordinary first boot therefore makes no model or telemetry network request.

Before serving, the store entrypoint verifies that the build-time fail-loud guard is present and loads/encodes with the selected backend. A missing baked model or unusable local backend stops the container before it serves. The guard independently rejects upstream's pure-Python hash-embedding fallback; it remains active even if preflight is skipped.

BAKE_EMBEDDING_MODEL=0 is an expert bring-your-own-model build contract, not an image-size toggle. Choose one of these explicit paths: build the default model into the image (the normal setting); make a custom model available in the runtime cache; or deliberately allow first-start download for a custom model with all three settings below:

HF_HOME: /home/panella/.cache/huggingface
HF_HUB_OFFLINE: "0"
TRANSFORMERS_OFFLINE: "0"

The same three-setting recipe is required when MCP_EMBEDDING_MODEL selects a custom model not already cached. Preflight is the first downloader in that mode and persists it below HF_HOME.

MCP_MEMORY_USE_ONNX=1 is an opt-in path. It requires onnxruntime, tokenizers, and a usable $HOME/.cache/mcp_memory/onnx_models/all-MiniLM-L6-v2/onnx/model.onnx cache; mount/populate that cache or leave the variable unset. Upstream can still transiently fall back from a failed ONNX initialization to SentenceTransformer, so treat ONNX as an explicit operational choice rather than the default model path.

MCP_EXTERNAL_EMBEDDING_URL delegates to upstream's external fail-loud path only for the sqlite_vec backend. hybrid still needs its local SQLite primary and local model; cloudflare uses Workers AI and has no local-model preflight requirement.

Two independent emergency knobs exist. PANELLA_SKIP_EMBEDDING_PREFLIGHT=1 skips only the entrypoint's model checks and prints a warning on every boot; failures then surface at serving start. PANELLA_REQUIRE_REAL_EMBEDDINGS=0 permits the guarded hash fallback and also prints a warning. Silent degraded hash embeddings require both settings to be deliberately enabled.

Zero-clone bootstrap

For a released wheel, bootstrap one self-hosted box without cloning the repository:

uvx panella up --yes

panella up materializes the wheel-embedded digest-pinned compose file and .env in ~/panella-box (or PANELLA_HOME / --home), starts the box, activates it with panella init, and prints a Claude Code connection block. The bare form above is the shortest path; for unattended or agent-driven runs, use the hardened invocation from the llms-install.md runner contract — pin the release and pass an explicit home (uvx panella@<ver> up --yes --home "$PWD", or uv tool install panella==<ver> then panella up --yes --home "$PWD"). The generated .env pins PANELLA_UID/PANELLA_GID to the invoking user so the containers can read the bind-mounted operator files on native Linux. One canonical home maps to one Compose project; use a different --home for a separate box. It is intentionally not a development command: when run from a clone, use panella init instead.

The embedded compose asset is release-specific. Hand edits, drift, or an asset from a different release are refused rather than upgraded in place; follow UPGRADE.md for upgrades. An air-gapped machine still needs the wheel and the digest-pinned images available locally: up does not fetch a compose file, but Docker may need to pull images unless they have been preloaded.

up/agent workflows never need, and are never handed, the approval credential — bearer and approval-token are separate credentials, and the agent-facing write path is propose-only by design (the MCP approval endpoints that do exist require that separate approval credential). Mode 0600 blocks subjects running as other UIDs. Since the arbitrary-uid work, the containers run as the operator's own UID, so neither the container nor a full-shell agent under that UID is a separate subject; the hard subject boundary is the agent's sandbox/permission model, or moving approval to another OS user/device (the operator console / C0-B .mcpb approval endpoint).

The per-home lock uses POSIX flock; it coordinates concurrent up calls on the same host only. up and a separately started init are not a transaction and should not be run concurrently. If .panella is lost while project containers or volumes remain, up stops and prints a recovery or explicit destructive-reset command; it never deletes those resources itself.

Environment

Variable Default Purpose
PANELLA_API_KEY required in compose Shared internal store/facade secret.
PANELLA_CONFIG_DIR /app/dist-config Rendered agent profile and wing config directory.
PANELLA_HTTP_PROFILE serving HTTP facade profile.
PANELLA_HTTP_HOST 0.0.0.0 in container Bind host inside the container.
PANELLA_HTTP_PORT 8001 Facade port.
PANELLA_BASE_URL http://panella:8000 in compose Store base URL used by the facade adapter.
PANELLA_STORE_PATH /data/sqlite_vec.db in compose Read-only store path for coherence checks.
PANELLA_GOVERNANCE_OVERLAY unset Optional local overlay merged over the generic base governance.
PANELLA_MCP_ENABLED 1 in image Enables the /mcp network surface.
PANELLA_MCP_PROFILE mcp-read Use mcp-write only after provisioning a local approval token and approver overlay.
PANELLA_MCP_ALLOWED_HOSTS loopback hosts Host allowlist for the MCP mount.
PANELLA_UID 10001 Container uid; set to id -u on native Linux.
PANELLA_GID 0 Container primary gid; set to id -g on native Linux.

Data

Compose creates three named volumes: panella-store, panella-model-cache, and panella-http-data. It also mounts local .panella/ read-only to /app/local — that host directory carries the operator-owned governance overlay and the operator-only approval token (.panella/approval-token, mode 0600, minted by panella init); the facade's own state (bearer token DB, audit files) lives in panella-http-data. The store runs with local SQLite embeddings and no provider API key. Its named model-cache volume is for the optional ONNX cache and other runtime caches; the default SentenceTransformer model is baked at /opt/hf-cache and is not hidden by that volume.

Approval Setup

The shipped default governance is inert-closed for approvals. panella init does the whole setup in one command (mints the owner bearer, writes the local_cli token file at mode 0600, writes the approver overlay, enabling PANELLA_MCP_PROFILE=mcp-write); panella init --verify then confirms it end to end. The owner bearer belongs in the MCP client config; the approval token is operator-only.

See QUICKSTART.md for the end-to-end submit, approve, and recall flow.

Running as your own uid (native Linux)

The approval token is a host file (mode 0600) that panella-http reads through the /app/local bind mount. On native Linux, make both services run as your host uid from their first instruction, and create .panella yourself first — the compose file bind-mounts ./.panella:/app/local:ro, and if the directory is missing at first boot the Docker daemon creates it root-owned, which then blocks panella init (running as your uid) from writing the approval token and overlay:

printf 'PANELLA_UID=%s\nPANELLA_GID=%s\n' "$(id -u)" "$(id -g)" >> .env
mkdir -m 0700 .panella          # create it as your uid BEFORE the first `docker compose up`

Compose adds supplementary group 0 to both services. The image makes each mutable image path group-0 writable, so a fresh install needs no migration: the caller uid can create state in those directories, and the files it creates are caller-owned. Facade token and audit files stay mode 0600, which their owner can read and enforce on every connection. Docker Desktop on macOS needs none of these lines; leaving both unset keeps the default 10001:0 container identity.

Upgrading pre-C0-U named volumes

Fresh installs need no migration. Existing named volumes retain their old 10001:10001 ownership (including any 0600 facade DB files), because the image's build-time group change does not alter a non-empty volume. For a native-Linux upgrade, stop the stack and run these one-time commands to make every existing volume entry owned by the caller identity that the services will use:

docker compose down
# TARGET must be the SAME identity Compose runs the services as. Ask Compose for its EFFECTIVE
# resolved `user:` — this honors the real interpolation precedence (a shell-exported PANELLA_UID
# outranks the .env file), so the chown can never disagree with the uid the services actually start
# as. Both services resolve to the same value.
TARGET="$(docker compose config --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["services"]["panella-http"]["user"])')"
docker compose run --rm --no-deps --user 0:0 -e TARGET="$TARGET" --entrypoint sh panella-http -c \
  'find -P /app/data \( -type d -o -type f \) -exec chown "$TARGET" {} +'
docker compose run --rm --no-deps --user 0:0 -e TARGET="$TARGET" --entrypoint sh panella -c \
  'find -P /data /home/panella/.cache \( -type d -o -type f \) -exec chown "$TARGET" {} +'
docker compose up -d --wait

The migration preserves existing modes: it uses chown, not chgrp plus chmod. This is necessary for existing 0600 token and audit DB files: the caller must own them so its normal connection-time chmod 0600 succeeds, rather than failing with EPERM as a non-owner. The helpers are explicitly root only for this one ownership repair; normal services and their healthchecks remain non-root. The walk is symlink-safe: find -P never traverses a symlinked directory, and \( -type d -o -type f \) restricts chown to real directories and files, so no symlink is ever dereferenced (a stale link could otherwise point chown outside the volume). Re-run panella init --verify after the stack is healthy; every check should PASS, including the two [container] lines.

Canonical source: docs/SELF_HOST.md on GitHub — the repository is the single source of truth; this page is rendered from it.