Configuration
Create a .env file or use npm run setup to generate one interactively. Runtime settings migrate to the database on first boot; later changes belong in Settings or the API. Bootstrap fields remain environment-backed. See Configuration precedence.
| Service | Port | Env Var |
|---|---|---|
| Backend API | 3005 | API_PORT (or PORT) |
| Web UI | 3007 | WEB_PORT (Vite development server and production server) |
.env.example ships PORT=3005, so that is the port you get from a normal
install; with neither API_PORT nor PORT set, the code falls back to 3000
(src/config/bootstrap-loader.ts). The Vite development server defaults to 3007 and accepts WEB_PORT — under Docker
Compose the host-side ports are OCTIPUS_API_PORT (default 3015) and
OCTIPUS_WEB_PORT (default 3017), mapped onto 3005/3007 in the container.
Environment Variables
Section titled “Environment Variables”# ─── Storage (choose embedded or external) ────────────────────STORAGE_MODE=external # embedded uses PGlite; no PostgreSQL service needed# DATA_DIR=~/.octipus/data # embedded database directoryDATABASE_URL=postgres://user:password@localhost:5432/octipus
# Security keys (minimum 32 characters each, use `npm run setup` to generate)MASTER_KEY=your-master-key-at-least-32-charactersJWT_SECRET=your-jwt-secret-at-least-32-charactersSESSION_SECRET=your-session-secret-at-least-32-chars
# ─── Server ───────────────────────────────────────────────────API_PORT=3005API_HOST=0.0.0.0LOG_LEVEL=infoLOG_STDERR=0 # 1 = write logs to stderr instead of stdoutGATEWAY_STDIO=0 # 1 = also serve the gateway protocol over this process's stdin/stdout as JSON lines (same as `--stdio`); logs move to stderrGATEWAY_MAX_CONNECTIONS_PER_USER=20 # signed-in /gateway connections per user; see "Gateway Config" belowGATEWAY_MAX_FRAME_BYTES=262144 # largest frame a gateway client may send, bytesGATEWAY_REPLAY_MAX_SESSIONS=500 # sessions whose recent events are kept for replayCORS_ORIGINS=http://localhost:3007 # your web origin; code default is http://localhost:3001TRUSTED_PROXIES= # reverse proxies whose X-Forwarded-For is believed; see "Reverse proxy" belowREGISTRATION_MODE=open # who may register: open | invite_only (with a space invite link) | closed; the first account always may (docs/SPACES.md → Guests)SELF_REGISTERED_INSTALL_MODELS=false # may self-registered accounts use the install's models? (docs/SPACES.md → Who may use the install's models)
# ─── Models ───────────────────────────────────────────────────LITELLM_URL=http://localhost:4000 # LiteLLM proxy (optional)OLLAMA_URL= # Ollama URL (optional — set in Settings > Configuration or here)# Default model is configured in the database via the Models page.# Provider API keys (OpenAI, Anthropic, Gemini) are stored in the encrypted vault.
# ─── First-boot model bootstrap (consumed once, then ignored) ─# Written by `npm run setup` / `octi init`. On first boot,# `bootstrapDefaultModel` reads these, seeds a single default# model_config row, stores the API key in the vault, and stops# touching .env. Editing these after first boot has no effect —# use the Models page instead. See CONFIGURATION-PRECEDENCE.md.BOOTSTRAP_PROVIDER= # ollama | litellm | openai | anthropic | gemini | deepseek | mistral | zai | moonshot | openrouter | cliBOOTSTRAP_MODEL= # e.g. llama3.2:3b, gpt-4o-mini, claude-haiku-4-5-20251001BOOTSTRAP_API_KEY= # required for cloud providersBOOTSTRAP_BASE_URL= # for litellm: the proxy URL
# ─── Channels (optional) ─────────────────────────────────────TELEGRAM_BOT_TOKEN= # From @BotFatherSLACK_BOT_TOKEN= # xoxb-...SLACK_APP_TOKEN= # xapp-...SLACK_USER_TOKEN= # xoxp-... optional; only channel_search needs itTEAMS_APP_ID=TEAMS_APP_PASSWORD=WHATSAPP_ACCESS_TOKEN= # Meta Cloud API tokenWHATSAPP_PHONE_NUMBER_ID= # From Meta dashboardWHATSAPP_VERIFY_TOKEN= # Webhook verify tokenWHATSAPP_APP_SECRET= # Meta App Secret
# ─── Agent Limits ────────────────────────────────────────────AGENT_MAX_TOKEN_BUDGET=100000 # Per-agent token limit (0 = unlimited)AGENT_DEFAULT_TIMEOUT=900000 # Per-agent timeout in ms (default: 15min)AGENT_MAX_ITERATIONS=50 # Max iterations per agent loop
# ─── Sessions ────────────────────────────────────────────────SESSION_RETENTION_DAYS=14 # Delete sessions idle this many days (0 = never); "keep"-marked sessions are exempt
# ─── Group channels ──────────────────────────────────────────GROUP_CHANNELS_UNPROMPTED_ENABLED=false # Let channels in listen/proactive mode post unprompted (docs/CHANNELS.md)
# ─── Shared spaces (docs/SPACES.md) ──────────────────────────SPACES_CREATION=any_user # Who may create a space: any_user | adminsSPACES_MAX_MEMBERS=50 # Most members per spaceSPACES_INVITE_MAX_TTL_HOURS=720 # Longest invite-link lifetime (hours); longer requests are clampedSPACES_PURGE_AFTER_ARCHIVE_DAYS=7 # Days a space stays archived before its owner can delete itSPACES_NOTE_MAX_BYTES=114688 # Largest space note (bytes); at most half of GATEWAY_MAX_FRAME_BYTES or startup failsSPACES_DOC_MAX_UPDATES_PER_SECOND=30 # Live-note edits one tab may send per secondSPACES_DOC_PERSIST_DEBOUNCE_MS=2000 # Idle time before a live note is saved (also saved when the last editor leaves)SPACES_DOC_REINDEX_MINUTES=10 # Most a live note's links/search index may lag its textSPACES_DOC_BASE_TTL_MINUTES=30 # How long a read of a live note stays a valid merge base for a writeSPACES_FILE_LEASE_TTL_SECONDS=180 # "Someone is editing" lease on a space file, without renewalSPACES_MEMORY_MAX_ITEMS=50 # Space-memory entries given to one turn of a space sessionROOMS_MAX_QUEUED_PER_MEMBER=3 # Requests one member may have waiting in a roomROOMS_APPROVAL_TIMEOUT_MINUTES=30 # A room turn waiting this long on an approval gives upROOMS_TRANSCRIPT_WINDOW_CHARS=6000 # Room transcript after the summary before the room is compacted
# ─── Spaces across installs (federation) ─────────────────────FEDERATION_MODE=off # off | visit | host | both (host needs PUBLIC_URL)FEDERATION_LAN_CIDRS= # Ranges a peer link may use over plain ws:// (e.g. 192.168.1.0/24)FEDERATION_HEARTBEAT_SECONDS=15 # Link ping interval; three missed pings close the linkFEDERATION_MAX_VISITORS_PER_INSTANCE=50 # Live memberships one other install may hold hereFEDERATION_MAX_REMOTE_TURNS_PER_INSTANCE=5 # Host agent turns one other install's members may have queued or runningFEDERATION_AGENT_POSTS_PER_HOUR=20 # Agent-labelled posts from other installs per room per hourFEDERATION_REMOTE_POSTS_PER_HOUR=120 # All posts from one other install per room per hour
WORKSPACE_PATH=./workspaceSEARXNG_URL=http://localhost:8888 # SearXNG meta-search (optional)
# ─── Integrations (optional) ─────────────────────────────────N8N_URL=http://localhost:5678N8N_API_KEY=MCP_SERVERS_CONFIG=./mcp-servers.json # Path to MCP client server config (see MCP-INTEGRATION.md)
# ─── Several server processes on one PostgreSQL (optional) ───# Task wakeups reach every process over LISTEN/NOTIFY, and a role heartbeat# runs one turn per hook across all processes (a lease on the hook row that# lapses after the agent timeout + 10 min if its process died). These only# decide which leases a process may clear at startup:# OCTIPUS_INSTANCE_ID=node-a # Unique per process AND stable across its restarts (e.g. a StatefulSet# # pod name, never a random or shared value). When set, a restarted process# # clears the leases its previous boots left. Unset: nothing is cleared at# # startup and a crashed process's leases lapse by TTL. Two processes must# # never share one id: each would clear the other's live leases.# OCTIPUS_SINGLE_PROCESS=0 # 1 = exactly one server process uses this database: clear every lease at# # startup. Never set it when several processes share the database.# # (Embedded PGlite always behaves this way.)
# ─── Migrations ──────────────────────────────────────────────SKIP_MIGRATIONS=false # Use true only if migrations are applied separately before deployment
# ─── Voice (optional) ────────────────────────────────────────WHISPER_MODEL_PATH=PIPER_MODEL_PATH=
# ─── Observability / Opt-outs ────────────────────────────────TRAJECTORY_LOGGING=true # Record one JSONL line per handleMessage run (default on)SKILL_AUTO_EXTENSION=true # Pattern-detect recurring topic/tool sequences into skill_proposals (default on)SPINNER_STYLE=classic # TUI spinner style: classic | kawaiiTopic → Model Routing (Authoritative)
Section titled “Topic → Model Routing (Authoritative)”Specialist model resolution uses topic bindings through ModelRegistry.getModelForTopic(role), with expert preferences and executor/backup bindings where configured. The general root can use the configured default model. See Model routing for planner/executor selection.
Resolution order inside the swarm spawner (SwarmSpawner.resolveChildModelAndExpert):
- Expert
modelPreference— if the matched expert has an explicit preference. ModelRegistry.getModelForTopic(childRole)— otherwise the model bound to the child’s topic.- Throw — if neither resolves, with a message pointing at the Models page. No silent fallback.
- Agent and Subagent children resolve their own topic bindings, not the parent’s model. The root alone can fall back to the configured default.
- The embedding path (
litellm-client.ts:embed()) and vision path (visual/analyzer.ts) resolve theembedding/visiontopic bindings the same way, or throw.
Bind models to topics via the web UI (Settings → Models → Edit → Topics) or the API (PATCH /api/models/:name).
Swarm Config
Section titled “Swarm Config”| Key | Default | Purpose |
|---|---|---|
swarm.perUserSpawnsPerMinute | 30 | Per-user rate limit on spawn_child invocations. Enforced by rate-limiter.ts. |
swarm.orphanReaperIntervalMs | 600000 | Interval for the orphan reaper sweep that flips long-running swarm_nodes to cancelled after process restart. |
Swarm level budgets are configurable under swarm.levelDefaults.root, .agent, and .subagent (tokens, wallMs, fanOut, maxPendingDetached). getLevelDefault in src/core/swarm/types.ts reads them, falling back to LEVEL_DEFAULT. These are execution bounds, not exact billing limits. swarm.contractRetries defaults to 1 (range 0–5) for retryable failed scorer gates.
In Settings → Configuration → Swarm, root.tokens is the root turn’s initial shared pool (swarm.levelDefaults.root.tokens, default 200,000). It is separate from Agent → maxTokenBudget (agent.maxTokenBudget), which bounds the worker and is also the default session budget. Raising the general agent budget does not raise the root swarm pool. New root turns snapshot their configured tokens, wall-clock and fan-out limits; their stored token cap is what the swarm tree displays. Existing tree rows retain their recorded caps. The spawn budget calculation can adopt a higher live parent pool for subsequent delegation without rewriting historical tree rows.
swarm.worktreeIsolation (default false, env SWARM_WORKTREE_ISOLATION) gives each coding-role child running on a CLI adapter (Claude Code, Codex, or any other CLI model) its own git worktree under ~/.octipus/worktrees/<id> on a branch octipus/<id>. It applies only to dev-mode sessions whose project directory is itself a git repository root (never the per-user sandbox), and only when that project has no uncommitted tracked changes at spawn time: a worktree starts at HEAD and would not see them, and its merge would be skipped anyway, so such a child runs on the shared tree. Retries of the same child reuse its worktree. On completion the framework commits any uncommitted work on that branch and reports {branch, headSha, diffStat, filesChanged, merge} on the child’s receipt. It merges the branch into the project’s current branch (git merge --no-ff) only when the child finished ok from an attempt that ran in the worktree (not a native backup-model attempt, skipped_other_attempt), and the project is on a branch, has no uncommitted tracked changes or merge in progress (skipped_dirty), and still contains the commit the worktree started from (skipped_moved). A conflict is aborted and the branch kept. Unmerged branches are never deleted, and a worktree whose HEAD is on no branch (a detached commit) is removed only after that commit is kept by fast-forwarding octipus/<id> or by a octipus/<id>-detached ref. Every server-side git call runs with repository hooks and fsmonitor disabled. Agents spawned by a worktree child (and their own children) inherit its worktree instead of creating another: a CLI descendant runs there, a native descendant’s file and shell tools resolve against it (its projectPath), and their evidence gates measure it; their work is captured by the worktree child’s own commit and merge. A fresh worktree has no dependencies installed, so when the repo root has node_modules, the worktree’s node_modules is a single symlink to it, and /node_modules is added once, with a comment, to the repository’s .git/info/exclude (git keeps that file in the shared git directory, so the line also applies to the main tree, where it only affects an untracked root node_modules). That node_modules is shared, not isolated: an install or delete inside a worktree changes the project’s real dependencies. swarm.worktreeLinkNodeModules (default true, env SWARM_WORKTREE_LINK_NODE_MODULES) turns the symlink off, leaving worktrees without node_modules. While the flag is on, the orphan reaper removes an abandoned worktree only when it is clean and its HEAD and branch are merged into the project, and reports the rest; a worktree whose owning process is still alive (<id>.pid) is left alone. If a worktree cannot be created, the child runs on the shared tree as before.
Both this tokens cap and AGENT_MAX_TOKEN_BUDGET (agent.maxTokenBudget, per-agent) are spend proxies, not context-fill meters. AgentWorker’s own budget gate (src/core/agent-worker.ts:973) checks this.billableTokensUsed directly — fresh input plus paid cache writes plus output, cache reads excluded (billableTokens in src/models/billable-tokens.ts) — a field the worker always owns, so there’s no fallback there. checkSpawnBudget’s syncParentTokenUsage (src/core/swarm/spawn-budget.ts:68) reads the same billable figure off the worker via getBillableTokens?.(), but falls back to getTotalTokens?.() — the grand total — for a worker reference with no billable method. getTotalTokens() itself, and sessions.token_count, stay on the grand total including cache reads; a well-cached run can show a large token count there while staying cheap and nowhere near its budget.
Pipeline Config
Section titled “Pipeline Config”| Key | Default | Purpose |
|---|---|---|
agent.pipelineTokenBudget | 2_000_000 | Token pool for one pipeline RUN, summed over every node visit and checked at each node boundary. 0 disables it. Per-node caps (a template step’s maxTokens) bound a single visit; this bounds the run, which is the only bound a foreach loop respects — plan items can be appended while it runs, so the number of visits is not known when the run starts. Env: PIPELINE_TOKEN_BUDGET. |
multiuser.unattendedDenyActions | [] | Legacy additional deny list for unattended calls; entries name a container (shell) or action (shell__run). ASK is blocked for unattended calls even when this list is empty. Attended children inherit their session approval surface. Use reviewed, scoped, expiring grants for permitted automation. Env: UNATTENDED_DENY_ACTIONS (comma-separated). |
Compaction Config
Section titled “Compaction Config”| Key | Default | Purpose |
|---|---|---|
compaction.minSavingsRatio | 0.10 | Minimum compression ratio that clears the session stall flag. A compaction pass that saves less than this keeps the stall flag set. |
compaction.growthMultiplier | 2.0 | Trigger compaction when current context grows by this multiple relative to the last compaction baseline. |
compaction.hardCeiling | 1_000_000 | Hard ceiling in tokens — compaction always runs above this threshold regardless of other gates. |
Gateway Config
Section titled “Gateway Config”| Key | Default | Purpose |
|---|---|---|
gateway.maxConnectionsPerUser | 20 | Signed-in /gateway connections one user may hold (each browser tab holds one, each TUI one). The one over the cap is refused; the web shows “Too many open tabs”. Env: GATEWAY_MAX_CONNECTIONS_PER_USER. |
gateway.maxFrameBytes | 262144 | Largest frame a gateway client may send, in bytes — the socket’s maxPayload, read at server start. A bigger frame closes the connection (1009). Raise it for large TUI image attachments. Env: GATEWAY_MAX_FRAME_BYTES. |
gateway.replayMaxSessions | 500 | Sessions whose recent events stay in memory so a reconnecting tab can replay what it missed; the least recently active is dropped first. Env: GATEWAY_REPLAY_MAX_SESSIONS. |
Spaces, Rooms and Registration Config
Section titled “Spaces, Rooms and Registration Config”Shared spaces are always on; these keys bound them. What each one governs is described in SPACES.md → Settings.
| Key | Default | Purpose |
|---|---|---|
security.registration | open | Who may create an account: open, invite_only (only with a valid space invite, redeemed with the account) or closed. The install’s first account may always register; SAML, SCIM and admin-created accounts are not affected. Env: REGISTRATION_MODE. |
security.selfRegisteredInstallModels | false | Whether accounts created on the sign-in page (open registration or a space invite link) may run on the install’s models and keys. Off: they use only their own models (Settings → My models) and what a space sponsors, until an admin allows it under Users. Admins, SAML, SCIM and admin-created accounts may always. Env: SELF_REGISTERED_INSTALL_MODELS. |
spaces.creation | any_user | Who may create a space: any_user or admins. Env: SPACES_CREATION. |
spaces.maxMembers | 50 | Most members per space. Env: SPACES_MAX_MEMBERS. |
spaces.inviteMaxTtlHours | 720 | Longest invite lifetime, hours; longer requests are clamped. Env: SPACES_INVITE_MAX_TTL_HOURS. |
spaces.purgeAfterArchiveDays | 7 | Days a space stays archived before its owner can delete it. Env: SPACES_PURGE_AFTER_ARCHIVE_DAYS. |
spaces.noteMaxBytes | 114688 | Largest space note (112 KiB). Startup fails above half of gateway.maxFrameBytes. Env: SPACES_NOTE_MAX_BYTES. |
spaces.docMaxUpdatesPerSecond | 30 | Live-note edits one tab may send per second. Env: SPACES_DOC_MAX_UPDATES_PER_SECOND. |
spaces.docPersistDebounceMs | 2000 | Quiet time before a live note is saved (it is also saved when the last editor leaves). Env: SPACES_DOC_PERSIST_DEBOUNCE_MS. |
spaces.docReindexMinutes | 10 | Most a live note’s links and search index may lag its text; billed as install work to the last editor. Env: SPACES_DOC_REINDEX_MINUTES. |
spaces.docBaseTtlMinutes | 30 | How long a read of a live note stays a valid merge base for a write. Env: SPACES_DOC_BASE_TTL_MINUTES. |
spaces.fileLeaseTtlSeconds | 180 | Lifetime of a lease on a space file without renewal. Env: SPACES_FILE_LEASE_TTL_SECONDS. |
spaces.memoryMaxItems | 50 | Space-memory entries given to one turn, newest first. Env: SPACES_MEMORY_MAX_ITEMS. |
rooms.maxQueuedPerMember | 3 | Requests one member may have waiting in a room. Env: ROOMS_MAX_QUEUED_PER_MEMBER. |
rooms.approvalTimeoutMinutes | 30 | A room turn waiting this long on its requester’s approval gives up. Env: ROOMS_APPROVAL_TIMEOUT_MINUTES. |
rooms.transcriptWindowChars | 6000 | Room transcript kept verbatim after the summary before the room is compacted. Env: ROOMS_TRANSCRIPT_WINDOW_CHARS. |
Backup Config
Section titled “Backup Config”| Key | Default | Purpose |
|---|---|---|
backup.enabled | true | Run the daily backup of the database and the workspace files. Env: BACKUP_ENABLED. |
backup.path | ~/.octipus/backups | Where backups are written. Env: BACKUP_PATH. |
backup.hour | 2 | Hour of the day (server time) the backup runs. Env: BACKUP_HOUR. |
backup.keep | 7 | Good backups kept. Env: BACKUP_KEEP. |
backup.includeFiles | true | Include the workspace files. Env: BACKUP_INCLUDE_FILES. |
See BACKUP.md.
Modules Config
Section titled “Modules Config”| Key | Default | Purpose |
|---|---|---|
modules.enabled | [] | The modules that are on, set in Settings → Modules or by the onboarding. Empty: the core only. Installs from before modules start with every module on. Env: MODULES_ENABLED (comma-separated), read only while nothing is stored: an upgraded install, or one where a module was switched, keeps the stored list, and the upgrade that declares a new module stores a list too. Use Settings → Modules rather than the env var. Changed through /api/modules only, not the generic settings routes. |
protection.remoteAtLevel3 | false | Let conversations holding level 3 content (personal data) use remote models, pseudonymized (PROTECTION-LEVELS.md). Off: level 3 stays on local models, like level 4. Env: PROTECTION_REMOTE_AT_LEVEL3. |
See MODULES.md.
Federation Config
Section titled “Federation Config”Spaces across installs (design: plans/federation-spec.md).
Off by default. Each install has an Ed25519 identity, created on first use and
kept in the vault as the reserved system secret federation.identity, which
the vault API does not list, change or delete: peers pin it. When this install
hosts, other installs dial /federation on it; the endpoint is always
registered and refuses every peer (close code 4403) unless federation.mode
is host or both, so a mode change applies without a restart.
| Key | Default | Purpose |
|---|---|---|
federation.mode | off | off, visit (members here join spaces on other installs), host (members of other installs join spaces here) or both. Hosting needs PUBLIC_URL; without it startup logs an error and invites carry no federation part. A change applies at once: turning hosting off closes every inbound link, turning visiting off every outbound one. Env: FEDERATION_MODE. |
federation.lanCidrs | [] | Addresses or CIDR ranges a peer link may use over plain ws://, dialled or accepted. Everything else needs wss:// to a public address (or, inbound, a trusted proxy that terminated TLS). Loopback, link-local and private addresses are never dialled unless listed here. Env: FEDERATION_LAN_CIDRS (comma-separated). |
federation.heartbeatSeconds | 15 | Ping interval on a peer link; three missed pings close it. Env: FEDERATION_HEARTBEAT_SECONDS. |
federation.maxVisitorsPerInstance | 50 | Live space memberships the members of one other install may hold here. Env: FEDERATION_MAX_VISITORS_PER_INSTANCE. |
federation.maxRemoteTurnsPerInstance | 5 | Host agent turns, queued or running, that one other install’s members may have started here. Env: FEDERATION_MAX_REMOTE_TURNS_PER_INSTANCE. |
federation.agentPostsPerHour | 20 | Posts labelled as a visitor’s agent that one room accepts per hour. Env: FEDERATION_AGENT_POSTS_PER_HOUR. |
federation.remotePostsPerHour | 120 | All posts, agent-labelled or not, that one room accepts per hour from the visitors of one install (the label is the visitor’s word; this bound is not). Env: FEDERATION_REMOTE_POSTS_PER_HOUR. |
Reverse proxy
Section titled “Reverse proxy”Octipus takes a client’s address from the TCP connection. The
X-Forwarded-For and X-Real-IP headers are ignored — anyone can send them —
unless the connection comes from a proxy listed in security.trustedProxies
(env TRUSTED_PROXIES, comma-separated; addresses or CIDR ranges, IPv4 or
IPv6). The address is used for the per-address REST rate limits, the login
and passkey lockouts, the gateway’s cap on unauthenticated connections, and
the audit log. It never grants anything: there is no “local” trust, so a
request from loopback is treated like any other.
Behind nginx, Caddy or a load balancer, list the proxy, or every client shares the proxy’s address for those limits:
# nginx / Caddy on the same hostTRUSTED_PROXIES=127.0.0.1,::1# a load balancer subnetTRUSTED_PROXIES=10.0.0.0/8The proxy must set (not append to a client-supplied) X-Forwarded-For, or
append the peer address as nginx’s $proxy_add_x_forwarded_for does: the
header is read right to left, skipping trusted proxies, and the first other
hop is the client. An entry that is not an address or CIDR range is rejected
when the configuration loads.
The bundled web server (web/serve.mjs: port 3007 in the image, published as
3017 by docker-compose.yml) proxies /api, /a and /__artifacts__ to the
backend over loopback and appends its peer address to X-Forwarded-For. In
that setup, set TRUSTED_PROXIES=127.0.0.1,::1 so the backend sees each
browser’s address instead of one shared one; a client that forges the header
only adds hops left of the real one, which are ignored. If nginx or a load
balancer sits in front of port 3017, list it too, and have it set or append
X-Forwarded-For as above (a front proxy that sends only X-Real-IP is not
enough here, because the bundled server always sends X-Forwarded-For).
Docker Services
Section titled “Docker Services”Embedded mode requires no external database service. For external mode, provide PostgreSQL with pgvector. The following is an example for operators who maintain a separate ~/docker-services Compose project; that directory is not shipped by Octipus:
# Start required servicescd ~/docker-servicesdocker compose up -d db
# Start optional servicesdocker compose up -d ollama litellm searxng| Service | Port | Image | Required |
|---|---|---|---|
| PostgreSQL | 5432 | pgvector/pgvector:pg16 | External storage mode only |
| Ollama | 11434 | ollama/ollama:rocm | No |
| LiteLLM | 4000 | ghcr.io/berriai/litellm:main-latest | No |
| SearXNG | 8888 | searxng/searxng:latest | No |
Artifacts hosting
Section titled “Artifacts hosting”Live artifacts (docs/ARTIFACTS.md) ship hosted HTML pages. Two modes:
Recommended: subdomain isolation
Section titled “Recommended: subdomain isolation”- Add a DNS record (A or CNAME) for
artifacts.<your-host>pointing to the same origin as your main app. Cloudflare users: orange-cloud is fine; Universal SSL covers it. If you already have*.<host>wildcard DNS, no new record is needed. - Set
ARTIFACTS_HOST=artifacts.<your-host>in your env. - Reverse-proxy any
Host: artifacts.<your-host>traffic to the same octipus backend; the in-process router matches the host header.
This puts hosted user-influenced HTML on a different origin so it cannot
read app cookies, ride localStorage, or hit /api/* directly with
session credentials.
No-DNS fallback
Section titled “No-DNS fallback”Leave ARTIFACTS_HOST unset. Artifacts are served at
/__artifacts__/a/:slug on the main host. The iframe still uses
sandbox (no allow-same-origin) but a CSP-escape bug becomes a
same-origin XSS against the app.
Acceptable for single-user / trusted-tenant deploys; not recommended when untrusted users can author artifacts in your workspace.
Other env vars
Section titled “Other env vars”| Var | Purpose |
|---|---|
ARTIFACTS_HOST | Subdomain for hosted pages. Empty → path-prefix fallback. |
ARTIFACTS_PROTO | https (default) or http for local dev. |
ARTIFACTS_GATEWAY_WSS | Gateway WS origin baked into the embed CSP connect-src. |
ARTIFACT_SDK_SHA256 | sha256 of octipus-artifact-client.js — pinned in CSP. |
ARTIFACT_TOKEN_SECRET | HMAC key for artifact-scoped JWT. Falls back to JWT_SECRET HKDF. |
ARTIFACT_BUNDLES_DIR | Filesystem root for custom JS bundles. Default data/artifacts/. |