Sign inSign up

terrakernel/odxproxy

By terrakernel

•Updated 8 days ago

A lightweight, high-performance reverse proxy for Odoo APIs.

Image
Networking
Integration & delivery
API management
0

3.1K

terrakernel/odxproxy repository overview

Version Language Base Image Run As Architecture

ODXProxy is a lightweight, high-performance reverse proxy for Odoo APIs, built in Rust and designed for enterprise environments. It provides API key authentication, rate limiting, request validation, and controlled access in front of Odoo—without modifying the Odoo application itself. Rust-based, minimal attack surface Static binary on distroless/static, non-root container: no libc or OpenSSL in the image Configuration-driven, predictable behavior Optimized for performance and low resource usage Ideal for teams that want a fast, secure, and auditable gateway for Odoo API traffic Visit ODXPROXY⁠ for trial and licensing

config.conf

# Address and Port for the proxy server to listen on
LISTEN_ADDR=0.0.0.0:6600
# The API key required from your api gateway
PROXY_API_KEY=YOUR_PROXY_KEY # Change this to your actual secret key
# Default log level
RUST_LOG=info,TheODXProxy=info # Info default, debug for our app
# Default request timeout to Odoo in seconds if x-request-timeout header is missing/invalid
DEFAULT_ODOO_TIMEOUT_SECS=15
LOG_PATH=/var/log/odxproxy
HOSTNAME=YOUR_ODX_HOSTNAME_FOR_HEARTBEAT
REDIS_URL=YOUR_REDIS_URL_IF_USING_HEARTBEAT
HEARTBEAT=false
LICENSE_KEY=/usr/share/odxproxy/config/license.json

docker-compose.yaml

services:
  odxproxy:
    image: terrakernel/odxproxy
    ports:
      - "6600:6600"

    volumes:
      - /host/config/file/path:/usr/share/odxproxy/config:ro
      - /host/log/path:/var/log/odxproxy

    restart: unless-stopped

for library integration you can visit github repos⁠ or join our discord⁠ channel


⁠ODXBrain and odxdocs (optional, from 0.10.0)

ODXProxy can also answer two questions an AI agent asks before it calls Odoo, by forwarding them to ODXBrain⁠ on your own network:

  • Which Odoo model holds this data? A question in, three ranked model names out.
  • How do we do this here? A task in, the matching SOP ids out, from the procedures your team approved in odxdocs⁠.

Your clients keep the one ODXProxy key they already use. ODXProxy holds a separate key for ODXBrain, and never forwards Odoo credentials there. Without ODXBrain, ODXProxy works exactly as before.

client --PROXY_API_KEY--> ODXProxy --ODXBRAIN_PROXY_KEY--> ODXBrain <--ODXBRAIN_DOCS_KEY-- odxdocs publish
agent  --ODXDOCS_API_KEY--> odxdocs serve     (the SOP text, fetched by id and version)
⁠Configure

Three environment variables on the ODXProxy container. Leave ODXBRAIN_URL unset and the brain routes answer 501.

VariableMeaning
ODXBRAIN_URLODXBrain's address, e.g. http://odxbrain:8080. Unset means ODXBrain isn't installed
ODXBRAIN_API_KEYThe one key ODXProxy sends to ODXBrain. Required unless the URL is loopback. It can't contain :
ODXBRAIN_TIMEOUT_SECSDefault 5. A client's x-request-timeout header overrides it per call

ODXProxy refuses to start if ODXBRAIN_URL is set but can't work: not http/https, no host, or a non-loopback host with no key.

One name, two meanings. On ODXBrain, ODXBRAIN_API_KEY is the colon-separated list of keys it accepts. On ODXProxy it's the single key it sends. Give ODXProxy and odxdocs a key each, and list both on ODXBrain, as the compose file below does. Then either one can be rotated without touching the other.

⁠Routes
RouteForwards toWhat it does
POST /brain/v1/resolve/v1/resolveA question in, ranked Odoo model names out
POST /brain/v1/sops/resolve/v1/sops/resolveA task in, ranked SOP ids and versions out
GET /brain/v1/info/infoThe model, corpus and SOP generation ODXBrain is serving

They take the same x-api-key as every other ODXProxy route. The body is ODXBrain's request plus an id, which is required and echoed back. The answer is ODXBrain's response, unchanged, in ODXProxy's usual JSON-RPC envelope. An odoo_instance block in the body is dropped, so an Odoo API key never reaches ODXBrain.

curl -s http://127.0.0.1:6600/brain/v1/resolve \
  -H "x-api-key: $PROXY_API_KEY" -H 'Content-Type: application/json' \
  -d '{"id":"q-1","query":"which invoice is not paid yet","odoo_version":"18.0","edition":"community"}'
{"jsonrpc":"2.0","id":"q-1",
 "result":{"candidates":[{"model":"account.move","score":0.513},
                         {"model":"account.payment","score":0.492},
                         {"model":"account.payment.term","score":0.462}],
           "corpus_version":"2026.09.4","dim":1024,"model":"bge-m3","usage":{"prompt_tokens":10}}}

The ODXBrain page⁠ documents the request fields and the response.

There is no route to ODXBrain's /v1/sops/reload, so no client key can change what ODXBrain serves; only odxdocs publish can. ODXProxy has no route to odxdocs either. Agents fetch the SOP text from odxdocs serve directly, with their own key.

⁠Errors

ODXBrain's own errors (a bad request, no pack loaded) come back with ODXBrain's HTTP status as the code. Failures of the hop itself have their own codes:

HTTPcodeMeaning
501-32008ODXBRAIN_URL is unset on this proxy
502-32009ODXBrain is unreachable, or didn't answer with its JSON
504-32010The call took longer than the timeout
502-32011ODXBrain refused ODXProxy's key: check ODXBRAIN_API_KEY on both containers

GET /_/about reports "features":{"odxbrain":true} when ODXBrain is configured. On /_/metrics, the gauge odxbrain_ready is 1 while ODXBrain's /ready answers. ODXProxy checks it every 10 seconds. Brain calls are counted under api="brain".

⁠Docker Compose: ODXProxy, ODXBrain and odxdocs

All three on one private network. Clients reach ODXProxy on port 6600. ODXBrain's port is published on loopback only, for the odxdocs GUI. The files ODXBrain needs, the model and the corpus pack, are described on the ODXBrain page⁠. The SOP folders are described on the odxdocs page⁠.

odx/
├── compose.yaml
├── .env
├── odxproxy/     config.conf, license.json   (readable by UID 65532)
├── models/       bge-m3-f16.gguf
├── pack/         the corpus pack
├── sop-files/    your SOPs, in git
├── sops/         snapshot.json, written by odxdocs
└── store/        every published SOP version, written by odxdocs

.env (chmod 600). Make each key with openssl rand -hex 32:

ODXBRAIN_PROXY_KEY=...   # ODXProxy -> ODXBrain
ODXBRAIN_DOCS_KEY=...    # odxdocs publish/gui -> ODXBrain
ODXDOCS_API_KEY=...      # agents -> odxdocs serve
ODX_UID=1000             # `id -u` of the user who owns the folders
ODX_GID=1000             # `id -g`

odxproxy/config.conf. The container's root filesystem is read-only, so logs go to a tmpfs:

LISTEN_ADDR=0.0.0.0:6600
PROXY_API_KEY=YOUR_PROXY_KEY
RUST_LOG=info
DEFAULT_ODOO_TIMEOUT_SECS=15
LOG_PATH=/tmp
HEARTBEAT=false
LICENSE_KEY=/usr/share/odxproxy/config/license.json

compose.yaml:

x-hardened: &hardened
  read_only: true
  cap_drop: [ALL]
  security_opt: ["no-new-privileges:true"]

services:
  odxproxy:
    <<: *hardened
    image: terrakernel/odxproxy:0.10.1
    environment:
      ODXBRAIN_URL: http://odxbrain:8080
      ODXBRAIN_API_KEY: ${ODXBRAIN_PROXY_KEY:?set ODXBRAIN_PROXY_KEY in .env}
    volumes:
      - ./odxproxy:/usr/share/odxproxy/config:ro    # config.conf + license.json
    tmpfs: ["/tmp"]                                  # LOG_PATH=/tmp in config.conf
    ports:
      - "127.0.0.1:6600:6600"     # drop "127.0.0.1:" to serve other machines
    depends_on: [odxbrain]
    restart: unless-stopped
    networks: [odx]

  odxbrain:
    <<: *hardened
    image: terrakernel/odxbrain:0.1.0
    command: ["--model", "/models/bge-m3-f16.gguf", "--model-id", "bge-m3",
              "--pack", "/pack", "--sops", "/sops/snapshot.json",
              "--host", "0.0.0.0", "--port", "8080"]
    environment:
      # ODXBrain accepts both keys: odxdocs' and ODXProxy's
      ODXBRAIN_API_KEY: "${ODXBRAIN_DOCS_KEY:?set ODXBRAIN_DOCS_KEY in .env}:${ODXBRAIN_PROXY_KEY:?}"
    volumes:
      - ./models:/models:ro
      - ./pack:/pack:ro
      - ./sops:/sops:ro
    cpus: 2
    expose: ["8080"]
    ports:
      - "127.0.0.1:8080:8080"     # only for the odxdocs GUI, which runs on host networking
    restart: unless-stopped
    networks: [odx]

  odxdocs:
    <<: *hardened
    image: terrakernel/odxdocs:0.1.0
    environment:
      ODXDOCS_API_KEY: ${ODXDOCS_API_KEY:?set ODXDOCS_API_KEY in .env}
    volumes:
      - ./store:/store:ro
    expose: ["8090"]
    restart: unless-stopped
    networks: [odx]

  publish:
    <<: *hardened
    image: terrakernel/odxdocs:0.1.0
    profiles: [tools]
    user: "${ODX_UID:?}:${ODX_GID:?}"
    command: ["publish", "/sop-files", "--out", "/sops/snapshot.json", "--store", "/store",
              "--brain", "http://odxbrain:8080", "--label", "${ODX_SOP_LABEL:-}"]
    environment:
      ODXDOCS_BRAIN_KEY: ${ODXBRAIN_DOCS_KEY:?}
    volumes:
      - ./sop-files:/sop-files:ro
      - ./sops:/sops
      - ./store:/store
    depends_on: [odxbrain]
    networks: [odx]

  gui:
    image: terrakernel/odxdocs:0.1.0
    profiles: [tools]
    network_mode: host
    user: "${ODX_UID:?}:${ODX_GID:?}"
    command: ["gui", "/sop-files", "--out", "/sops/snapshot.json", "--store", "/store",
              "--brain", "http://127.0.0.1:8080"]
    environment:
      ODXDOCS_BRAIN_KEY: ${ODXBRAIN_DOCS_KEY:?}
    volumes:
      - ./sop-files:/sop-files
      - ./sops:/sops
      - ./store:/store

networks:
  odx: {}
# first time only: write the first SOP snapshot, which ODXBrain needs to start
docker compose run --rm --no-deps publish build /sop-files --out /sops/snapshot.json --store /store

docker compose up -d                  # ODXProxy + ODXBrain + odxdocs serve
docker compose run --rm publish       # after an SOP change: check, publish, reload ODXBrain
docker compose run --rm gui           # SOP editor on http://127.0.0.1:8091/

Not using SOPs? Drop the odxdocs, publish and gui services, ODXBrain's --sops flag and its /sops mount, and set ODXBrain's ODXBRAIN_API_KEY to ${ODXBRAIN_PROXY_KEY:?} alone.

Check it:

curl -s http://127.0.0.1:6600/_/about                                   # "features":{"odxbrain":true}
curl -s http://127.0.0.1:6600/brain/v1/info -H "x-api-key: $PROXY_API_KEY"

Network. ODXBrain and odxdocs make no outbound connections. ODXProxy does: it checks its license against time.odxproxy.io, so its container needs a route to the internet.

⁠Known Issues:

  • None. From 0.10.1 the image is a static binary on distroless/static-debian13, with no glibc or OpenSSL, and Docker Scout reports 0 vulnerabilities (earlier glibc images had 8 Low from the Debian base).

Tag summary

Content type

Image

Digest

sha256:1237e0b5b…

Size

5.3 MB

Last updated

8 days ago

docker pull terrakernel/odxproxy