How Odoo Servers Are Built¶
TL;DR
Your Odizy Odoo server is assembled by ikb from a single recipe file: .muppy/buildit.jsonc. To add an addon (your own, or one from a git repo), tune Odoo, or pull a Python package, you edit that file, then run ikb install in a terminal. Commit the file when you are happy — that is what makes your build reproducible.
What builds your server: buildit / ikb¶
An Odizy Odoo server is not a hand-installed Odoo. It is assembled from a declarative recipe by a tool called Inouk Buildit, whose command-line is ikb.
ikb takes care, in one command, of everything that a working Odoo needs:
- cloning Odoo itself at the right version,
- cloning your addons from any number of git repositories,
- installing all the Python dependencies,
- generating the Odoo configuration file and a ready-to-use launcher.
It is heavily inspired by zc.buildout, reworked around a single JSON recipe. The engine lives at gitlab.com/inouk/buildit and its Odoo plugin at gitlab.com/inouk/ikb.
Your interface is the file
On Odizy you do not have a buildit GUI. Your single control surface for the build is buildit.jsonc. Everything on this page is something you express by editing that file and re-running ikb install.
The build loop¶
Everything happens from a terminal on your App Server — either the Code Server web IDE (a bash terminal in your browser) or an SSH session.
# from the repository root of your App Server
ikb install # (re)build: clone/update Odoo + addons, install deps, generate config + launcher
ikb install is idempotent: run it as often as you like. On an existing build it updates the Odoo and addon clones (git fetch / checkout / pull) and reinstalls dependencies.
To start Odoo after a build, use the generated launcher (it accepts all standard Odoo arguments and options):
odoo-ikb # start Odoo
odoo-ikb -u my_addon # upgrade a module, etc.
Resetting¶
ikb reset # remove the generated config (etc/) and the launcher
ikb reset does not delete everything — on purpose
ikb reset only removes the generated parts (the etc/ config and the launcher). It deliberately keeps the cloned Odoo (parts/odoo19) and your cloned addons, because re-cloning them on every reset would be far too slow.
If you really want a clean-slate rebuild (a fresh Odoo or a fresh addon clone), delete the corresponding folder by hand and run ikb install again:
rm -rf parts/odoo19 # then: ikb install (re-clones Odoo)
Reproducibility¶
When your build is the way you want it, commit buildit.jsonc. The committed recipe is the single source of truth: any future build — on this server, on a teammate's server, or in the Kubernetes / CI-CD pipeline — reproduces exactly the same Odoo from it.
Where is buildit.jsonc?¶
On your Odizy server the recipe is at .muppy/buildit.jsonc, and the environment variable IKB_BUILDIT_JSONC_PATH points ikb to it:
IKB_BUILDIT_JSONC_PATH=/opt/<your-app>/.muppy/buildit.jsonc
Default location
If IKB_BUILDIT_JSONC_PATH is not set, ikb looks, from the directory it is launched in, for (in order): buildit.json, buildit.jsonc, .ikb/buildit.json, .ikb/buildit.jsonc. On Odizy the variable is set for you, so the recipe is always .muppy/buildit.jsonc.
The file is JSONC — JSON with // comments allowed.
Anatomy of the recipe¶
buildit.jsonc describes a set of parts, each driven by a plugin. The one you care about is the odoo part. Here is a trimmed, representative version of a delivered Odoo 19 recipe:
{
"buildit": {
// ── infrastructure parts: delivered, leave them alone (see "Delivered — don't touch") ──
"buildit-plugins": { "plugin": "inouk.buildit.git_checkout", "repositories": { /* … */ } },
"ENV": { "plugin": "ikb.environ" },
"py3x": { "plugin": "ikb.virtualenv", "name": "py3x", "interpreter": "python3" },
// ── the Odoo part: this is what you tune ──
"odoo": {
"plugin": "ikb.odoo",
"python": "${py3x}",
"major_version": "19",
"version": {
"type": "git",
"repository": "https://github.com/odoo/odoo.git",
"directory": "parts/odoo19",
"branch": "19.0", "refspec": "19.0", "depth": 5
},
"requirements": {
"requirements_file": "user_addons/requirements.txt",
"options": { "no-deps": false }
},
"addons": {
"project_addons": {
"install": true,
"type": "local",
"root_folder": "repository",
"path": "./user_addons"
}
// … more addons here …
},
"config": {
"options": {
"http_port": 8069,
"workers": 1,
"proxy_mode": true
// … more Odoo options here …
}
}
},
"running-env-root-path": "$MPY_APP_ABS_PATH"
}
}
The three things you will edit are inside the odoo part: addons, requirements, and config. Everything else is delivered and should be left untouched.
Adding addons¶
Addons are declared under buildit.odoo.addons, keyed by addon name. Each entry has:
install— a boolean. Set it tofalseto keep the declaration but skip the addon. (true/falseis all you normally need.)server_wide— optional boolean. Whentrueandinstallistrue, the addon is added to Odoo'sserver_wide_modules(loaded for the whole server, e.g. a session store).type—"local"or"git"(an advanced"http_file"type also exists).
Pre-wired Inouk addons
The delivered recipe already lists a set of inouk_* addons (session store, message queue, …). Some are shipped install: true, others install: false — flip the boolean to enable or disable one. You do not need to add them yourself.
Your own addons — type: local¶
Your project code lives in the user_addons/ folder of your repository. The delivered recipe already maps it:
"project_addons": {
"install": true,
"type": "local",
"root_folder": "repository", // path is relative to the repo root
"path": "./user_addons"
}
A starter example_addon/ ships inside user_addons/. To add your own module, just create or copy it into user_addons/ (next to example_addon) and run ikb install. No recipe change is needed — the whole folder is already on Odoo's addons path.
Addons from a git repo — type: git¶
To pull an addon (or a whole collection) from a repository:
"my_addon": {
"install": true,
"type": "git",
"repository": "https://github.com/me/my_addon.git",
"directory": "my_addon",
"group": "parts/my_addons", // parent folder; this folder becomes the addons-path entry
"refspec": "19.0" // required: branch, tag or commit to checkout
}
Optional keys: branch + depth for a faster shallow clone.
The group vs directory distinction decides what lands on Odoo's addons path:
- A repo that is a single addon (the module sits at the repo root): set
groupto a shared parent folder anddirectoryto the addon's name. The repo is cloned into{group}/{directory}, and{group}is added to the addons path. Several such repos sharing the samegroupproduce one addons-path entry. - A "collection" repo containing many addons (e.g. an OCA repository): omit
groupand pointdirectoryat the clone folder. The clone folder itself becomes the addons-path entry, and Odoo discovers every module inside it.
// OCA collection: the whole parts/oca_web folder is added to the addons path
"oca_web": {
"install": true,
"type": "git",
"repository": "https://github.com/OCA/web.git",
"directory": "parts/oca_web",
"refspec": "19.0"
}
Private repositories¶
To clone a private repo, inject a token into the URL with an environment-variable reference, ${ENV:VAR_NAME}. The delivered enterprise addon shows the pattern — the token is in the URL and the same variable gates whether the addon installs at all:
"enterprise": {
"install": "${ENV:MPY_GITHUB_ODOO_TOKEN_URL_AUTH}",
"type": "git",
"repository": "https://${ENV:MPY_GITHUB_ODOO_TOKEN_URL_AUTH}github.com/odoo/enterprise.git",
"directory": "parts/enterprise",
"refspec": "19.0"
}
Here MPY_GITHUB_ODOO_TOKEN_URL_AUTH carries the URL auth prefix (the user:token@ part). Environment variables come from your server's environment (/etc/muppy.env, managed on the Manganese side) and are exposed to the recipe through the ENV part.
install can be conditional
Because install is evaluated, a value like "${ENV:SOME_TOKEN}" means "install only when that variable is set" — empty/unset resolves to false. Handy for optional, credential-gated addons.
Python dependencies¶
Your project's pip packages go in user_addons/requirements.txt — a standard requirements.txt. The recipe already points at it:
"requirements": {
"requirements_file": "user_addons/requirements.txt",
"options": { "no-deps": false }
}
Add your packages to that file and run ikb install. Odoo's own requirements.txt is always installed as well. Set options.no-deps to true to pass --no-deps to pip.
Odoo parameters¶
Anything you would normally put in odoo.conf goes under buildit.odoo.config.options. These override the generated etc/odoo.buildit.cfg (the [options] section). Values support ${ENV:VAR} interpolation.
"config": {
"options": {
"admin_passwd": "${ENV:IKB_ODOO_ADMIN_PASSWORD}",
"http_port": 8069,
"gevent_port": 8072,
"workers": 1,
"proxy_mode": true,
"limit_time_cpu": 360,
"limit_time_real": 720,
"db_maxconn": 256,
"list_db": false
}
}
You can also add extra addons-path folders explicitly with buildit.odoo.addons_path (a list of paths), appended after the cloned addons.
Advanced¶
Recipe inheritance and overloads¶
A buildit.jsonc can extend a base file with __$extends, then layer changes on top. When merging lists, three commands (used as a suffix on the key) give you fine control:
key__$let— replace the list outright.key__$merge— merge list elements (dicts are matched by theirnamekey).key__$delete— remove elements; the value"__$delete"removes a key entirely.
Addon archives — type: http_file¶
Pull an addon pack from a URL (.zip only). The target folder is wiped on every install:
"theme_pack": {
"install": true,
"type": "http_file",
"uri": "https://example.com/addon_pack.zip",
"directory": "parts/downloaded_addons"
}
The launcher¶
launcher_name (here odoo-ikb) and install_into (the /usr/local/bin/ symlink) control the generated launcher. They are delivered with sensible values — change them only if you know why.
requirements.eggs — avoid¶
The recipe also supports an eggs dict under requirements for pip packages. Prefer requirements.txt (above): eggs is being phased out in favour of it.
Delivered — don't touch¶
These keys are set when your server is delivered and configured to work together. Changing them will likely break the build:
buildit.odoo.version(the Odoo repo / branch) andmajor_versionbuildit.odoo.python(${py3x}) andrunning-env-root-path($MPY_APP_ABS_PATH)- the
ENVandpy3xparts buildit-plugins(theinouk.buildit.git_checkoutplugin that fetches theikbplugins)do_not_generate_config,requirements.do_not_install
Appendix: installing buildit locally (advanced)¶
You normally never need this
Manganese always delivers a Dev Server with ikb already installed (via uv tool). On Odizy you have nothing to do. This appendix only applies if you want to run buildit on your own machine — for example to build or test a repository outside Odizy.
ikb is the inouk.buildit Python package; installing it puts the ikb command on your PATH. Use an isolated tool installer — uv (recommended, the same method Odizy uses) or pipx:
# with uv (https://docs.astral.sh/uv/)
uv tool install git+https://gitlab.com/inouk/buildit.git
# or with pipx
pipx install git+https://gitlab.com/inouk/buildit.git
Check it:
ikb --help
Requirements
Python 3.8+. buildit pulls its own dependencies (invoke, pyyaml, jsonc-parser). For SSH-based private repos, use git+ssh://git@gitlab.com/inouk/buildit.git instead.