How to back up and restore Odoo
An Odoo backup has two halves that must come from the same moment: the PostgreSQL database and the filestore, the folder <data_dir>/filestore/<database> that holds every attachment, image and stored PDF. Keep odoo.conf and your custom modules with them. The database manager's zip bundles both halves; for scheduled backups, use pg_dump and tar, and neutralize any copy you restore for testing.
What an Odoo backup includes
Odoo Community is open source under the LGPL v3. Odoo Enterprise is Community plus proprietary modules sold by subscription, installed from Odoo's Enterprise package or as an extra addons folder. Both store data the same way. This guide covers self-hosted Odoo 17 to 20 from Odoo's Debian/Ubuntu package, with notes for the official Docker image. The database is called mydb.
| Part | Where | Notes |
|---|---|---|
| Database | PostgreSQL | Records, settings, users and the list of installed modules. |
| Filestore | <data_dir>/filestore/mydb | Attachments, images, stored PDFs and asset bundles, as files named after their SHA-1 hash. |
odoo.conf | /etc/odoo/odoo.conf | Database connection, paths and the master password. Keep it, encrypted. |
| Custom and third-party modules | Folders in addons_path | In git, or archived if you cannot download them again. |
| Enterprise modules | Enterprise package or enterprise folder | Download again for the same version. |
| Sessions | <data_dir>/sessions | Skip. Users sign in again. |
Where the filestore lives depends on the install:
| Install | Filestore of mydb |
|---|---|
Debian/Ubuntu package (no data_dir set; the odoo user's home is /var/lib/odoo) | /var/lib/odoo/.local/share/Odoo/filestore/mydb |
Official Docker image (data_dir = /var/lib/odoo, a volume) | /var/lib/odoo/filestore/mydb in the container |
| Source install | ~/.local/share/Odoo/filestore/mydb of the user running odoo-bin |
data_dir set in odoo.conf | <data_dir>/filestore/mydb |
sudo ls /var/lib/odoo/.local/share/Odoo/filestoreOdoo looks for a database's files only in the folder with exactly its name. A database restored without its filestore opens, but images, attachments and stored PDFs are gone. Back up both halves every time.
The odoo.conf settings that matter
[options]
admin_passwd = $pbkdf2-sha512$...
db_host = False
db_port = False
db_user = odoo
db_password = False
data_dir = /var/lib/odoo/.local/share/Odoo
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/opt/odoo/custom-addons
list_db = False
dbfilter = ^mydb$admin_passwdis the master password for the database manager. Odoo stores it hashed once it is set from the manager.db_host = Falseanddb_user = odooconnect over the local socket as theodoorole the package creates, which is why the commands below run as theodoouser.data_dirholdsfilestore/andsessions/. Setting it to the package default makes the path obvious to whoever restores it.addons_pathlists module folders. Enterprise modules must come before the Community ones.list_dbanddbfilterlock the database manager, below.
The database manager backup, and why to lock it down
The database manager at /web/database/manager downloads backups. Backup asks for the master password and a format: zip holds dump.sql (plain pg_dump output), manifest.json (Odoo and PostgreSQL versions, installed modules) and, with Include filestore ticked, filestore/. "pg_dump custom format (without filestore)" is the database alone. Odoo 17 and later do the same from the command line, even with the manager disabled, into a folder the odoo user can write to:
install -d -m 700 -o odoo -g odoo /var/backups/odoosudo -u odoo odoo db -c /etc/odoo/odoo.conf dump mydb /var/backups/odoo/mydb-$(date +%F).zipBoth copy the whole filestore into a temporary folder before zipping, so they need that much free space, and a zip download is built completely before it starts. Odoo's deployment guide says the manager "is not designed to handle large databases, and may trigger memory limits". With workers enabled, a request that runs past limit_time_real (120 seconds by default) is killed.
The same guide says: "It is strongly recommended to disable the Database Manager for any internet-facing system!" The manager also duplicates, restores and deletes databases. While admin_passwd is still the default admin, Odoo saves the password typed into the next manager form as the new master password, so whoever reaches the page first sets it. Once db_name or dbfilter picks the database for every request, set list_db = False (the --no-database-list option), which blocks the database selection and management screens, then restart:
systemctl restart odooScriptable backup: pg_dump plus a filestore archive
sudo -u odoo pg_dump --no-owner mydb | gzip > /var/backups/odoo/mydb-db-$(date +%F).sql.gz--no-owner leaves out ownership commands, as Odoo's own dump does, so the role that loads the file owns the tables. The output is plain SQL, like the dump.sql in Odoo's zip; the pg_dump guide covers other formats. Then the filestore:
tar -czf /var/backups/odoo/mydb-filestore-$(date +%F).tar.gz -C /var/lib/odoo/.local/share/Odoo/filestore mydbDump the database first. A stored file is named after the hash of its content and never changes, and Odoo deletes files no record uses only in its daily auto-vacuum job, so a filestore copied after the dump has every file the dump refers to, plus a few newer ones that do no harm. Odoo's own zip does it the other way round.
With the official Docker image and Compose services named web and db, as in Docker's example, run the same steps through the containers from the project folder (Compose database backups has more):
docker compose exec -T db pg_dump -U odoo --no-owner mydb | gzip > /var/backups/odoo/mydb-db-$(date +%F).sql.gzdocker compose exec -T web tar -czf - -C /var/lib/odoo/filestore mydb > /var/backups/odoo/mydb-filestore-$(date +%F).tar.gzKeep custom modules in git and note the commit each server runs, so a restore uses the code that matches the database. Archive modules you bought, and keep odoo.conf in its own encrypted archive: db_password is stored there in plain text.
Automate it with cron
The script writes under temporary names, checks that pg_dump wrote its closing line, and only then renames. tar exits with 1 when something changed while it read, so 1 counts as success.
#!/bin/bash
set -euo pipefail
DB="mydb"
FILESTORE="/var/lib/odoo/.local/share/Odoo/filestore"
BACKUP_DIR="/var/backups/odoo"
KEEP_DAYS=14
STAMP="$(date +%Y-%m-%d_%H%M)"
SQL="$BACKUP_DIR/$DB-db-$STAMP.sql.gz"
FILES="$BACKUP_DIR/$DB-filestore-$STAMP.tar.gz"
trap 'rm -f "$SQL.partial" "$FILES.partial"' EXIT
sudo -u odoo pg_dump --no-owner "$DB" | gzip > "$SQL.partial"
gunzip -c "$SQL.partial" | tail -n 6 | grep -q "PostgreSQL database dump complete"
mv "$SQL.partial" "$SQL"
tar -czf "$FILES.partial" -C "$FILESTORE" "$DB" || [ $? -eq 1 ]
mv "$FILES.partial" "$FILES"
find "$BACKUP_DIR" -type f -name "$DB-*" -mtime +"$KEEP_DAYS" -deletechmod 755 /usr/local/bin/odoo-backup.shPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * root flock -n /run/lock/odoo-backup.lock /usr/local/bin/odoo-backup.sh >> /var/log/odoo-backup.log 2>&1Copy the files off the server each night, for example with rclone; Odoo's guide asks for a remote server "that is not accessible from the server itself". The cron guide covers alerts when a run fails.
Restore onto a new server
Install the same major version and edition of Odoo, your custom modules at the matching commit, and /etc/odoo/odoo.conf, adjusted for any new paths or database host. Odoo 20 warns below PostgreSQL 16, Odoo 19 below 13. Stop Odoo, then create the database as Odoo does, in UTF-8 with the C collation from template0, owned by the odoo role:
systemctl stop odoosudo -u postgres createdb -O odoo -E UTF8 -T template0 --lc-collate=C mydbgunzip -c /var/backups/odoo/mydb-db-2026-10-04.sql.gz | sudo -u odoo psql -v ON_ERROR_STOP=1 --quiet -d mydbON_ERROR_STOP=1 stops at the first error instead of carrying on. Restoring a PostgreSQL dump covers other formats. Put the filestore back under the database's exact name, then start Odoo:
sudo -u odoo mkdir -p /var/lib/odoo/.local/share/Odoo/filestoretar -xzf /var/backups/odoo/mydb-filestore-2026-10-04.tar.gz -C /var/lib/odoo/.local/share/Odoo/filestorechown -R odoo:odoo /var/lib/odoo/.local/share/Odoo/filestore/mydbsystemctl start odooA zip from the manager or odoo db dump restores with odoo db load, which accepts nothing else. --move keeps the database UUID, which an Enterprise subscription is tied to; use it when production moves. Without it Odoo treats the restore as a copy and creates a new UUID, like the manager's "This database is a copy" answer.
sudo -u odoo odoo db -c /etc/odoo/odoo.conf load --move mydb /var/backups/odoo/mydb-2026-10-04.zipIf the address changed, check web.base.url under Settings > Technical > System Parameters (developer mode). Odoo resets it to the address an administrator signs in from, unless web.base.url.freeze is set. See also moving a server to a new provider.
Neutralize test copies
A restored copy still has your mail servers, scheduled actions, payment providers and webhooks, so a test restore can email customers. Restore it under a test name (renaming the extracted filestore folder to match), then neutralize it before Odoo serves it:
sudo -u odoo odoo neutralize -c /etc/odoo/odoo.conf -d mydb_testThis runs each installed module's neutralization script. The base one deactivates every mail server and clears its credentials, adds a dummy server so nothing falls back to one set on the command line, deactivates every scheduled action except the auto-vacuum, and disables webhooks. Odoo's documentation adds payment providers, delivery methods, bank synchronization and search-engine indexing, and a red banner marks the database. --stdout prints the SQL instead; odoo db load -n and the manager's Neutralize box do the same during a restore.
An Enterprise copy restored from SQL also keeps production's UUID, and Odoo's documentation says no two databases should share one. This makes the same call Odoo's restore makes for a copy, and also resets database.secret and web.base.url:
echo "env['ir.config_parameter'].init(force=True); env.cr.commit()" | sudo -u odoo odoo shell -c /etc/odoo/odoo.conf -d mydb_testUpgrades are not restores
Odoo's documentation separates updating, a newer build of the same version that leaves the database alone, from upgrading to a new major version, which it calls irreversible. A backup from Odoo 18 restores only into Odoo 18; Odoo 19 does not upgrade it, and its database list marks it "This database may not be compatible".
- Back up database and filestore before every update or upgrade, and keep the old version's code.
- Odoo's upgrade service is included with Enterprise;
python <(curl -s https://upgrade.odoo.com/upgrade) test -d <your db name> -t <target version>requests an upgraded test copy. Community databases are usually upgraded with OpenUpgrade, the Odoo Community Association's open-source project. - The upgraded database comes back without your filestore. Odoo's guide says to merge its
filestorefolder into production's. - Upgrading PostgreSQL is a separate job: see PostgreSQL major upgrades.
Verify the restore
List every file the database expects that the filestore lacks. No output means nothing is missing:
sudo -u odoo psql -d mydb -Atc "SELECT DISTINCT store_fname FROM ir_attachment WHERE store_fname IS NOT NULL" | while read -r f; do [ -e "/var/lib/odoo/.local/share/Odoo/filestore/mydb/$f" ] || echo "missing: $f"; donegrep -E '_read_file reading|_file_read reading|Some modules are not loaded' /var/log/odoo/odoo-server.logcurl -sS -o /dev/null -w '%{http_code}\n' https://erp.example.com/web/loginExpect 200. Sign in, open a record with attachments, and print a PDF. Testing restores makes this routine.
Common errors
| Error | Fix |
|---|---|
Database backup error: Access Denied (17 to 19) or Database management function blocked, bad admin password. (20) | Wrong master password. It is admin_passwd in odoo.conf; Odoo's deployment guide explains how to reset it. |
The database manager has been disabled by the administrator | list_db = False. Use odoo db dump or pg_dump instead. |
WorkerHTTP followed by a process ID and timeout after 120s in the log | The manager backup ran past limit_time_real. Back up from the command line. |
Not a zipped dump file, use followed by a pointer to pg_restore and psql | odoo db load takes only zips. Load SQL dumps with psql. |
Database mydb not initialized, you can force it with -i base | The database is empty; the dump went elsewhere or failed. Load it again. -i base would start a blank database. |
_read_file reading (17 to 19) or _file_read reading (20) followed by a path | The filestore is missing, under another database name, outside data_dir, or not owned by odoo. |
Some modules are not loaded, some dependencies or manifest may be missing: | addons_path lacks custom or Enterprise modules the database uses. |
invalid command \restrict | psql from before the August 2025 minor releases (17.6, 16.10, 15.14, 14.19, 13.22) is loading a dump from a newer pg_dump. Update the client. |
| psql reports that a role does not exist | The dump was made without --no-owner. Dump again with it, or create the role. |
Frequently asked questions
- Where does Odoo store attachments?
- In the filestore: a folder named after the database inside data_dir/filestore. For the Debian/Ubuntu package that is /var/lib/odoo/.local/share/Odoo/filestore; for the official Docker image, /var/lib/odoo/filestore inside the container.
- Does an Odoo database backup include the filestore?
- The database manager's zip format includes it when Include filestore is ticked. The pg_dump custom format, and any pg_dump you run yourself, contains the database only, so archive the filestore separately.
- Can I restore an Odoo 18 backup into Odoo 19?
- No. A backup restores only into the major version it came from. Restore it into Odoo 18, then upgrade it with Odoo's upgrade service (Enterprise) or OpenUpgrade (Community).
- What is the Odoo master password?
- The admin_passwd setting in odoo.conf. It protects the database manager's create, duplicate, backup, restore and delete actions. Set a random one, or disable the manager with list_db = False.
How this was checked
Commands, limits and prices were checked against these official pages, on October 4, 2026:
- Odoo 19.0 documentation: System configuration (database manager security, list_db, dbfilter, backups)
- Odoo 19.0 documentation: Neutralized database
- Odoo 19.0 documentation: On-premise (duplicate, database UUID)
- Odoo 19.0 documentation: Bugfix updates (updating vs upgrading)
- Odoo 19.0 documentation: Upgrade (upgrade service, filestore merge)
- Odoo 19.0 documentation: Source install (Enterprise addons path)
- Odoo 20.0 source: odoo/tools/config.py (data_dir default, filestore path, admin_passwd, list_db, limit_time_real, db_template)
- Odoo 20.0 source: odoo/modules/db.py (dump, restore, duplicate, master password checks)
- Odoo 19.0 source: odoo/service/db.py (dump and restore)
- Odoo 20.0 and 19.0 source: database manager controller (master password, error messages)
- Odoo 20.0 source: database manager template (backup formats, Neutralize, moved or copied)
- Odoo 17.0 to 20.0 source: odoo/cli/db.py (db dump and db load)
- Odoo 20.0 source: neutralize command and base neutralize.sql
- Odoo 20.0 source: ir_attachment.py (filestore layout, garbage collection, read errors)
- Odoo 19.0 source: ir_config_parameter.py and res_users.py (database.uuid, web.base.url)
- Odoo 19.0 source: odoo/service/server.py (worker timeout)
- Odoo 20.0 source: odoo/modules/loading.py (missing modules, uninitialized database)
- Odoo 20.0 and 19.0 source: odoo/release.py (minimum PostgreSQL versions)
- Odoo 20.0 source: Debian packaging (odoo.conf, postinst, odoo.service)
- Odoo official Docker image 20.0 (odoo.conf, Dockerfile)
- Docker Official Images documentation: odoo
- OCA OpenUpgrade
- PostgreSQL documentation: pg_dump
- PostgreSQL documentation: createdb
- PostgreSQL 17.6 release notes (psql \restrict, CVE-2025-8714)