Self-Hosting Guide
This guide covers everything you need to configure, customize, and extend your pi-tree deployment. Whether you're running from source or via Docker, the configuration concepts are the same.
Environment Variables
| Variable | Default | Description |
|---|---|---|
PI_PROVIDER | — | LLM provider name (anthropic, openai, google, zhipu, deepseek, etc.) |
PI_API_KEY | — | API key for your provider |
PI_BASE_URL | — | Custom base URL (for proxies or self-hosted models) |
PI_MODEL | — | Model ID for reading sessions (e.g., claude-sonnet-4-20250514) |
PI_LOOKUP_MODEL | — | Model ID for dictionary lookups (can be a cheaper/faster model) |
DATA_PATH | ~/.local/share/pi-tree | Root for all state: sessions, database, library, user skills |
PI_TREE_DB_URL | — | Remote sqld/libsql-server URL for the main database (http:// or libsql://). Unset = local file at <DATA_PATH>/pi-tree.db. See Remote Database |
PI_TREE_DB_AUTH_TOKEN | — | JWT auth token for PI_TREE_DB_URL (if the sqld server has auth enabled) |
PI_TREE_NEWS_DB_URL | — | Remote sqld URL for the news plugin database. Unset = local file at <DATA_PATH>/plugins/news/news.db |
PI_TREE_NEWS_DB_AUTH_TOKEN | — | JWT auth token for PI_TREE_NEWS_DB_URL |
SKILLS_PATH | <DATA_PATH>/skills | Custom skills directory |
EXTENSIONS_PATH | <DATA_PATH>/extensions | Custom extensions directory |
PORT | 3847 | Server port |
RSS_CRAWL_INTERVAL_MIN | 30 | How often to crawl RSS feeds (in minutes) |
JINA_API_KEY | — | Optional Jina Reader API key for article extraction. Without it, the anonymous tier (20 RPM) is used. With a key, you get 100 RPM and token tracking. |
TIP
Env vars are the simplest way to configure a single provider. For multiple providers, use models.json — see Multi-Provider Models.
Multi-Provider Models (models.json)
Pi-tree uses Pi's native models.json for advanced model configuration. This lets you define multiple providers and models — e.g., Ollama for offline reading and DeepSeek for cloud — and switch between them at runtime.
Place the file at ~/.pi/agent/models.json:
{
"providers": {
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"api": "openai-completions",
"apiKey": "ollama",
"models": [
{ "id": "gemma4:12b" },
{ "id": "qwen3.6:8b" }
]
},
"deepseek": {
"apiKey": "$DEEPSEEK_API_KEY",
"models": [
{ "id": "deepseek-v4-flash" }
]
}
}
}Resolution Order
Env vars and models.json merge automatically. If you set PI_PROVIDER + PI_API_KEY in .env and have providers in models.json, all providers are available. The env var provider's API key takes precedence over models.json for that specific provider.
For Docker, mount the file into the container:
volumes:
- ~/.pi/agent/models.json:/root/.pi/agent/models.json:roSee the Models guide for more details on model configuration.
Data Layout
All mutable state lives under DATA_PATH (default: ~/.local/share/pi-tree/):
<DATA_PATH>/ # ~/.local/share/pi-tree by default
├── pi-tree.db # SQLite database (users, sessions, config)
├── mcp.json # MCP server config (optional, see below)
├── sessions/ # Pi SDK session JSONL files
│ └── <sourceId>/<userId>/ # Per-user per-source sessions
├── sources/ # All source content (books, news, etc.)
│ ├── <sourceId>/
│ │ ├── original.epub # Uploaded source file
│ │ ├── markdown/ # Converted markdown
│ │ ├── analysis/ # AI-generated outlines
│ │ └── cover.jpg # Cover image
│ └── news/ # News plugin data
│ ├── analyses/ # AI-generated news analyses (.md)
│ └── summaries/ # AI-generated news summaries (.md)
├── plugins/ # Plugin-specific data
│ └── news/
│ └── news.db # News plugin's own database
├── skills/ # ← Your custom skills go here
│ └── my-skill/
│ └── SKILL.md
├── extensions/ # ← Your custom extensions go here
│ └── my-extension/
│ └── index.ts
└── global-config.json # Runtime config overrides (from Settings UI)Docker Compose
Docker Compose is the recommended way to manage pi-tree in production. The Getting Started page covers the basics of docker run — this section covers advanced Compose setups.
Basic Setup
services:
pi-tree:
image: ghcr.io/shuowu/pi-tree:latest
restart: unless-stopped
ports:
- "3847:3847"
environment:
- PI_PROVIDER=anthropic
- PI_API_KEY=${PI_API_KEY}
- PI_MODEL=claude-sonnet-4-20250514
volumes:
- ~/.local/share/pi-tree:/data # all state: DB, sessions, libraryWith Custom Skills, Extensions, and MCP Tools
volumes:
- pi-tree-data:/data
- ./my-skills:/data/skills:ro # your custom skills
- ./my-extensions:/data/extensions:ro # your custom extensions
- ./mcp.json:/data/mcp.json:ro # MCP server configUsing a Local LLM
If you're running Ollama, LM Studio, or another local model server on the host machine, there are a few networking and compatibility details to get right.
1. Enable Docker-to-host networking
Docker containers can't reach localhost on the host. You need host.docker.internal to resolve to the host machine:
services:
pi-tree:
# ... your other config ...
extra_hosts:
- "host.docker.internal:host-gateway"INFO
extra_hosts is required on Linux. On Docker Desktop (macOS/Windows), host.docker.internal works out of the box — but adding it explicitly doesn't hurt.
2. Bind your model server to all interfaces
Local model servers typically bind to 127.0.0.1 (localhost only) by default. Docker containers connect via the host's bridge IP, so the server must listen on 0.0.0.0:
Ollama — listens on 0.0.0.0 by default. No changes needed.
LM Studio — binds to 127.0.0.1 by default. Change it:
# Via CLI (preferred)
lms server stop
lms server start --bind 0.0.0.0
# Or edit ~/.lmstudio/.internal/http-server-config.json
# Change "networkInterface": "127.0.0.1" → "networkInterface": "0.0.0.0"3. Configure the provider
Simple setup (single local provider via env vars):
environment:
- PI_PROVIDER=ollama
- PI_API_KEY=not-needed
- PI_BASE_URL=http://host.docker.internal:11434/v1
- PI_MODEL=gemma4:12bMulti-provider setup (local + cloud via models.json):
{
"providers": {
"lmstudio": {
"baseUrl": "http://host.docker.internal:1234/v1",
"api": "openai-completions",
"apiKey": "not-needed",
"compat": { "supportsDeveloperRole": false },
"models": [
{ "id": "qwen/qwen3.6-27b" }
]
},
"deepseek": {
"apiKey": "$DEEPSEEK_API_KEY",
"models": [
{ "id": "deepseek-v4-flash" }
]
}
}
}Mount the file into the container:
volumes:
- ~/.pi/agent/models.json:/root/.pi/agent/models.json:roCompatibility flags
Local model servers don't always support all OpenAI API features. The compat field tells pi-tree how to adapt:
| Flag | Default | When to set false |
|---|---|---|
supportsDeveloperRole | true | LM Studio, Ollama, and most local servers only support system and user roles — not the developer role. Set this to false to avoid silent failures. |
If your local model returns empty responses or hangs, missing compat flags are usually the cause.
Complete Docker Compose example
A full working setup with a local LM Studio provider and a cloud fallback:
services:
pi-tree:
image: ghcr.io/shuowu/pi-tree:latest
restart: unless-stopped
ports:
- "3847:3847"
env_file: .env
volumes:
- ${PI_TREE_DATA:-./data}:/data
- ~/.pi/agent/models.json:/root/.pi/agent/models.json:ro
environment:
- DATA_PATH=/data
extra_hosts:
- "host.docker.internal:host-gateway"Volumes
| Mount Point | Purpose | Access |
|---|---|---|
/data | All state — database, sessions, library, processed content | Read-write |
The /data volume contains everything pi-tree needs: the SQLite database, session files, uploaded books, processed content, and user configuration.
Build from Source
If you prefer to build the image locally instead of using the pre-built one:
docker compose up --buildThis builds the image from the Dockerfile in the repository root and starts the container.
Remote Database (sqld)
By default pi-tree stores its databases as SQLite files under DATA_PATH. That's the right choice as long as DATA_PATH is on a local disk. It is not safe when DATA_PATH lives on a network filesystem (SSHFS, NFS, SMB): SQLite's WAL mode coordinates through memory-mapped shared memory, which network filesystems can't provide coherently, causing intermittent SQLITE_CORRUPT: database disk image is malformed errors — even with a single writer.
If you want your data on another machine (e.g. a NAS, so multiple machines can share one library), run sqld / libsql-server on that machine and point pi-tree at it over HTTP instead:
PI_TREE_DB_URL=http://your-nas:8880 # main database
PI_TREE_NEWS_DB_URL=http://your-nas:8881 # news plugin databasePi-tree already uses @libsql/client, so no other configuration changes — schema migrations still run automatically on startup, and unsetting the variables falls back to local files. This is also the correct multi-machine setup: sqld is a single writer process, so concurrent clients are properly serialized instead of fighting over file locks.
A ready-made Compose file for the sqld side (two instances plus a nightly backup job) lives at config/nas/docker-compose.sqld.yml. Copy it and config/nas/sqld-backup.sh to the target machine and run docker-compose up -d.
Seeding from an existing database
To migrate existing SQLite files into sqld:
- Stop pi-tree, then checkpoint and verify each file on a local disk:
PRAGMA wal_checkpoint(TRUNCATE); PRAGMA integrity_check; - Stop the sqld container.
- Replace
<sqld-volume>/iku.db/dbs/default/datawith your file, deletingdata-wal,data-shm, and thewallogdirectory next to it. - Start the container. The log line
recovering from database fileconfirms sqld adopted your data. Verify with a row-count comparison before switching pi-tree over.
Backups and restore
The bundled sqld-backup.sh runs nightly inside an alpine container, using the SQLite backup API (safe against a live writer) plus an integrity check, keeping 14 dated copies per database. Restoring a backup is the same procedure as seeding, using a file from the backup directory.
WARNING
sqld ships without authentication by default — anyone on the network can read and write. On a trusted home LAN that may be acceptable; otherwise configure sqld's JWT auth and set PI_TREE_DB_AUTH_TOKEN / PI_TREE_NEWS_DB_AUTH_TOKEN.
Custom Skills
Skills are markdown instruction files that shape how the AI behaves during reading sessions. Core skills are bundled with their respective plugins: interactive-reading, book-outline, book-analysis (book plugin), news-reading (news plugin), paper-reading (paper plugin), youtube-watching (YouTube plugin), and session-router (server). You can add your own or override the core ones.
Creating a Skill
Create a directory with a SKILL.md file:
<DATA_PATH>/skills/socratic-reading/SKILL.mdThe file uses YAML frontmatter + markdown body:
---
name: socratic-reading
description: Guide reading through Socratic questioning
---
# Socratic Reading Skill
When discussing book content with the reader:
1. Never explain concepts directly — ask questions that lead to understanding
2. Start with the reader's interpretation: "What do you think the author means by...?"
3. Build on their answers with deeper questions
4. Only provide direct explanation if the reader explicitly asks
5. Reference specific passages from the book to ground the discussionSkill Format
name(required): Identifier for the skilldescription(required): One-line summary — shown to the AI as a skill catalogue entry- Body (required): Detailed instructions the AI follows when the skill is activated
Skills follow the Pi Agent Skills standard. The same format works in the Pi terminal.
Overriding Core Skills
To override a core skill, create a skill with the same name in your user skills directory:
<DATA_PATH>/skills/interactive-reading/SKILL.mdUser skills load first. The Pi SDK uses first-wins dedup, so if a user skill has the same name as a core skill, the user version wins.
How Skills Are Discovered
On each new reading session, the server scans:
<DATA_PATH>/skills/(or<SKILLS_PATH>/) — user skills (loaded first, wins on name collision)- Plugin-bundled skills (
packages/plugin-*/skills/) — shipped with each plugin - Server-bundled skills (
packages/server/src/agents/skills/) — core routing
TIP
No restart is needed — new skills are picked up when a reading session starts.
Custom Tools (Pi Extensions)
Pi-tree's built-in capabilities come from plugins. For lightweight custom tools that don't need the full plugin system, you can use Pi SDK's extension mechanism directly.
Extensions are TypeScript modules that register tools and commands with the Pi agent. They're more powerful than skills — they can execute code, call APIs, and provide interactive tools.
Creating an Extension
<DATA_PATH>/extensions/my-tool/index.tsimport type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function myExtension(pi: ExtensionAPI) {
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Does something useful",
parameters: Type.Object({
input: Type.String({ description: "Input text" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
// Your logic here
return {
content: [{ type: "text", text: `Result: ${params.input}` }],
details: { input: params.input },
};
},
});
}Extensions are loaded at runtime via jiti — no build step required. They have access to the full Pi extension API (tools, commands, events).
WARNING
Extensions run with the server's permissions. Only load extensions you trust.
Custom Session Profiles
Session profiles define the "recipe" for an AI session — which skills, extensions, and model to use. Pi-tree ships with built-in profiles for books and news, but you can define your own to add custom session modes for existing source types.
Creating a Profile
Create a YAML file in <DATA_PATH>/profiles/ (one file per profile):
# ~/.local/share/pi-tree/profiles/socratic-discussion.yml
name: socratic-discussion
label: Socratic Discussion
description: Explore ideas through dialectical questioning
source_type: book
skills:
- socratic-readingProfile Format
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier (used as profile key, e.g. socratic-discussion) |
label | Yes | Human-readable name shown in the UI |
description | No | One-line summary of what this profile does |
source_type | No | Source type this profile applies to (e.g. book, news). Shown only for matching sources. |
skills | Yes | List of skill names to load (must exist in skills dirs) |
extensions | No | List of extension names to load (default: []) |
exclude_tools | No | Pi SDK tools to block (default: ["bash", "edit"]) |
model | No | Model override (falls back to server default) |
How Profiles Are Used
User profiles appear as additional session modes in the SessionPicker for matching source types. They are discovered at startup and merged with built-in profiles.
INFO
User profiles override built-in profiles with the same name.
Custom Source Types
Pi-tree ships with built-in support for books, news, and papers — but you can define your own source types for any content. Custom source types combine three pieces you've already seen: content, skills, and profiles.
The Complete Flow
Here's how to add a custom source type, end to end:
1. Prepare Your Content
Place your content as a markdown file at:
<DATA_PATH>/sources/<source-id>/markdown/content.mdYou can convert from any format — PDF, HTML, Notion export, Obsidian vault, etc. — using tools like Pandoc, Calibre, or any other converter. Pi-tree reads markdown.
Alternatively, you can create the source via the API with a contentPath and the server copies the file for you:
curl -X POST http://localhost:3847/api/library/sources/create \
-H "Content-Type: application/json" \
-d '{"id": "my-tutorial", "title": "React Tutorial", "type": "tutorial", "contentPath": "/path/to/tutorial.md"}'2. Create a Skill (Optional)
If you want custom AI behavior for your source type, create a skill:
<DATA_PATH>/skills/tutorial-reading/SKILL.md---
name: tutorial-reading
description: Guided tutorial walkthrough with exercises and checkpoints
---
# Tutorial Reading
## Workflow
1. Read the tutorial content section by section
2. Explain concepts with practical examples
3. Suggest exercises at natural checkpoints
4. Track progress through the tutorial3. Create a Profile
Wire your skill to a source type with a profile:
# <DATA_PATH>/profiles/tutorial-reading.yml
name: tutorial.reading
label: Tutorial Reading
source_type: tutorial
skills: [tutorial-reading]
extensions: [mcp]
exclude_tools: [bash, edit]The source_type: tutorial field makes this profile appear as a session mode only for sources of type tutorial.
4. Register the Source
Create the source via the API:
curl -X POST http://localhost:3847/api/library/sources/create \
-H "Content-Type: application/json" \
-d '{
"id": "react-tutorial",
"title": "React Tutorial",
"type": "tutorial",
"author": "optional"
}'If you already placed the content at <DATA_PATH>/sources/react-tutorial/markdown/content.md (step 1), you're set. The app will look up the tutorial.reading profile, load your custom skill, and start a session with your custom flow.
Example: Codebase Explorer
A complete custom source type for exploring codebases:
<DATA_PATH>/
skills/
codebase-reading/
SKILL.md ← "Walk through this codebase…"
extensions/
codebase/
index.ts ← clone_repo, list_files tools
profiles/
codebase-reading.yml ← wires skill + extension# profiles/codebase-reading.yml
name: codebase.reading
label: Codebase Explorer
source_type: codebase
skills: [codebase-reading]
extensions: [codebase, mcp]See the Plugin Guide for full examples and API reference.
Profile Resolution Order
When starting a session for source type T with mode M:
T.M— exact match (e.g.,tutorial.reading)T— source type fallback (e.g.,tutorial)_default— universal fallback
This means custom source types always work — even without a custom profile, they fall back to _default.
MCP Bridge (External Tools)
Pi-tree can connect to external MCP servers and expose their tools to the AI agent. This lets you add web search, academic databases, translation APIs, or any MCP-compatible tool — without writing code.
Configuration
Create <DATA_PATH>/mcp.json (same format as Claude Desktop / Cursor):
{
"mcpServers": {
"brave-search": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-brave-search"],
"env": { "BRAVE_API_KEY": "your-key-here" }
},
"fetch": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-fetch"]
}
}
}TIP
A ready-made template with popular servers lives at packages/server/config/mcp.example.json. Copy it to your data path and enable what you need:
cp packages/server/config/mcp.example.json ~/.local/share/pi-tree/mcp.jsonEach server entry can have:
command+args: Spawn a stdio-based MCP server (most common)url: Connect to an HTTP/SSE-based MCP server (alternative to command)env: Environment variables passed to the spawned processdisabled: Set totrueto skip this server without removing the config
How It Works
On server startup, the MCP bridge:
- Reads
<DATA_PATH>/mcp.json - Connects to each configured server via stdio or SSE
- Discovers available tools via
tools/list - Registers each tool with the Pi SDK, prefixed as
mcp_<server>_<tool>(e.g.,mcp_brave-search_web_search)
The AI agent can then use these tools during any session. If an MCP server disconnects unexpectedly, the bridge attempts to reconnect automatically with exponential backoff.
If no mcp.json exists or is empty, the MCP bridge silently does nothing — no configuration is needed if you don't want external tools.
For Docker, mount the config file:
volumes:
- ./mcp.json:/data/mcp.json:roNews Feeds
Pi-tree includes an RSS news feed feature. Feeds are crawled on a schedule, and the AI can analyze, summarize, and discuss recent news with you.
Default Feeds
On first startup, pi-tree seeds a small set of default feeds (Hacker News, TechCrunch, Ars Technica, The Verge, MIT Tech Review, Nature, Quanta, Reuters, BBC) from packages/plugin-news/config/default-feeds.yml. These are only seeded if no feeds exist yet — they won't overwrite feeds you've added.
Managing Feeds
Feeds can be managed through the web UI (News section) or the API:
# List feeds
curl http://localhost:3847/api/news/feeds
# Add a feed
curl -X POST http://localhost:3847/api/news/feeds \
-H "Content-Type: application/json" \
-d '{"id": "ars-technica", "name": "Ars Technica", "url": "https://feeds.arstechnica.com/arstechnica/index", "tags": ["tech"]}'
# Remove a feed
curl -X DELETE http://localhost:3847/api/news/feeds/ars-technica
# Trigger a manual crawl
curl -X POST http://localhost:3847/api/news/crawlCrawl Schedule
Feeds are crawled automatically every 30 minutes by default. Set RSS_CRAWL_INTERVAL_MIN to change the interval. On startup, feeds are crawled immediately if they're stale (no crawl in the last interval).
Remote Crawler (NAS Deployment)
If pi-tree itself doesn't run 24/7 (e.g. a laptop), you can offload crawling to a standalone crawler service on an always-on machine like a NAS. Run the ghcr.io/shuowu/pi-tree-rss-crawler image there and point pi-tree at it:
RSS_REMOTE_URL=http://your-nas:3948In remote mode, pi-tree proxies all feed operations to the crawler and skips local crawling entirely. See packages/rss-crawler/README.md for the crawler's full API and Docker Compose setup.
Data Storage
News data lives under <DATA_PATH>/sources/news/:
analyses/— AI-generated news analyses (Markdown files)summaries/— AI-generated news summaries (Markdown files)
Feed metadata and cached articles are stored in the news plugin's own SQLite database at <DATA_PATH>/plugins/news/news.db, separate from the main pi-tree.db.
Runtime Configuration
Settings can also be changed at runtime through the web UI (Settings page), which writes to <DATA_PATH>/global-config.json. This overrides environment variables for:
- Reading model
- Lookup model
- Provider
- API key
- Base URL
INFO
Environment variables are used as initial defaults. The global-config.json file takes precedence once saved through the Settings UI.