Skip to content

Reference

This page is the exhaustive one. It describes the stack as it runs today, which is the Compose deployment of the published images. Commands that belong to the q15 binary are marked not built and paired with what to type instead.

This is the sequence that produces a running stack on the published images, in order, with what you should see at each step. It is longer than the four commands on Install q15 because it is the path that exists.

  1. Get the deployment directory.

    Today that means a checkout, because the Compose file and the secret templates live in the repository. When the q15 binary ships, this step is the installer instead, and it disappears.

    Terminal window
    git clone --depth 1 https://github.com/q15co/q15.git
    cd q15
  2. Seed the configuration and secret files.

    Terminal window
    make compose-secrets-init
    cp deploy/compose/release.env.example deploy/compose/release.env

    You should now see nine new files: eight under deploy/compose/secrets/ and auth/auth.json beside them. Each printed line is either wrote or kept; kept means the file was already there, because nothing is overwritten.

  3. Fill in what your deployment needs.

    At minimum, a provider block with a key your provider accepts, and the matching file in secrets/ with one value in it and mode 600. Models and Credentials cover those two, and the shipped config already points the embedding backend at the local q15-tei container.

  4. Decide the origin before you enrol.

    release.env, or the environment
    Q15_WEB_ORIGIN=https://chat.example.net

    The passkey you create in step 6 is bound to this hostname. Changing it later fails startup, so this is the one value you cannot repair afterwards.

  5. Start the stack.

    Terminal window
    podman compose --env-file deploy/compose/release.env \
    -f deploy/compose/docker-compose.image-first.yml up -d --wait

    --wait returns only when every health check passes, so the command ending is the answer.

    You should now see six containers running, with only the web tier publishing a port. If one is missing or restarting, podman compose … logs -f <name> names the input it could not read, and Troubleshooting has the short list.

  6. Enrol your authenticator.

    Terminal window
    podman compose --env-file deploy/compose/release.env \
    -f deploy/compose/docker-compose.image-first.yml exec q15-web \
    /usr/local/bin/q15-web auth enroll 'Laptop'

    The command prints a blob and waits. Open the app, paste the blob, create the credential, and paste the one-line response back inside the five-minute window. See Chat for the constraints on acceptable authenticators.

  7. Reach it. The web tier binds loopback, so use an SSH forward, or put a TLS terminator in front and set the origin to that hostname. A worked reverse-proxy example is in development.

  8. Back it up before you need it. /memory and /workspace are the two that cannot be recreated. Backups has the list and the order.

Stack File Images Use it for
Deployment deploy/compose/docker-compose.image-first.yml Published ghcr.io/q15co/q15-* tags A server that should stay running.
Development docker-compose.yml (repository root) Built locally from source Working on q15 itself. Project q15-local.

Both are plain Compose files and the images are ordinary OCI images, so podman (the default) and Docker run the same commands. The examples here use podman compose; on Docker, use docker compose and change nothing else.

deployment, from a checkout of the repository
make compose-secrets-init
cp deploy/compose/release.env.example deploy/compose/release.env
# edit release.env, agent-config.yaml, proxy-policy.yaml and secrets/
podman compose --env-file deploy/compose/release.env \
-f deploy/compose/docker-compose.image-first.yml up -d --wait

--wait returns only when every health check passes. Nothing is built on your host: the deployment file uses image: only.

Paths are relative to deploy/compose/, which is where the Compose file expects them.

File Mounted at Purpose
release.env — Q15_IMAGE_TAG and embedding-backend overrides. Passed with --env-file.
agent-config.yaml /etc/q15/agent/config.yaml Providers, tools, Telegram, the bridge socket.
proxy-policy.yaml /etc/q15/proxy/policy.yaml Egress passthrough and credential injection rules.
auth/ /etc/q15/auth OpenAI OAuth credential state. Mount the directory, not the file.
secrets/<name> /run/q15-secrets/<name> One file per credential, each paired with a *_FILE environment variable.

make compose-secrets-init copies the tracked *.example templates to those names and never overwrites a file that already exists.

Volume Mount Contents
q15_workspace /workspace The durable project tree, working files, embedding source registry.
q15_memory /memory Identity, semantic knowledge, working state, turn history.
q15_skills /skills Installed skills. @builtin/ ships with the image.
q15_media /media The media store: attachments and generated media.
q15_agent_state /var/lib/q15/agent Scheduled-job definitions and run records.
q15_proxy_state /var/lib/q15/proxy Proxy-owned state, including the interception CA.
q15_web_state /var/lib/q15-web Web credential store and admin socket. Seeded UID 65532, GID 1000, mode 0700.
q15_bridge /run/q15 The bridge socket. Mounted in the agent and the web tier only, read-only on web.
q15_qdrant_storage /qdrant/storage Embedding collections.
q15_exec_nix_store /nix The executor’s package store, kept warm across restarts.
q15_tei_hf_cache /data Embedding model weights.
Service Inside the stack Published
q15-web 0.0.0.0:8080 (the deployment file sets it) 127.0.0.1:8080
q15-agent bridge socket at /run/q15/bridge.sock —
q15-exec :50051 (gRPC) —
q15-proxy :18080 (HTTP proxy), :50052 (admin gRPC) —
q15-qdrant :6334 —
q15-tei :80 —

Only the web tier publishes anything, and only on loopback. The port is pinned by the contract test, not by a variable:

Terminal window
make compose-check
podman-compose -f docker-compose.yml config
podman-compose -f deploy/compose/docker-compose.image-first.yml config

make test runs the same check, so a change that moves the port or the mount set fails the gate.

Route What it does
GET /ws The browser socket: streaming replies, tool activity, history paging, media.
GET /api/turns Transcript history, newest first, paged with after_seq and limit.
POST /api/media Uploads a sealed batch of attachments. One proof-authorized request per send.
GET /api/media/{hash} Serves a stored attachment back, streamed in 32 KiB encrypted chunks.
GET / and assets The app itself, served from the binary. Hashed files only; no directory listings.
GET /healthz The only unauthenticated endpoint. Used by the container health check.

There is no password, no bearer token, no query token and no proxy-identity fallback. Unauthenticated requests to anything but /healthz answer 401, and an empty credential store starts locked.

Every frame that carries content is sealed between the browser and the agent: chat bodies, reasoning, tool arguments and results, live deltas, snapshots and replayed history. Unknown frame types are sealed by default, so a new frame type fails closed rather than leaking. Only a small control set (ids, counters, timestamps, error codes) travels in plaintext.

The keys are derived per connection and bound to the session: an ephemeral ECDH exchange between the browser and the agent, then HKDF-SHA-256 key derivation, then AES-256-GCM per direction, with the chunk index bound into each message. The web tier relays opaque bytes; it holds no key and parses no chat content.

Attachments ride the same envelope. The composer holds files in memory until you send; one proof-authorized upload carries a sealed batch, capped at 16 files and 8 MiB of decoded file bytes per send. The agent sniffs the type, stores the file under a content hash, and serves it back through an authenticated descriptor carrying filename, type and size.

providers:
- name: ollama-cloud
type: ollama
base_url: https://ollama.com
key_env: OLLAMA_API_KEY
discovery:
models_dev: true
agent:
name: Q15
memory_recent_turns: 6
bridge:
listen_target: unix:///run/q15/bridge.sock
tools:
web_search:
brave_api_key_env: BRAVE_API_KEY
embeddings:
qdrant_url_env: Q15_QDRANT_URL
provider: openai
base_url_env: Q15_EMBEDDINGS_BASE_URL
model: qwen3-embedding-0.6b
dimensions: 1024
batch_size: 128
schedule:
max_jobs: 64
max_run_turns: 16
telegram:
token_env: Q15_TELEGRAM_TOKEN
allowed_user_ids_env: Q15_TELEGRAM_ALLOWED_USER_IDS
Key Type Default Notes
providers[].name string required The label used in model refs.
providers[].type enum required ollama, openai-compatible, openai-codex.
providers[].base_url string provider default For example https://ollama.com, or your own server.
providers[].key_env string — The environment variable holding the key. *_FILE also works.
providers[].discovery.models_dev bool false Enrich discovered models with cost, context and benchmark metadata.
providers[].discovery.include/exclude []string — Glob filters over discovered model ids.
agent.name string required The agent’s own name, written into its identity files. The planned installer derives the unit and volume names from it; the shipped Compose stack uses fixed names.
agent.memory_recent_turns int 6 How many recent turns are replayed verbatim.
agent.bridge.listen_target string unix:///run/q15/bridge.sock Unix only. TCP is refused: this is the identity surface.
agent.tools.web_search.brave_api_key_env string — Omit the block to run without web search.
agent.tools.embeddings.* — see Memory provider is openai (local TEI) or gemini; omit the block to disable.
agent.tools.schedule.max_jobs int 64 Bounded at 1000.
agent.tools.schedule.max_run_turns int 16 Bounded at 128.
agent.telegram.token_env string — Omit to run without Telegram.
agent.telegram.allowed_user_ids_env string — Required if Telegram is configured. An empty list is refused.

There is no model list in this file. Providers publish rosters, and the current model is runtime state. See Models.

proxy:
no_proxy:
- localhost
- 127.0.0.1
- ::1
- q15-proxy
- q15-exec
set_lowercase_proxy_env: true
secrets:
- github_token
rules:
- name: github-api
match_hosts:
- api.github.com
env:
- name: GH_TOKEN
secret: github_token
rules:
- github-api
in:
- header
Key Purpose
proxy.no_proxy Hosts that bypass the proxy entirely.
proxy.set_lowercase_proxy_env Also export the proxy variables in lowercase.
proxy.secrets The secret aliases this policy may use.
proxy.rules[].match_hosts Hostnames the rule applies to.
proxy.rules[].match_path_prefixes Optional path restriction inside a host.
proxy.rules[].set_header Headers to set, with {{ secret.alias }} placeholders.
proxy.rules[].set_basic_auth username plus a secret, injected as an Authorization: Basic header.
proxy.rules[].replace_placeholder Literal placeholders replaced in a query, header or body.
proxy.env[] An environment variable the executor receives, backed by a secret, gated by rules.
proxy.env[].in Where the value may appear: header, env, and so on.

Full detail, including the interception model and response scrubbing, is in Credentials.

Variable Default Purpose
Q15_IMAGE_TAG stable The release tag applied to all four q15 images.
Q15_WEB_ORIGIN required Exact public HTTPS origin, no trailing slash. Bound to passkeys.
Q15_WEB_LISTEN 127.0.0.1:8080 Listen address inside the container. The deployment file sets 0.0.0.0:8080 so the published port works.
Q15_WEB_BRIDGE unix:///run/q15/bridge.sock The agent bridge the web tier dials.
Q15_WEB_STATE_DIR /var/lib/q15-web Credential and session state.
Q15_WEB_TLS_CERT, Q15_WEB_TLS_KEY unset Optional direct TLS; both or neither.
Q15_WEB_DIR unset Serve a development bundle from a directory instead of the embedded one.
TEI_IMAGE ghcr.io/huggingface/text-embeddings-inference:turing-1.9 The embedding server image.
TEI_MODEL Qwen/Qwen3-Embedding-0.6B The model the container serves.
TEI_SERVED_MODEL_NAME qwen3-embedding-0.6b Must match model in agent-config.yaml.
TEI_MAX_CLIENT_BATCH_SIZE 128 Must be at least the config’s batch_size.

Every binary reads a secret from NAME, and failing that from the file named by NAME_FILE. The file’s contents are trimmed, and an empty file is an error rather than an empty secret. That one rule is how secrets/github_token reaches a container as GITHUB_TOKEN_FILE=/run/q15-secrets/github_token.

The tracked templates are:

secrets/brave_api_key secrets/ollama_api_key
secrets/gemini_api_key secrets/q15_telegram_allowed_user_ids
secrets/github_token secrets/q15_telegram_token
secrets/moonshot_api_key secrets/zai_api_key
auth/auth.json.example

They are templates: make compose-secrets-init copies each to its real name, and the real files are git-ignored. See Credentials for what each one is used for.

Command Status Today
q15 init not built Write agent-config.yaml, proxy-policy.yaml and secrets/ by hand.
q15 up not built podman compose … up -d --wait
q15 status not built podman compose … ps
q15 logs -f <service> not built podman compose … logs -f q15-agent
q15 doctor not built Container health checks plus the log lines that name the failing input.
q15 secret set not built Write the file into secrets/ and recreate the container that reads it.
q15-web auth enroll|list|revoke DEVICE_ID built podman compose exec q15-web /usr/local/bin/q15-web auth …
q15-auth login|status|logout built Run on the host against the auth/ directory.

A release is one set of four images, always published together:

Tag Meaning
stable A moving tag. It advances on all four packages only after the same release is verified on all four.
YYYY.MM.DD.<run-number> One immutable release, for example 2026.10.10.84. The same tag always selects one compatible set of four.

Published tags are on the q15 packages page, and the images pull without registry authentication. Pin an immutable tag if you want control over when your stack moves. See Updating for what does not roll back.

Terminal window
make project-setup # the pinned toolchain, under ./.tools
make compose-up # builds from the working tree and starts with --wait
make compose-ps
make compose-logs SERVICE=q15-agent
make compose-down
make verify # lint, tests, contract checks
make build # agent, auth, exec, proxy and web binaries into ./bin
make ui-dev # the browser client against a local API
make ui-test # browser unit tests

This stack builds from source and follows the moving :main tag. Use it to work on q15; use the deployment stack to run it.