Set up and work in an Odoo 18 development environment with odoo-nix
Repository layout, daily commands, adding OCA modules, running a dev server with a mail catcher, and upgrading modules in an odoo-nix managed Odoo 18 project.
On this page
This guide is for a developer joining an Odoo 18 Community project that uses OCA modules and is managed with odoo-nix. It covers how the repository is laid out, which commands you run every day, how to add OCA modules, how to run the dev server, and how to apply code changes to a database.
Production hosting is a separate topic and is not covered here. For how the module set is chosen, read Odoo Community and OCA Overview first.
What you get
odoo-nix turns the project into one reproducible shell. You need Nix with flakes enabled and direnv. Everything else comes from the flake:
PostgreSQL 16, started for you by devenv.
The Odoo server, running from source with
--dev=all.Mailpit, a local mail catcher.
A pinned Python environment (Python 3.11 for Odoo 18) built from
pyproject.tomlanduv.lock.One
odoocommand for database, module and project tasks. Anything it does not know is passed straight to the realodoo-bin.
Repository layout
| Path | What it is |
|---|---|
odoo/ | The OCB 18.0 source, a git submodule. |
modules/ | OCA repositories, one git submodule per repository. |
custom/ | The project's own modules. |
modules.txt | The install set: one module name per line. |
pyproject.toml, uv.lock | The Python environment, resolved by uv. |
flake.nix | A thin wrapper that sets project name, series, database name and so on. |
odoo.conf | A symlink into the Nix store. Generated, never edited. |
.venv, odools.toml | Symlinks for editors. Generated. |
.devenv/state/ | The development PostgreSQL cluster and the Odoo filestore. Not tracked. |
Three of these deserve a closer look.
modules.txt
odoo db provision installs base plus every module listed in modules.txt. The file is also what the Python environment is built from: each listed module becomes an install root in pyproject.toml, and uv resolves the dependency graph from there. A module listed here must exist on disk, either under modules/ or under custom/.
odoo module add keeps the file sorted and free of duplicates. Many projects keep it to OCA modules only and install their own modules from custom/ explicitly, but nothing in odoo-nix forces that convention. Follow whatever your project already does.
pyproject.toml
Three blocks in pyproject.toml are managed by odoo-nix and sit between marker comments that start with # >>> odoo-nix:: the install set, the module sources, and the build dependencies for packages that only ship as source. Do not edit anything between the markers. odoo-nix regenerates them whenever you run odoo module add, odoo module add-bundle or odoo project update.
The [dependency-groups].dev block is yours. It already carries watchdog (for code reload), freezegun and websocket-client (both needed to run Odoo tests). Leave those in. Without freezegun no Odoo test can run at all.
odoo.conf
The file is rendered from your flake settings and the folders on disk. addons_path is derived from what exists, in this order: the two core OCB roots, each OCA repository under modules/ that contains a module, then custom/. You never maintain that list by hand.
To change a setting such as the database name or the HTTP port, change the odooConf options in flake.nix, run direnv reload, and restart devenv up.
Start working
On a fresh clone, take the submodules along and let direnv build the shell:
git clone --recurse-submodules <REPO_URL> <PROJECT_DIR>
cd <PROJECT_DIR>
direnv allowIf you do not use direnv, nix develop --no-pure-eval gives you the same shell. The first entry also checks out any submodule you have never had, so a clone made without --recurse-submodules still works.
Run the dev server
Start PostgreSQL, Odoo and Mailpit together:
devenv upIn a second terminal, in the same project directory, create the database and install the install set:
odoo db provisionWith no argument, the command uses the database name configured in odoo.conf. It is safe to run again: on a database that already exists it migrates instead of reinstalling.
When it finishes, open Odoo at http://localhost:8069 and the mail catcher at http://localhost:8025.
Mail never leaves your machine
In the dev shell every outgoing email is redirected to Mailpit. This is done by a small server-wide addon that ships with odoo-nix, so it covers every database on the server and cannot be bypassed by an outgoing mail server record. It is never installed into a database and never copied into your repository. At startup the Odoo log warns that all outgoing email is being redirected.
Use Mailpit to check invitations, quotation emails and notifications. The catch-all exists only in the dev shell. It is not part of the server or container builds.
Live reload
Edits are picked up without restarting devenv up:
A change to a
.pyfile anywhere on theaddons_pathrestarts the server in place. A syntax error is logged and the restart is skipped until you fix it.XML views, QWeb templates and JS or SCSS assets are re-read from disk. Reload the browser.
Odoo framework code under
odoo/odoo/is not watched. Restartdevenv upafter changing it.
Look for AutoReload watcher running with watchdog in the log to confirm the watcher is on. Reload only works with workers = 0, which is the dev default.
Note
Reload picks up code, not schema. A new field, a changed data file or a migration script only takes effect once the module is upgraded. See Apply code changes to a database.
Daily commands
Run these from the project directory inside the dev shell.
| Command | What it does |
|---|---|
devenv up | Starts PostgreSQL, Odoo and Mailpit. |
odoo db provision [DB] | Creates the database and installs modules.txt, or migrates it if it exists. |
odoo db upgrade <MODULE[,MODULE]> [DB] | Upgrades the named modules, with a progress bar and a summary table. |
odoo db migrate [DB] | Upgrades only the modules whose code or version changed. |
odoo shell [DB] | Opens the Odoo Python shell. |
odoo test <MODULE[,MODULE]> [DB] | Runs module tests. |
odoo module add [MODULE ...] | Adds OCA modules and their repositories. |
odoo project update | Pulls submodules, refreshes dependencies, re-locks, then migrates. |
odoo db backup [DB] | Backs up a database (schema and filestore by default). |
odoo db list | Lists databases. |
Commands that take a database fall back to the one in odoo.conf. The db upgrade, db migrate and db backup commands also accept --all for every database matching dbfilter.
Add OCA modules
odoo module add resolves which OCA repositories a module needs, adds only the missing ones as submodules under modules/, records the modules in modules.txt, regenerates the managed blocks in pyproject.toml and re-locks.
odoo module add # interactive picker
odoo module add <MODULE_NAME> # by name
odoo module add-bundle # pick a curated bundle
odoo module add-bundle base sales # bundles by nameThe picker lists every installable module in the bundled catalog for your series, not just applications. Bundles live in data/oca-bundles.json in the odoo-nix repository and include base, oca-accounting, project, purchase and sales.
You can also add a repository that is not in the OCA catalog, from any git host:
odoo module add https://example.com/owner/repo.git
odoo module add owner/repo 17.0 my-repoThe second form is GitHub shorthand with an explicit branch and a submodule folder name. When you leave the branch out, odoo-nix uses your Odoo series and falls back to the repository's default branch if that series does not exist.
After any add, reload the shell so Nix re-derives addons_path and rebuilds the Python environment:
direnv reloadThen restart devenv up. Adding a module does not install it into a database you already have. On an existing database, odoo db provision only updates what is installed, so install the new modules from the Apps menu (use Update Apps List first so Odoo notices them). A brand-new database picks up everything in modules.txt when you run odoo db provision.
Check git status and commit the whole result together: .gitmodules, the new submodule folders, modules.txt, pyproject.toml and uv.lock.
Tip
Treat a submodule update as a code change. OCA modules vary in maturity, so after odoo project update smoke-test the features you depend on before you commit the new submodule pointers.
Apply code changes to a database
Odoo creates columns, reloads data files and runs migration scripts only for modules being updated. Changed code without an update leaves the schema silently behind the code. You have two tools.
Upgrade specific modules
When you know which module you changed:
odoo db upgrade my_module
odoo db upgrade my_module,my_other_module <DB>Upgrade whatever changed
After pulling a teammate's work, or after git submodule pointers move, let odoo-nix work out the set:
odoo db migrateIt compares each module's content checksum with the one stored the last time a migration succeeded, and each module's recorded version with its manifest. It prints the plan before it does anything. Before updating it takes a snapshot of the database (a zip with the filestore, by default). Useful options:
--fullupdates every installed module instead of only the changed ones.--also <MODULE,MODULE>forces specific modules into the update.--snapshot zip|dump|nonechooses the snapshot format, or skips it.--rollbackrestores the snapshot if the update fails.
The first migration on a database with no stored checksums updates every module once to set a baseline. odoo db provision records that baseline for a fresh install, so you only meet it on a database restored from elsewhere. The checksums use the same algorithm as the OCA module_auto_update module.
odoo project update runs the same migration across every database matching dbfilter after it updates the code. Pass --no-migrate to skip that step.
Run tests
This is the short version. Testing Custom Odoo Modules covers tags, phases and writing tests that survive upgrades.
odoo test my_module <TEST_DB>Use a separate database from the one you click around in. The command upgrades the module with -u, so the module must already be installed in that database. It refuses to run if the headless browser or the websocket-client package is missing, because Odoo would otherwise skip browser tours and report that as a pass. You can override that with ODOO_TEST_ALLOW_SKIP=1.
If the dev server is running, the test run clashes with its ports. Use the odoo-bin passthrough with different ports instead:
odoo -d <TEST_DB> -u my_module --test-enable --test-tags /my_module --stop-after-init --http-port 8098 --gevent-port 8097Everything else
Any command the odoo wrapper does not know goes to odoo-bin unchanged, so scaffold, populate, cloc and the rest still work. Database housekeeping lives under odoo db:
odoo db duplicate <SOURCE_DB> <NEW_DB>
odoo db restore <DB> <BACKUP_FILE> --force
odoo db drop <DB> --yesBackups land in .devenv/state/odoo/backups/<DB>/ unless you pass --path. Take one before experiments.
Editor setup
The shell links ./.venv to the Nix-built Python environment and generates odools.toml for the Odoo language server. For VS Code it seeds .vscode/settings.json and a recommendation for the Odoo.odoo extension, once, and never overwrites your own files. Other editors can point their language server at ./.venv and the .devenv/state/pythonpath folder.
Start a new project
If you are starting from nothing rather than joining a project, the scaffolder creates the layout above, adds OCB and your chosen OCA repositories as submodules, and locks the Python environment:
nix run github:Avunu/odoo-nix -- --series 18.0 --name <PROJECT_NAME> --db <DB_NAME> <TARGET_DIR>Add --bundles <NAME> or --modules <MODULE,MODULE> to preselect OCA content. Without them you get plain Odoo core, and you can add modules later with odoo module add.
Troubleshooting
A module you just added is not found. Run
direnv reloadsoaddons_pathis regenerated, restartdevenv up, then use Update Apps List in Odoo.Python changes do not reload. Check the log for the
AutoReload watcherline, and confirmworkersis 0.A new field is missing in the UI or errors in the database. Upgrade the module with
odoo db upgrade, or runodoo db migrate.uv lockfails after adding a module. Fix the cause reported in the output, then runodoo project updateto regenerate the managed blocks and lock again.
Sources
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.