635 lines
24 KiB
YAML
635 lines
24 KiB
YAML
# =====================================================================
|
|
# owuitools - OpenAPI tool suite for the qwen3.8 assistant in Open WebUI
|
|
#
|
|
# docker compose up -d --build (from the repository root)
|
|
#
|
|
# WHY TEN CONTAINERS
|
|
#
|
|
# Anyone who can talk to this assistant can reach anything the model can
|
|
# call. The realistic attack is not someone breaking out of a container -
|
|
# it is someone talking the model into fetching an address on your LAN
|
|
# and reading the answer back out. A URL allowlist in Python is one typo
|
|
# away from failing open, so the boundary is the network instead.
|
|
#
|
|
# Six containers are the tool server:
|
|
#
|
|
# netguard owns a network namespace with a default-deny filter. api and
|
|
# searxng run inside it and have no interface of their own. See
|
|
# netguard/firewall.sh for why an `internal: true` network alone was
|
|
# NOT enough - the host is on-link at the bridge gateway address, and
|
|
# was reachable until this existed.
|
|
#
|
|
# gw is the only container on Open WebUI's network, and it is a plain
|
|
# reverse proxy holding no application code.
|
|
#
|
|
# egress (squid) is the only container that touches the internet, and
|
|
# it re-resolves every hostname itself before applying its
|
|
# private-range denies - so DNS rebinding dies there too.
|
|
#
|
|
# valkey is the cache, and octave is the code sandbox, which has
|
|
# `network_mode: none` and is reached by a file drop.
|
|
#
|
|
# Four more are the VM sub-agent, and they repeat the same shape one
|
|
# level down:
|
|
#
|
|
# vmguard owns a namespace whose allowlist is one address. vmagent runs
|
|
# inside it, holds no secret, and can reach nothing else.
|
|
#
|
|
# vmgw is that one address: an nginx exposing exactly two Ollama paths.
|
|
# Ollama has no authentication and serves DELETE /api/delete, so
|
|
# anything that can reach it can delete your model.
|
|
#
|
|
# Addresses are static because the firewall rules name them, and a rule
|
|
# pointing at a stale address is a rule that silently stops matching.
|
|
# =====================================================================
|
|
|
|
name: owuitools
|
|
|
|
x-hardened: &hardened
|
|
restart: unless-stopped
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
# No docker socket. Bind mounts are read-only config, with two
|
|
# deliberate exceptions: ./logs, which every service writes its own log
|
|
# file into, and vmagent's /run/owuivm/agent socket directory - the one
|
|
# host mount in the suite, and the whole reason the privileged side is
|
|
# reachable only through a fixed verb list.
|
|
|
|
services:
|
|
|
|
# -------------------------------------------------------------------
|
|
# Ingress. Bridges openwebui_default -> the guarded namespace.
|
|
# -------------------------------------------------------------------
|
|
gw:
|
|
<<: *hardened
|
|
image: nginx:stable-alpine
|
|
container_name: owuitools-gw
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=16m,mode=1777
|
|
- /var/cache/nginx:size=16m
|
|
- /var/run:size=1m
|
|
cap_add:
|
|
- CHOWN
|
|
- SETGID
|
|
- SETUID
|
|
volumes:
|
|
- ./configs/services/gateway.nginx.conf:/etc/nginx/nginx.conf:ro
|
|
# Every component writes its own file into ./logs, so there is one
|
|
# place to look rather than nine compose invocations. stdout is
|
|
# unchanged, so `docker compose logs` still works. See logs/README.md.
|
|
- ./logs:/logs
|
|
networks:
|
|
core:
|
|
ipv4_address: 172.22.0.10
|
|
openwebui:
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
mem_limit: 64m
|
|
pids_limit: 64
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8000/health"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
labels:
|
|
wud.watch: "true"
|
|
wud.tag.include: '^\d+\.\d+\.\d+-alpine$$'
|
|
|
|
# -------------------------------------------------------------------
|
|
# The guarded namespace. Holds no application logic - it exists to own
|
|
# the netns and the iptables rules that api and searxng live behind.
|
|
# -------------------------------------------------------------------
|
|
netguard:
|
|
<<: *hardened
|
|
image: nicolaka/netshoot:v0.16
|
|
container_name: owuitools-netguard
|
|
cap_add:
|
|
- NET_ADMIN
|
|
volumes:
|
|
- ./netguard/firewall.sh:/firewall.sh:ro
|
|
- ./logs:/logs
|
|
# ---------------------------------------------------------------
|
|
# RECREATING THIS CONTAINER STRANDS THE ONES THAT SHARE ITS NETNS.
|
|
#
|
|
# api and searxng join this container's network namespace. Docker
|
|
# does not re-attach them when it is replaced, so after a
|
|
# `docker compose up -d netguard` they are still running, still
|
|
# healthy by their own healthcheck - which is a loopback call - and
|
|
# have no route to anything at all. Measured 2026-09-15: squid and
|
|
# valkey both came back "Network is unreachable" and the gateway got
|
|
# connection refused, with every container reporting Up.
|
|
#
|
|
# So: touching this service means recreating its dependents too.
|
|
#
|
|
# docker compose up -d --force-recreate netguard api searxng
|
|
#
|
|
# The same applies to vmguard and vmagent below.
|
|
# ---------------------------------------------------------------
|
|
environment:
|
|
# The filter's allowlist, from the same file that pins the addresses.
|
|
OWUITOOLS_GW_IP: 172.22.0.10
|
|
OWUITOOLS_EGRESS_IP: 172.22.0.12
|
|
OWUITOOLS_VALKEY_IP: 172.22.0.14
|
|
OWUITOOLS_API_PORT: "8000"
|
|
entrypoint: ["/bin/sh", "-e", "/firewall.sh"]
|
|
networks:
|
|
core:
|
|
ipv4_address: 172.22.0.11
|
|
mem_limit: 64m
|
|
pids_limit: 32
|
|
healthcheck:
|
|
# Nothing may start in this namespace until the filter is confirmed
|
|
# applied; firewall.sh only creates this after verifying its rules.
|
|
test: ["CMD", "test", "-f", "/tmp/firewall-ready"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 3s
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
# -------------------------------------------------------------------
|
|
# The tool server. No network interface of its own.
|
|
# -------------------------------------------------------------------
|
|
api:
|
|
<<: *hardened
|
|
build:
|
|
context: ./app
|
|
image: local/owuitools-api:latest
|
|
container_name: owuitools-api
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=64m,mode=1777
|
|
volumes:
|
|
- octave_jobs:/jobs
|
|
# The VM sandbox's dispatch, same file-drop protocol as octave.
|
|
# Mounting it is also what makes the vm_agent tool appear at all:
|
|
# main.py registers that router only when this directory exists, so
|
|
# a host without the VM stack does not describe a tool it cannot run.
|
|
- vm_jobs:/vmjobs
|
|
# The tool switch. Read at startup: a tool set to false here is
|
|
# dropped from the OpenAPI spec AND its endpoint stops existing, so
|
|
# the model is never told about it and you stop paying its schema
|
|
# tokens. Mounted rather than baked so a change is a restart, not a
|
|
# rebuild - but the spec is cached for the process lifetime, so it
|
|
# IS a restart plus a re-save of the Open WebUI connection.
|
|
- ./configs/tools.toml:/configs/tools.toml:ro
|
|
# Every component writes its own file into ./logs, so there is one
|
|
# place to look rather than nine compose invocations. stdout is
|
|
# unchanged, so `docker compose logs` still works. See logs/README.md.
|
|
- ./logs:/logs
|
|
network_mode: "container:owuitools-netguard"
|
|
environment:
|
|
OWUITOOLS_OCTAVE_JOBS_DIR: /jobs
|
|
OWUITOOLS_VM_JOBS_DIR: /vmjobs
|
|
OWUITOOLS_TOOLS_CONFIG: /configs/tools.toml
|
|
OWUITOOLS_VM_WAIT_SECONDS: ${OWUITOOLS_VM_WAIT_SECONDS:-7000}
|
|
OWUITOOLS_AUTH_TOKEN: ${OWUITOOLS_AUTH_TOKEN:?generate one - see .env.example}
|
|
OWUITOOLS_EGRESS_PROXY: http://172.22.0.12:3128
|
|
# searxng shares this namespace, so it is reachable on loopback.
|
|
OWUITOOLS_SEARXNG_URL: http://127.0.0.1:8080
|
|
OWUITOOLS_VALKEY_URL: redis://172.22.0.14:6379/0
|
|
OWUITOOLS_CONTACT: ${OWUITOOLS_CONTACT:-owuitools@localhost}
|
|
OWUITOOLS_PUBLIC_URL: ${OWUITOOLS_PUBLIC_URL:-}
|
|
OWUITOOLS_MAX_RESPONSE_CHARS: ${OWUITOOLS_MAX_RESPONSE_CHARS:-12000}
|
|
OWUITOOLS_PROBE_RATE_PER_HOUR: ${OWUITOOLS_PROBE_RATE_PER_HOUR:-30}
|
|
OWUITOOLS_LOG_LEVEL: ${OWUITOOLS_LOG_LEVEL:-INFO}
|
|
# All optional. Each lifts a rate limit that is otherwise tight
|
|
# enough to matter for a shared assistant; none are required.
|
|
OWUITOOLS_GITHUB_TOKEN: ${OWUITOOLS_GITHUB_TOKEN:-}
|
|
OWUITOOLS_STACKEXCHANGE_KEY: ${OWUITOOLS_STACKEXCHANGE_KEY:-}
|
|
OWUITOOLS_NVD_KEY: ${OWUITOOLS_NVD_KEY:-}
|
|
depends_on:
|
|
netguard:
|
|
condition: service_healthy
|
|
octave:
|
|
condition: service_started
|
|
valkey:
|
|
condition: service_healthy
|
|
egress:
|
|
condition: service_started
|
|
searxng:
|
|
condition: service_started
|
|
mem_limit: 1g
|
|
cpus: 2.0
|
|
pids_limit: 256
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/health')"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
# -------------------------------------------------------------------
|
|
# Search backend. Also inside the guarded namespace: it does not accept
|
|
# arbitrary URLs, but it has no business reaching the host either, and
|
|
# sharing the namespace costs nothing.
|
|
# -------------------------------------------------------------------
|
|
searxng:
|
|
<<: *hardened
|
|
image: searxng/searxng:latest
|
|
container_name: owuitools-searxng
|
|
cap_add:
|
|
- CHOWN
|
|
- SETGID
|
|
- SETUID
|
|
- DAC_OVERRIDE
|
|
network_mode: "container:owuitools-netguard"
|
|
volumes:
|
|
- ./configs/services/searxng.settings.yml:/etc/searxng/settings.yml:ro
|
|
- ./logs:/logs
|
|
# THE ONE COMPONENT THAT CANNOT WRITE ITS OWN LOG FILE.
|
|
#
|
|
# SearXNG logs through Python's logging to stdout and exposes no file
|
|
# option, and the image dropped uwsgi (which did have one). So its
|
|
# output is teed on the way past instead.
|
|
#
|
|
# The cost is that PID 1 becomes the shell, which does not forward
|
|
# SIGTERM through a pipeline - so stopping this container waits out
|
|
# the timeout and then kills it. That is acceptable here and nowhere
|
|
# else in this file: SearXNG holds no state, serves no writes, and a
|
|
# hard kill loses nothing.
|
|
entrypoint: ["/bin/sh", "-c",
|
|
"/usr/local/searxng/entrypoint.sh 2>&1 | tee -a /logs/searxng.log"]
|
|
environment:
|
|
SEARXNG_BASE_URL: http://127.0.0.1:8080/
|
|
# NOTE: this image used to run under uwsgi and no longer does, so
|
|
# the UWSGI_* settings below are inert on current tags. They are
|
|
# kept because they cost nothing and still apply if you pin an
|
|
# older image.
|
|
# Engine fan-out goes through the same chokepoint as everything else.
|
|
HTTP_PROXY: http://172.22.0.12:3128
|
|
HTTPS_PROXY: http://172.22.0.12:3128
|
|
NO_PROXY: localhost,127.0.0.1
|
|
SEARXNG_LIMITER: "false"
|
|
UWSGI_WORKERS: "4"
|
|
UWSGI_THREADS: "4"
|
|
depends_on:
|
|
netguard:
|
|
condition: service_healthy
|
|
egress:
|
|
condition: service_started
|
|
mem_limit: 512m
|
|
pids_limit: 128
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
# -------------------------------------------------------------------
|
|
# The only container with internet access. See configs/services/egress.squid.conf.
|
|
# -------------------------------------------------------------------
|
|
egress:
|
|
<<: *hardened
|
|
image: ubuntu/squid:latest
|
|
container_name: owuitools-egress
|
|
read_only: true
|
|
# Squid normally starts as root and setuids to proxy - after which it
|
|
# can no longer reopen /dev/stdout, because the container's stdout pipe
|
|
# belongs to root, and it dies with "Cannot open '/dev/stdout' for
|
|
# writing". Being proxy from the outset keeps the access log on
|
|
# `docker logs`, which is where the audit trail of what the assistant
|
|
# fetched needs to be.
|
|
user: "13:13"
|
|
tmpfs:
|
|
- /var/spool/squid:size=32m
|
|
- /var/log/squid:size=16m
|
|
- /var/run:size=4m
|
|
- /tmp:size=8m
|
|
volumes:
|
|
- ./configs/services/egress.squid.conf:/etc/squid/squid.conf:ro
|
|
- ./logs:/logs
|
|
networks:
|
|
core:
|
|
ipv4_address: 172.22.0.12
|
|
wan:
|
|
mem_limit: 256m
|
|
pids_limit: 128
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
# -------------------------------------------------------------------
|
|
# Response cache. Load-bearing, not an optimisation: Stack Exchange
|
|
# allows 300 calls/day/IP without a key and GitHub 60/hour, and those
|
|
# budgets are shared by every user of the assistant at once.
|
|
# -------------------------------------------------------------------
|
|
valkey:
|
|
<<: *hardened
|
|
image: valkey/valkey:9-alpine
|
|
container_name: owuitools-valkey
|
|
read_only: true
|
|
# The image's entrypoint chowns /data when it starts as root, which
|
|
# cap_drop: ALL makes impossible. Starting as valkey's own uid skips
|
|
# that branch; the tmpfs is created owned by it instead.
|
|
user: "999:1000"
|
|
tmpfs:
|
|
- /data:size=128m,uid=999,gid=1000,mode=0700
|
|
volumes:
|
|
- ./logs:/logs
|
|
# `umask 022` so the log lands world-readable: valkey writes it 0600
|
|
# as its own uid, which the operator cannot read. `exec` keeps valkey
|
|
# as PID 1, so signals and a clean shutdown are unaffected - unlike
|
|
# the pipeline searxng needs.
|
|
entrypoint: ["/bin/sh", "-c", "umask 022; exec \"$$@\"", "--"]
|
|
command: >
|
|
valkey-server
|
|
--logfile /logs/valkey.log
|
|
--save ""
|
|
--appendonly no
|
|
--maxmemory 192mb
|
|
--maxmemory-policy allkeys-lru
|
|
networks:
|
|
core:
|
|
ipv4_address: 172.22.0.14
|
|
mem_limit: 256m
|
|
pids_limit: 64
|
|
healthcheck:
|
|
test: ["CMD", "valkey-cli", "ping"]
|
|
interval: 15s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 5s
|
|
labels:
|
|
wud.watch: "true"
|
|
wud.tag.include: '^\d+-alpine$$'
|
|
|
|
# -------------------------------------------------------------------
|
|
# Code execution sandbox. The only container that runs code written by
|
|
# whoever is talking to the model, and therefore the most locked down:
|
|
#
|
|
# network_mode "none" - no network stack at all, not even loopback
|
|
# to the rest of the suite. It cannot be
|
|
# reached over HTTP and cannot call out, so an
|
|
# SSRF or exfiltration bug has nothing to use.
|
|
# read_only + tmpfs - nothing it writes survives the run.
|
|
# user 10001 - same uid as the api, purely so the job tmpfs
|
|
# can be mode 0700 rather than world-writable.
|
|
#
|
|
# Work reaches it as files on octave_jobs, a 64 MB tmpfs shared with the
|
|
# api. See octave/run.sh for the protocol and for the one limitation
|
|
# this design keeps: a hostile script can delete another in-flight job.
|
|
# -------------------------------------------------------------------
|
|
octave:
|
|
<<: *hardened
|
|
build:
|
|
context: ./octave
|
|
image: local/owuitools-octave:latest
|
|
container_name: owuitools-octave
|
|
read_only: true
|
|
network_mode: "none"
|
|
user: "10001:10001"
|
|
tmpfs:
|
|
- /tmp:size=256m,mode=1777
|
|
volumes:
|
|
- octave_jobs:/jobs
|
|
- ./logs:/logs
|
|
environment:
|
|
HOME: /tmp/octhome
|
|
# Without a UTF-8 locale gnuplot writes "iconv failed to convert degree
|
|
# sign" to stderr on every plot that has a degree in a label, which the
|
|
# model then reads alongside its real errors.
|
|
LANG: C.UTF-8
|
|
LC_ALL: C.UTF-8
|
|
OWUITOOLS_OCTAVE_TIMEOUT: ${OWUITOOLS_OCTAVE_TIMEOUT:-25}
|
|
OWUITOOLS_OCTAVE_MAX_TIMEOUT: ${OWUITOOLS_OCTAVE_MAX_TIMEOUT:-60}
|
|
mem_limit: 1g
|
|
cpus: 1.5
|
|
pids_limit: 128
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "test -n \"$$(find /jobs/.heartbeat -newermt '-90 seconds' 2>/dev/null)\""]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
# -------------------------------------------------------------------
|
|
# The VM sub-agent. Three containers, and the split is the security
|
|
# argument rather than tidiness:
|
|
#
|
|
# vmgw holds BOTH secrets - an Open WebUI admin key and the only
|
|
# permitted Ollama path - and exposes three URLs. It is the
|
|
# one that can reach Open WebUI and Ollama.
|
|
# vmguard owns a network namespace with a default-deny filter whose
|
|
# entire allowlist is vmgw.
|
|
# vmagent runs the agent loop inside that namespace, holding no
|
|
# credential and able to reach exactly one address.
|
|
#
|
|
# Ollama has NO authentication and serves DELETE /api/delete. That is
|
|
# why it is behind vmgw's `location =` rather than on any container's
|
|
# allowlist: a URL-validation bug anywhere else must not be one request
|
|
# away from deleting the model the whole assistant runs on.
|
|
# -------------------------------------------------------------------
|
|
vmgw:
|
|
<<: *hardened
|
|
image: nginx:stable-alpine
|
|
container_name: owuitools-vmgw
|
|
read_only: true
|
|
cap_drop: [ALL]
|
|
cap_add: [CHOWN, SETGID, SETUID]
|
|
volumes:
|
|
- ./configs/services/vmgw.nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
|
|
- ./logs:/logs
|
|
environment:
|
|
# No credential of any kind. Live progress used to need an Open
|
|
# WebUI admin key here; it is now collected by a filter running
|
|
# inside Open WebUI, so there is nothing to hold.
|
|
OWUITOOLS_OLLAMA_HOST: ${OWUITOOLS_OLLAMA_HOST:-172.17.0.1:11434}
|
|
NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
|
|
tmpfs:
|
|
- /var/cache/nginx:size=16m
|
|
- /var/run:size=1m
|
|
- /tmp:size=16m
|
|
- /etc/nginx/conf.d:size=1m
|
|
networks:
|
|
vmcore:
|
|
ipv4_address: 172.22.2.10
|
|
# `vmcore` is internal:true, so on its own this container has no
|
|
# default route and cannot reach Ollama on the docker bridge at
|
|
# all - the healthcheck passes and every proxied request times out.
|
|
# This membership exists purely for that route. It is no longer
|
|
# used to reach Open WebUI: that location is gone, and nginx
|
|
# proxies only what its config names.
|
|
openwebui: {}
|
|
mem_limit: 64m
|
|
pids_limit: 64
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/health >/dev/null || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
vmguard:
|
|
<<: *hardened
|
|
image: nicolaka/netshoot:v0.16
|
|
container_name: owuitools-vmguard
|
|
cap_drop: [ALL]
|
|
cap_add: [NET_ADMIN]
|
|
volumes:
|
|
- ./vmguard/firewall.sh:/firewall.sh:ro
|
|
- ./logs:/logs
|
|
# vmagent shares this namespace. Recreating this service without
|
|
# recreating vmagent leaves the agent with no route to vmgw, and so
|
|
# to Ollama - see the note on netguard above.
|
|
#
|
|
# docker compose up -d --force-recreate vmguard vmagent
|
|
environment:
|
|
# The one address the agent container may reach. Must equal
|
|
# OWUITOOLS_VMGW_URL on the vmagent service below.
|
|
OWUITOOLS_VMGW_IP: 172.22.2.10
|
|
OWUITOOLS_VMGW_PORT: "8000"
|
|
entrypoint: ["/bin/sh", "/firewall.sh"]
|
|
networks:
|
|
vmcore:
|
|
ipv4_address: 172.22.2.11
|
|
mem_limit: 64m
|
|
pids_limit: 32
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "test -f /tmp/firewall-ready"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 5s
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
vmagent:
|
|
<<: *hardened
|
|
build:
|
|
context: .
|
|
dockerfile: vmagent/Dockerfile
|
|
image: local/owuitools-vmagent:latest
|
|
container_name: owuitools-vmagent
|
|
read_only: true
|
|
user: "10001:10001"
|
|
network_mode: "container:owuitools-vmguard"
|
|
volumes:
|
|
- vm_jobs:/vmjobs
|
|
# The ONLY host bind mount in this suite, and the reason for it is
|
|
# the guarantee itself: everything privileged - /dev/kvm, the
|
|
# frozen base images, the ZFS root, the Gitea token, the ability to
|
|
# unfreeze a disk - is on the far side of this socket, reachable
|
|
# only through vmd's fixed verb list. The DIRECTORY is mounted, not
|
|
# the socket: Docker creates a directory in place of a bind source
|
|
# that does not exist yet, and vmd could then never bind at all.
|
|
- /run/owuivm/agent:/run/owuivm/agent
|
|
# The agent's prompts and its tool definitions. Mounted rather than
|
|
# baked into the image, so rewording a tool description or a prompt
|
|
# fragment is a container restart instead of a rebuild. Read-only,
|
|
# and it is the whole of what this container is configured by -
|
|
# note there is still no secret anywhere in here, which is the
|
|
# property the two boundaries above exist to protect.
|
|
- ./configs/agent:/configs/agent:ro
|
|
# Shared with the api container. The sub-agent's research verbs are
|
|
# calls BACK into the tool server, so a tool switched off there must
|
|
# not be offered here - it would 404 and read as a broken tool.
|
|
- ./configs/tools.toml:/configs/tools.toml:ro
|
|
# The machine registry. The agent reads the shell, project path and
|
|
# path example for ITS machine out of this - the substitutions that
|
|
# turn one set of tool descriptions into two.
|
|
- ./configs/vms.toml:/configs/vms.toml:ro
|
|
- ./logs:/logs
|
|
tmpfs:
|
|
- /tmp:size=64m
|
|
- /home/agent:size=16m
|
|
# The model settings and session bounds live with the agent's prompts
|
|
# and tool definitions, in configs/agent/params.env, rather than being
|
|
# spread between here and the stack's .env. env_file is the right
|
|
# mechanism: this container reads them from its environment, and
|
|
# unlike ${...} substitution it needs nothing in the compose file to
|
|
# be kept in step with the config.
|
|
env_file:
|
|
- ./configs/agent/params.env
|
|
environment:
|
|
HOME: /home/agent
|
|
OWUITOOLS_VM_JOBS_DIR: /vmjobs
|
|
OWUITOOLS_AGENT_CONFIG_DIR: /configs/agent
|
|
OWUITOOLS_TOOLS_CONFIG: /configs/tools.toml
|
|
OWUITOOLS_VMGW_URL: http://172.22.2.10:8000
|
|
OWUITOOLS_LOG_LEVEL: ${OWUITOOLS_LOG_LEVEL:-INFO}
|
|
depends_on:
|
|
vmguard:
|
|
condition: service_healthy
|
|
vmgw:
|
|
condition: service_healthy
|
|
mem_limit: 512m
|
|
pids_limit: 128
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "test -n \"$$(find /vmjobs/.heartbeat -newermt '-120 seconds' 2>/dev/null)\""]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 20s
|
|
labels:
|
|
wud.watch: "false"
|
|
|
|
networks:
|
|
# No route off its own subnet. Necessary but, on its own, not sufficient
|
|
# - the host sits on this subnet at .1. netguard/firewall.sh closes that.
|
|
core:
|
|
name: owuitools_core
|
|
internal: true
|
|
ipam:
|
|
config:
|
|
- subnet: 172.22.0.0/24
|
|
|
|
wan:
|
|
name: owuitools_wan
|
|
ipam:
|
|
config:
|
|
- subnet: 172.22.1.0/24
|
|
|
|
# The VM agent's own namespace. Separate from `core` on purpose: the
|
|
# agent's allowlist is vmgw alone, while the api's is squid and valkey,
|
|
# and collapsing them would give one the other's reach.
|
|
vmcore:
|
|
name: owuitools_vmcore
|
|
internal: true
|
|
ipam:
|
|
config:
|
|
- subnet: 172.22.2.0/24
|
|
|
|
# Pre-existing, owned by the openwebui stack. Joining is additive and
|
|
# does not restart or reconfigure open-webui.
|
|
#
|
|
# `openwebui_default` is what Docker Compose names the default network
|
|
# of a stack in a directory called `openwebui`. Yours will differ if
|
|
# the directory or the project name does - check with
|
|
# `docker network ls` and set OWUITOOLS_OWUI_NETWORK in
|
|
# configs/owuitools.env. If Open WebUI is not on this host at all,
|
|
# publish the gateway's port instead and point the tool connection at
|
|
# the host address.
|
|
openwebui:
|
|
name: ${OWUITOOLS_OWUI_NETWORK:-openwebui_default}
|
|
external: true
|
|
|
|
volumes:
|
|
# Deliberately tmpfs, not a disk volume: job files are transient, they
|
|
# contain code somebody typed into a chat window, and none of it should
|
|
# outlive a reboot or land on the operator's disk. 64 MB is a cap as much as an
|
|
# allocation - it bounds what a runaway script can write.
|
|
octave_jobs:
|
|
driver: local
|
|
driver_opts:
|
|
type: tmpfs
|
|
device: tmpfs
|
|
o: size=64m,uid=10001,gid=10001,mode=0700
|
|
|
|
# Same reasoning as octave_jobs, same shape. A job is a sentence of
|
|
# instructions and a result is a page of prose; neither should outlive
|
|
# a reboot or land on the operator's disk.
|
|
vm_jobs:
|
|
driver: local
|
|
driver_opts:
|
|
type: tmpfs
|
|
device: tmpfs
|
|
o: size=16m,uid=10001,gid=10001,mode=0700
|