Key takeaways
- Keep every custom module in Git, with one folder per addon, so you always know which code is running where.
- Use one branch per environment, such as staging and production, and promote changes by merging.
- New code is not enough. Odoo needs a module update when models, fields, views or data files change.
- A good deploy can be undone: you should be able to go back to the previous commit in minutes.
Why Git, and not copying files
Many Odoo servers start with modules uploaded by SFTP or unzipped into a folder. It is quick, but after a few months nobody knows exactly which version is on the server, who changed it, or how to put back yesterday's code. Two developers can overwrite each other's changes without noticing.
Git fixes this with very little effort:
- History. Every change has an author, a date and a message, and can be compared or reverted.
- A known state. Each server runs a specific commit, so you can say exactly what is deployed.
- Review. Changes can go through a pull request before they reach any server.
- Repeatable deploys. Staging and production get the same code by checking out the same commit.
How to lay out the repository
Odoo finds modules through its addons path: a list of directories,
each containing addon folders. An addon folder is one module, with its
__manifest__.py and __init__.py at the top. The simplest
layout puts each addon directly at the root of the repository:
my-odoo-addons/
sale_custom_discount/
__init__.py
__manifest__.py
models/
views/
stock_label_print/
__init__.py
__manifest__.py
...
requirements.txt
README.md
This is the layout used by the Odoo Community Association (OCA) repositories, so the same tools and habits work for both. If you prefer to group addons in sub-folders, for example one folder per client or per project, that works too, as long as your deployment adds each folder to the addons path.
One repository or several?
A single repository for all your custom addons is easiest to start with. Split it when addons have different owners or release cycles, for example a shared library of modules used by several clients. Keep third-party modules, such as OCA addons, in their own repositories rather than copying them into yours, so you can follow their updates.
Python dependencies
If an addon needs an extra Python package, list it in a
requirements.txt at the repository root or inside the addon, and declare
it under external_dependencies in the manifest. Pin versions where an
upgrade could break you. That way a new server gets the same packages as the old one.
Branches: one per environment
Odoo projects work well with a simple model: a branch for each environment, named after the Odoo series it targets.
-
Production branch (for example
19.0): only code that has passed testing. -
Staging branch (for example
19.0-staging): code being tested on a copy of production. - Feature branches: one per change, merged into staging when ready.
A change moves forward by merging: feature into staging, then staging into production.
Nothing is edited directly on a server. When you plan a major upgrade, you create the
next series branch, such as 20.0, and port the modules there, as
described in our
Odoo 18 to 19 upgrade guide.
The staging branch only makes sense with a place to run it. Our article on why every Odoo project needs a staging environment explains how to set one up safely.
What a deploy has to do
Getting new code onto the server is only the first step. A complete deploy of an Odoo module does four things:
- Update the code by fetching the branch and checking out the new commit.
- Install Python requirements if they changed.
- Restart Odoo so it loads the new Python code.
-
Update the changed modules in the database, with Odoo's
-uoption, when models, fields, views, security rules or data files changed.
Step 4 is the one that is most often forgotten. A restart loads new Python code, but new fields are not added to the database and changed views are not reloaded until the module is updated. The symptoms are confusing: errors about missing columns, or a view that still looks the same after the deploy.
Update only what changed
Running -u all on every deploy is slow on a large database and touches
modules you did not change. It is better to update only the addons whose files changed
between the old and new commit. Bump the version in the manifest when you change a
module, so it is clear which version is installed.
Plan for a failed deploy
Deploys fail: a syntax error, a missing dependency, a migration that hits unexpected data. Before you deploy to production, know how you will go back. With Git, the code part is easy: check out the previous commit and restart. Database changes made by a module update are harder to undo, which is why you test the update on staging first and take a backup before deploying to production. Our Odoo backup strategy guide covers what that backup needs to include.
Private repositories and access
Most custom modules live in private repositories. The usual way to give a server read access is a deploy key: an SSH key that belongs to one repository and can only read it. It is safer than putting a personal access token or a developer's own key on the server. Give each repository its own deploy key, and remove it when the server is retired.
To deploy automatically on every push, the Git host calls a webhook on your deployment system. Protect it with a secret, so only your Git host can trigger a deploy.
Doing it yourself vs using a platform
On a plain server you can script all of this: a Git checkout per repository, a deploy key, a webhook receiver, pip, a restart and a module update, plus a way to roll back. Many teams do, and it works well once it is built. The cost is the time to build it and keep it working, and the risk that a step is skipped when someone deploys by hand.
Odoo.sh, Odoo's own platform, is built around Git: each branch is an environment, and it deploys when you push (as of October 2026). It is a good fit if you are happy to host on Odoo's infrastructure. Our CloudPilot vs Odoo.sh comparison covers the differences.
How CloudPilot deploys modules from Git
CloudPilot runs Odoo on servers in your own cloud account or on your own server, and handles the deploy steps above from an instance's Modules tab:
- Connect a repository and branch. Pick a repository from GitHub, or add any Git repository over SSH with the deploy key CloudPilot shows you. You can list addon sub-directories and addons to install after the first sync.
- Deploy on push. A push webhook from GitHub, GitLab or Bitbucket redeploys the branch.
- Auto-upgrade on push. When turned on, CloudPilot updates only the addons whose files changed in the new commit.
-
Install requirements. When turned on, CloudPilot installs the
packages listed in the repository's
requirements.txtfiles. For safety, only plain package requirements are accepted, not URLs or pip options. - Back to the previous commit. If a deploy fails after the code has moved, CloudPilot puts back the previous commit and Odoo configuration and restarts the instance.
Pro and Business include custom modules from any Git host. Staging copies can follow their own branch, so you test a change on staging before merging it into production. See pricing for each plan and Deploy custom modules from Git for the step-by-step setup.
Git deploys come with every plan, including Odoo Community hosting and the Odoo.sh alternative workflow for teams leaving Odoo.sh. For module structure, follow the Odoo developer tutorial and the OCA repositories for examples.
Frequently asked questions
Do I need to restart Odoo after deploying a custom module?
Yes, Odoo has to restart to load changed Python code. If you changed models,
fields, views, security rules or data files, you also need to update the module
with the -u option so the database matches the new code.
Should OCA modules go in the same repository as my custom modules?
It is better to keep them in their own repositories and add those to the addons path. You can then follow OCA updates without mixing them into your own code.
How do I deploy a private Git repository to an Odoo server?
Add a read-only deploy key for the server to the repository, and clone it over SSH. Avoid putting a personal access token or a developer's own SSH key on the server.