# lynox — single-instance Docker Compose setup # # Quick start: # 1. Copy .env.example to .env and fill in ANTHROPIC_API_KEY # 2. Required on native Linux, before the first start: mkdir -p ~/.lynox, # then the line under "Who the container runs as" in .env.example # (Docker Desktop: only the mkdir) # 3. docker compose up # 4. Open http://localhost:3000 and enter the access token: # docker compose exec lynox cat /home/lynox/.lynox/.access-token # (the log shows only its first 8 characters, on the first start) # # Full docs: https://docs.lynox.ai/getting-started/ services: lynox: image: ghcr.io/lynox-ai/lynox:latest # Explicit platform pin — safe default while the multi-arch manifest # rolls out. Apple Silicon users can remove this line once :latest is # multi-arch to get a native arm64 image instead of Rosetta emulation. platform: linux/amd64 restart: unless-stopped read_only: true # The user the engine runs as. It has to own ~/.lynox on the host, which # is mounted below. Set LYNOX_UID/LYNOX_GID in .env before the first start # (see .env.example and https://docs.lynox.ai/setup/docker/). Without them it # runs as the image's own user, 1001, which can only write ~/.lynox if that # directory belongs to 1001. user: "${LYNOX_UID:-1001}:${LYNOX_GID:-1001}" ports: - "3000:3000" tmpfs: - /tmp:size=512M - /workspace:size=256M,uid=${LYNOX_UID:-1001},gid=${LYNOX_GID:-1001} # ONNX embedding cache for the Knowledge Graph (Xenova/transformers.js). # Without this writable tmpfs the read_only root filesystem rejects the # subdirectory `mkdir` and every web-research call spams the engine log # with ENOENT; semantic search has to re-download from huggingface on # every restart anyway, so a tmpfs is exactly the right lifetime. - /home/lynox/.cache:size=512M,uid=${LYNOX_UID:-1001},gid=${LYNOX_GID:-1001} cap_drop: - ALL security_opt: - no-new-privileges pids_limit: 512 logging: driver: json-file options: max-size: "20m" max-file: "3" environment: # An id the image has no passwd entry for would otherwise get / as its home. - HOME=/home/lynox # ── Required ────────────────────────────────────── - ANTHROPIC_API_KEY # Your Anthropic API key (starts with sk-ant-) # ── Recommended ────────────────────────────────── # Set these explicitly for production. If omitted, they are # auto-generated on first start and saved to the volume. # - LYNOX_HTTP_SECRET # Access token for Web UI login # - LYNOX_VAULT_KEY # Encrypt data at rest (generate: openssl rand -base64 48) # ── Required for the /login form (CSRF) ────────── # SvelteKit's CSRF guard rejects POST forms when the request URL # protocol doesn't match the browser's Origin header. adapter-node # defaults to https when ORIGIN is unset, so http://localhost gets # 403'd on every form submit (e.g. token-based login). Set ORIGIN # to your actual bind URL — scheme + host + port, no path. - ORIGIN=http://localhost:3000 # ── Web Search ──────────────────────────────────── - SEARXNG_URL=http://searxng:8080 # Free, unlimited via SearXNG below. # Without SEARXNG_URL set, lynox falls back to a best-effort # DuckDuckGo HTML-scrape — works for casual research, but SearXNG # is the only full-quality backend. # ── Optional features ───────────────────────────── # - GOOGLE_CLIENT_ID # Google Workspace OAuth # - GOOGLE_CLIENT_SECRET # Google Workspace OAuth # - LYNOX_BUGSINK_DSN # Error reporting (opt-in, Bugsink) # ⚠ CRITICAL: This volume persists your data AND vault key. # Without it, all data and encryption keys are lost on container restart. # Create it first: mkdir -p ~/.lynox. The long form makes a missing # directory fail the start; the short form would let Docker create it # owned by root, which only sudo can repair. volumes: - type: bind source: ${HOME}/.lynox target: /home/lynox/.lynox bind: create_host_path: false networks: - internal # ── SearXNG — free web search ────────────────────── searxng: image: searxng/searxng:latest restart: unless-stopped read_only: true tmpfs: - /tmp:size=64M cap_drop: - ALL security_opt: - no-new-privileges pids_limit: 128 logging: driver: json-file options: max-size: "5m" max-file: "2" volumes: - ./searxng/settings.yml:/etc/searxng/settings.yml:ro networks: - internal # What this network is, and what it is not — measured 2026-08-10, because the # docs used to call it "network isolation" and it is not that. # # It is NOT `internal: true`. Outbound from both containers is unrestricted; # the engine needs it to reach the LLM API, so `internal: true` would break it. # # What it DOES buy is opt-in membership: a service added without a `networks:` # key joins Compose's implicit `default` network instead and gets no route and # no DNS to the services here — measured both ways with real containers. # # ⚠ Two limits, both measured, so nobody mistakes this for a boundary: # - A sidecar with the Docker socket defeats it in one command # (`docker network connect`), so it stops accidents, not adversaries. # - It cuts BOTH ways. Anything you add that must reach `lynox` — the # cloudflared sidecar in setup/remote-access.md is the live example — needs # `networks: [internal]`, or it starts happily and silently cannot connect. networks: internal: driver: bridge