CLI Reference¶
Global Options¶
# Show version
oduflow --version
# Disable anonymous usage telemetry for this run only (e.g. test or CI runs);
# place it before any subcommand
oduflow --no-telemetry --transport http
oduflow --no-telemetry stack apply oduflow.yaml
--no-telemetry overrides disable_telemetry in oduflow.toml for the
current process only. See Telemetry.
# Log only warnings and errors (default: info); also before any subcommand
oduflow --log-level warning stack apply oduflow.yaml
--log-level takes debug, info, warning or error and applies to the
server too, its HTTP access log included. warning leaves a one-shot command
such as stack apply printing just its result; at info the progress
messages of long steps (image pulls, environment creation) are shown as well.
A command that fails on invalid input prints one error line; debug adds the
traceback.
Running the Server¶
# Single-user / stdio mode (default — for local MCP clients)
oduflow
uvx oduflow
# Server / HTTP mode (for remote and multi-user deployments)
oduflow --transport http
oduflow -t http
uvx oduflow --transport http
uvx oduflow -t http
Shared infrastructure (Docker network, PostgreSQL, team directories) is initialized automatically on startup.
stdio mode — the server communicates over stdin/stdout. The MCP client starts the process directly; no network port is needed. Ideal for local clients like Claude Desktop, Windsurf, etc.
HTTP mode — starts a persistent HTTP server on http://0.0.0.0:8000 by default. Exposes the MCP endpoint at /mcp, a Web Dashboard at /, and a REST API at /api/. MCP uses Bearer tokens; the dashboard and its REST/WS API use session cookies obtained through the login form (with TOTP when enabled). HTTP Basic is not accepted.
Configuration is loaded from oduflow.toml (see Installation).
See Quick Start for MCP client configuration examples for both modes.
To reconcile a declarative Stack before starting the server:
Startup stops with a non-zero exit if Stack validation, preflight, or apply fails. See Declarative Stacks.
Dashboard Two-Factor Authentication¶
Run locally on the server as the Oduflow service user, with the server's existing configuration and persistent data directory. Docker users should execute these commands inside the running Oduflow container with an interactive terminal.
# Print a local QR code and enable TOTP after confirming an authenticator code
oduflow ui-2fa setup --team 1
# Confirm disabling TOTP and revoke full operator UI sessions
oduflow ui-2fa reset --team 1
--team defaults to 1. Setup requires a configured ui_password; an existing
factor must be reset before replacement. Reset returns the UI to password-only
login until setup runs again. Commands use the same ODUFLOW_TOML configuration
lookup as the server, do not require Docker access, and do not start the server.
Changes take effect without a restart. Shared links and MCP clients are
unaffected. See UI 2FA for enrollment,
session expiry, and recovery details.
License Commands¶
Manage the commercial license on the server. This is the only license interface under a Custom license, where the dashboard shows no license dialog to your clients.
# Holder, validity, and for a Custom license the brand, domains and white-label state
oduflow license status
# Verify and install a key file into the config directory
oduflow license install ./license.key
# Install a renewed key from the license server, if one was paid
oduflow license refresh
# Print a payment link when the license period ended without a subscription
oduflow license subscribe
The commands read the existing configuration only and do not require Docker. Restart the service after installing or renewing a license so MCP clients see the change; the dashboard picks it up within a minute.
Server Settings Console¶
The Server settings console at /admin edits oduflow.toml from
the browser. Its password is the one setting it cannot create for itself:
# Generate a console password, store it as [admin] password, print it once
oduflow admin enable
# Replace the password (signs every console session out)
oduflow admin enable --reset
# Remove the password; /admin answers 404 again
oduflow admin disable
# Optional: authenticator-app 2FA for the console
oduflow ui-2fa setup --admin
oduflow ui-2fa reset --admin
enable and disable write through the same validated, history-keeping writer
as the console. Restart Oduflow afterwards so the running server picks the
password up.
Declarative Stack Commands¶
# Local syntax and schema validation (does not require Docker)
oduflow stack validate oduflow.yaml
# Read-only comparison with live resources
oduflow stack plan oduflow.yaml --team 1
# Reconcile under the team's lock
oduflow stack apply oduflow.yaml --team 1
# JSON status: drift plan plus the last successful apply record
oduflow stack status oduflow.yaml --team 1
Stack apply is additive and non-destructive in V1. Existing resources owned by someone else and environment changes that require replacement are reported as conflicts; no automatic deletion or pruning is performed.
System Commands¶
# Destroy all shared infrastructure (requires no active environments)
oduflow destroy
# Three-way merge deployed files with the installed bundled versions
oduflow upgrade
# Skip the confirmation prompt and overwrite conflicts with the new bundle
oduflow upgrade --force
# Preview the unified host resource plan and managed config diffs
oduflow retune-postgres
# Back up and write configs; stage production Odoo configs in containers
oduflow retune-postgres --apply
# Upgrade the Oduflow package itself, reconcile bundled files, restart
oduflow self-update
# Non-interactive: overwrite bundle conflicts; or skip the service restart
oduflow self-update --force
oduflow self-update --no-restart
Cluster Disaster Recovery¶
Cluster-wide WAL control and whole-cluster PITR are operator actions: they affect every team's productions at once, so they are not MCP tools. They are available in the server settings console (/admin → Cluster DR) and on the owner CLI:
# Cached cluster WAL state (queue, disk reserve, protection latch)
oduflow wal status
# Archiving control / disk-protection recovery (see Production Hosting docs)
oduflow wal pause --confirm ALL-PRODUCTIONS
oduflow wal resume --confirm ALL-PRODUCTIONS
oduflow wal retry --confirm ALL-PRODUCTIONS
oduflow wal recover --confirm ALL-PRODUCTIONS
oduflow wal release --confirm ALL-PRODUCTIONS
# DISASTER RECOVERY: restore the whole production PostgreSQL cluster from
# WAL-G (optionally to a point in time). Also the "resurrect production
# elsewhere" path: a fresh install with the same [backup] section rebuilds
# the cluster from S3.
oduflow restore-cluster --confirm RESTORE-CLUSTER
oduflow restore-cluster --target-time "2026-07-10 12:00:00+00" --confirm RESTORE-CLUSTER
restore-cluster stops every production Odoo container, restores the cluster
from the newest suitable base backup plus WAL replay, and restarts the
productions it stopped. The displaced data directory is kept inside the Docker
volume for manual cleanup. See
Production Hosting for the full semantics.
retune-postgres accounts for [production].enabled and does not restart
containers. For existing productions, --apply also regenerates odoo.conf
with the planned worker count and copies it into the container; the command
then lists every PostgreSQL and Odoo container that should be restarted. It
refuses to replace a custom PostgreSQL config unless --apply --force is
given. See PostgreSQL resource planning.
oduflow upgrade reconciles each team's odoo.conf, agent guides, and bundled
sanitize script against a stored pristine baseline. It compares complete file
contents, updates an untouched file, preserves local-only changes, and uses
git merge-file when both the local and bundled versions changed. Before a live
update, the previous file is saved under
<team-data>/.bundled_upgrade/backups/.
On a clean merge the live file and baseline advance together. On conflict the
live file and accepted baseline stay untouched; the merge result is written to
*.oduflow-merge. Existing customized installations with no baseline receive
*.oduflow-new for a one-time manual reconciliation. Resolve/install the
sidecar and remove it; until then the command exits with status 1.
--force makes the command fully non-interactive: it skips the confirmation
prompt and resolves conflicts, legacy files, and merge failures in favour of
the new bundle. The replaced live file is saved under
<team-data>/.bundled_upgrade/backups/, the baseline advances, and any stale
sidecar is removed, so a forced run leaves nothing to review and exits 0.
Clean merges still merge, local-only changes are still left untouched, and a
first-line # KEEP remains an unconditional opt-out.
This command is separate from upgrading the Python package (for example,
uv tool upgrade oduflow). It does not manage postgresql.conf; use
oduflow retune-postgres for PostgreSQL planning and updates.
Before reconciling, oduflow upgrade checks that the next server start
accepts the configured [database].image and can complete a PostgreSQL upgrade
it requests (see Upgrading PostgreSQL).
If environments or unexpected databases block the upgrade, or the image needs
attention, the command lists the problems and exits with an error without
changing anything, and self-update does not restart the service.
oduflow self-update chains the whole documented upgrade: it compares the
installed version with the latest GitHub release, upgrades the package through
its own installer (uv tool upgrade oduflow for a uv tool install, otherwise
pip install --upgrade oduflow in the same environment), verifies the installed
version, runs the bundled-file reconciliation above through a fresh process in
that Python environment, and restarts the systemd service when the unit
installed by oduflow systemd-install exists and the command runs as root.
--force is forwarded to the reconciliation, and it
also finishes an interrupted upgrade: if the package is already at the latest
version — for example after a first run stopped on a bundle conflict — the
command reconciles and restarts instead of reporting "already up to date" and
doing nothing. --no-restart leaves the running server on the old version
until you restart it yourself.
If the installer succeeds but the advertised release is not installed (for
example, uv has a version pin or the package index has not received the release
yet), the command exits with an error before reconciliation or restart. Check
the installer's constraints and index, then retry. A uv tool upgrade also checks
that uv's tool directory contains the running installation; use the installing
user and original UV_TOOL_DIR if they differ.
It refuses, with an error, installations it cannot upgrade durably: a
container (a package upgraded inside the oduist/oduflow container reverts
when the container is recreated — pull the new image and recreate it instead,
see Docker), a source checkout or editable install (update those
with git pull), an ephemeral uvx run (use uvx oduflow@latest to refresh the
cached version), and an environment pip cannot upgrade in place — a virtualenv
created without pip, or a site-packages the current user cannot
write, where pip install --upgrade would install a second copy into
~/.local that the running service never loads. Re-run those as the user that
owns the installation.
Template Commands¶
All template commands accept --team to specify the team ID (default: 1).
# Generate a clean template from a Docker image
oduflow init-template --odoo-image odoo:19.0 --template-name myproject [--modules base,web,sale] [--force] [--team 1]
# Save a branch environment as the new template.
# Other environments on this template keep their filestore changes by default;
# pass --reset-env-changes to discard them and reset to the new baseline.
oduflow template-from-env <branch> --template-name myproject [--reset-env-changes] [--team 1]
# Re-apply a template's current filestore to live overlay environments
# (non-destructive by default; --reset-env-changes discards env deltas)
oduflow refresh-template <template_name> [--reset-env-changes] [--team 1]
# Attach or replace a template filestore from a local dir, archive, rsync://, or SSH rsync source
oduflow attach-filestore <template_name> <source> [--strip-prefix auto|none|PREFIX] [--reset-env-changes] [--team 1]
# List all template profiles
oduflow list-templates [--team 1]
# Delete a template profile
oduflow delete-template <template_name> [--team 1]
# Import a template from a running Odoo instance
oduflow import-template <odoo_url> <master_pwd> --template-name myproject [--db-name <db>] [--without-filestore] [--team 1]
# Import (or incrementally re-sync with --overwrite) from an S3 prefix or a
# local path holding dump.* + filestore/; a single dump file imports DB-only
oduflow import-template s3://bucket/backups/mydb/ --template-name myproject [--overwrite] [--team 1]
oduflow import-template /backups/mydb/ --template-name myproject [--overwrite] [--team 1]
# A single Odoo backup .zip already sitting in a bucket
oduflow import-template s3://bucket/backups/mydb-2026-10-13.zip --template-name myproject [--team 1]
# Reload the template DB from files already placed in the template directory
# (an external rsync/scp drop) and refresh its metadata
oduflow import-template --template-name myproject --refresh [--team 1]
# Run a saved, enabled SSH sync schedule (cron normally invokes this)
oduflow sync-template myproject [--team 1]
# Only consume a manual request queued by the dashboard
oduflow sync-template myproject --pending [--team 1]
For a private bucket, pass --s3-access-key with --s3-secret-key (plus
--s3-endpoint/--s3-region where needed). The secret key is stored as the
team secret s3-template-<template> and the template records only a
secret:<name> reference, so a later --overwrite re-sync needs no key —
omit --s3-secret-key and the stored one is reused. It is required again when
the endpoint or access key changes. --s3-secret-key defaults to
$ODUFLOW_S3_SECRET_KEY, which keeps the key out of your shell history; pass
secret:<name> to sign with an existing team secret instead.
template-from-env, refresh-template, attach-filestore, and import-template with --overwrite/--refresh are non-destructive for live overlay environments: each is unmounted and remounted against the new template filestore while keeping its upper changes. Use --reset-env-changes (on template-from-env/refresh-template/attach-filestore) to reset environments to the clean baseline instead. Without --overwrite, import-template creates a new template and refuses an existing template name.
Scheduled SSH sync uses retained filestore generations and opt-in environment DB/filestore resets. Ordinary template refresh leaves pinned environments on their previous generation and lists them in its output. See Scheduled SSH sync for source preparation, cron installation and recovery.
Service Commands¶
# List all managed services
oduflow list-services [--team 1]
# List persistent PostgreSQL databases for auxiliary services (no passwords)
oduflow list-service-databases [--team 1]
Create, inspect, rotate, and delete databases through the matching MCP tools
with oduflow call, for example:
oduflow call create_service_database '{"name":"events"}'
# Or on the dedicated production PostgreSQL cluster (requires production hosting)
oduflow call create_service_database '{"name":"events","cluster":"prod"}'
oduflow call get_service_database '{"name":"events"}'
oduflow call rotate_service_database_password '{"name":"events"}'
oduflow call delete_service_database '{"name":"events"}'
Maintenance Commands¶
# Show orphaned databases, workspaces, and port entries (dry-run by default)
oduflow cleanup [--team 1]
# Same as above — only show what would be removed
oduflow cleanup --dry-run [--team 1]
# Actually remove orphaned resources
oduflow cleanup --force [--team 1]
The cleanup command detects and removes resources that no longer have a corresponding running or stopped container:
- Orphan databases — PostgreSQL databases with the
oduflow_prefix that have no matching environment container - Orphan workspaces — workspace directories on disk that have no matching environment container
- Orphan port entries — entries in
ports.jsonthat have no matching environment container
By default, cleanup runs in dry-run mode and only reports what would be removed. Use --force to actually delete the orphaned resources; anything that could not be removed is reported on an Error: line. The dashboard's Cleanup button shows the same preview and can also remove unused Docker images.
Systemd Service¶
# Install and enable systemd service
oduflow systemd-install
# Remove the systemd service
oduflow systemd-uninstall
The systemd-install command generates a unit file at /etc/systemd/system/oduflow.service, runs daemon-reload, and enables the service.
See Auto-start with systemd for the full setup guide.
Tool Introspection¶
Direct Tool Invocation¶
You can invoke any registered MCP tool directly from the terminal using oduflow call, without running the server or connecting an MCP client. This is useful for scripting, debugging, and manual operations.
# List all available tools with their parameters
oduflow call
# Call a tool with positional arguments (mapped to parameters in order)
oduflow call create_environment dev "" "" https://github.com/owner/repo.git odoo:19.0
oduflow call delete_environment dev
oduflow call list_environments
oduflow call get_environment_logs main 50
oduflow call run_odoo_command dev "ls /mnt/extra-addons"
oduflow call create_service redis redis:7 6379
# Call a tool with JSON-encoded arguments
oduflow call create_environment '{"branch":"dev","repo_url":"https://github.com/owner/repo.git","odoo_image":"odoo:19.0","template_name":"myproject"}'
# Service with NET_ADMIN capability (VPN / tun / iptables)
oduflow call create_service '{"name":"vpn","image":"linuxserver/wireguard","port":51820,"net_admin":true}'
# Type coercion is automatic: int, bool, and float parameters are cast from strings
oduflow call get_environment_logs dev 500
Remote Tool Invocation¶
oduflow client calls the same registered tools on a running Oduflow HTTP
server through FastMCP. Unlike oduflow call, it does not load the local
oduflow.toml, access local Docker, or execute server functions in the client
process.
Configure the exact full or scoped MCP endpoint and its Bearer credential:
export ODUFLOW_MCP_URL="https://oduflow.example.com/mcp"
export ODUFLOW_MCP_TOKEN="<team-auth-token>"
# Tools advertised by this endpoint
oduflow client list
oduflow client list --verbose
# Read live help generated from a tool's input schema
oduflow client create_environment --help
# Call team-wide tools
oduflow client list_environments
oduflow client create_environment \
--repo-url https://github.com/owner/addons.git \
--odoo-image odoo:19.0 \
--template-name myproject
The client reads the live tools/list response, converts kebab-case flags to
MCP parameter names, validates basic scalar/JSON types, and then calls the tool.
A single JSON object is also accepted for complex arguments:
Client options must appear before the tool name:
oduflow client --timeout 1200 --json list_environments
oduflow client --env demo pull_and_apply --upgrade sale_custom --strict
When a remote schema requires env_name, the client uses --env, then
ODUFLOW_ENV_NAME, then the current Git branch. When create_environment
requires branch, it uses the current Git branch unless --branch is supplied;
other tools that take a required branch, such as create_production, always
need it spelled out.
An explicit environment override is useful when an environment name differs
from its source branch.
For confined development access, use the environment's scoped endpoint and Secret Key from More → MCP Access:
export ODUFLOW_MCP_URL="https://oduflow.example.com/mcp/feature-x"
export ODUFLOW_MCP_TOKEN="<environment-secret-key>"
# env_name is absent from the scoped schema and injected by the server
oduflow client get_environment_info
oduflow client pull_and_apply --upgrade sale_custom --strict
oduflow client run_odoo_tests --modules sale_custom
The scoped server advertises only its allowlisted single-environment tools, so
team-wide commands such as list_environments and create_environment are not
available. The client does not maintain a second authorization list; the live
server schema remains authoritative.
For scripts and CI, --json emits the complete MCP result and tool failures
return a non-zero exit code. To avoid storing a token in the environment, pass
it on standard input:
In summary, oduflow call <tool> is local in-process execution, while
oduflow client <tool> is remote authenticated MCP execution.