Skip to content

Developing for Cetmix Tower

This file summarizes best practices that should be followed when developing for Cetmix Tower.

General rules

Variables and Metadata

  • Use variables to store configuration values that can be updated manually. E.g. Odoo edition, nginx timeout, database name, etc.
  • Use metadata to store system parameters and complex data structures used in automation only. E.g. Docker build details, server monitoring data, etc.
  • Variable values resolution order: Jet -> Jet Template -> Server -> Global
  • Benefit from using higher level variable values instead of defining them directly in every entity. E.g. if for most servers the home dir is /home/<username>, add a global value instead of adding it to every server.
  • Use readable and self-explanatory references and names:
  • Bad: "Var1" var_1
  • Good: "Request Timeout" request_timeout
  • Use custom values in flight plans in case those variables are not used outside of the flight plan context.

SSH commands

  • Use simple commands. One Tower SSH command - one SSH command on server. Avoid running multiple shell commands using && or ;.
  • Never change directories in SSH command using chdir. Use the path field in command or in a flight plan line to specify the directory.
  • Never use sudo() explicitly. If you need to run command using sudo(), enable the use_sudo option in the flight plan line or command wizard.
  • When Tower runs a command that contains && or ; using sudo() with password it splits such command into single ones. If you want to prevent the command from being split when executed, set the no_split_for_sudo field to True.

Python commands

  • Always use the get_by_reference(<reference>) function to get a record using its reference. Use the system variables when available to access servers, jets and other objects. E.g.:
env["cx.tower.server"].search([("reference", "=", "my_server")])  # bad
server.get_by_reference("my_server") # good
  • Python commands are executed using Odoo safe_eval method. Some functions like getattr, setattr etc. are not available in the running context.
  • Never use . properties to update fields. Use the write method instead:
server.note = "Some note" # wrong, will raise an error
server.write({"note": "Some note"}) # correct
  • Use custom_values to modify existing variable values or pass values between commands in the current flight plan. Prefer adding a _ prefix for custom values you create in the flight plan context to avoid possible shadowing of the existing variables. E.g.:
custom_values["odoo_version"] = "22.0"  # overrides value set in server or jet

custom_values["_such_much_value"] = "wow" # creates a new temporary value that will exist only inside this flight plan run
  • Prefer to use the model functions described in the model reference. However you can use any functions using source code as a reference.

Flight plans

  • Prefer to re-use existing commands when creating new flight plans.
  • Opt for creating generic flight plans using the python conditions in the condition field for the conditional-based flow.
  • Hint: include a Python command or commands that analyze and modify the running context by adding and adjusting custom values that are later checked in the plan condition. Example:
# Python command
if server.os == "ubuntu_2804":
    custom_values["_is_ubuntu_latest"] = True
# And check as a condition in the flight plan line
{{ _is_ubuntu_latest }}
  • Use post run actions to control the flow based on the command result. E.g. if you want a flight plan to continue running despite the command error, add a post run action: if exit_code != 0 -> Run next command

  • Group often used command sequences into smaller flight plans and reuse them later in bigger plans using commands with action Run flight plan.

  • When working with Jets, prefer triggering actions using commands with the Trigger jet action action if you need to run the connected flight plan for the jet.

Jets / Jet Templates

  • Prefer to use unified action patterns across jet templates. E.g.:
Action What is done Initial State Transit State Final State On Error State
Prepare Preparing the initial structure, e.g. folders and files Preparing Draft
Build Building an image, compiling a binary, etc. Draft Building Stopped
Start Starts the app/container/pod Stopped Starting Running
Stop Stops the app/container/pod Running Stopping Stopped
Restart Optional action if you need to restart or reload without stopping Running Restarting Running Stopped
Remove Removes the container or app binary but leaves the directory structure so it can be re-used Stopped Removing Removed
Destroy Final action that removes all the resources (files, databases, images) and performs a clean up Removed Destroying
  • You can use an action without a flight plan if you want to keep the actions template while not doing any actual actions during the state transition.
  • Jet can be deleted only when the deletable field is set to True. This is the default value. It makes sense to set it to False using a Python command when the Jet is in the "Prepare" or "Build" actions and set it back to True in the "Destroy" action.
  • Build and Start need different plans. Start must not reuse the Build plan — Build creates resources; re-running on Start fails. Use different commands for Start (e.g. docker start for Docker). Production templates should define Stop, Remove, Destroy with distinct plans. Actions that do the same thing in different contexts (e.g. different template variants or different initial states) can share a plan.

Jet Template structure: atomized vs monolithic

Choose between atomized (separate reusable Jet Templates) and monolithic (single Jet Template with everything) based on your use case.

Atomized structure (building blocks)

Keep database, web proxy, and application as separate Jet Templates. Use template dependencies (template_requires_ids) so an app Jet requires a DB Jet in running state, etc.

Pros Cons
Reusability — MariaDB, Traefik, or Nginx templates can be shared across many app templates More setup — user creates multiple Jets (DB, proxy, app) instead of one
Explicit dependenciestemplate_requires_ids documents what this template needs Orchestration — user must know deployment order (DB → proxy → app)
Independent lifecycle — upgrade MariaDB once, all dependent Jets benefit Networking — cross-Jet connectivity (Docker networks, hostnames) can be trickier
Modular testing — components can be tested in isolation More complexity — more templates to maintain and document

Use atomized when:

  • Building shared infrastructure (e.g. one Traefik for many apps)
  • Components have different lifecycles or owners
  • Creating a library of reusable templates for advanced users
  • Following the demo data pattern: docker → nginx/postgres/mariadb → odoo/wordpress

Monolithic structure (single stack)

One Jet Template deploys the full stack (e.g. Traefik + MariaDB + WordPress in one Docker Compose).

Pros Cons
Simplicity — one Jet to create and manage No reuse — each stack is self-contained
Easier onboarding — fewer steps for new users Tighter coupling — components cannot be shared
Simpler networking — services share one Compose network All-or-nothing — upgrade one component, redeploy entire stack
Quick deployment — suitable for demos and POCs

Use monolithic when:

  • Targeting simple single-server deployments
  • Ease of use and onboarding are top priority
  • Components are always deployed together
  • Building demos, POCs, or small projects

Choosing the approach

  • Atomized: shared infra, platform teams, template libraries, mix-and-match scenarios.
  • Monolithic: single-server stacks, quick demos, straightforward workflows.

Document your choice in the YAML manifest so users understand the intended use case.