← Back to guides

Odoo backups

Odoo backups: database, filestore, and custom modules

This guide explains what a real Odoo backup should include before you trust a deployment, migration, automation, upgrade, or AI/database integration.

The backup principle

A useful Odoo backup must be restorable. For most self-hosted environments, that means backing up the , the , , and the deployment configuration needed to run the system again.

Read this before running backup commands

This guide is practical, but backup commands still need care. A backup may contain customer data, employee data, invoices, API keys, passwords, and other sensitive business information. Store backups securely, restrict permissions, and avoid copying production backups to unsafe locations.
Before changing production backup automation, first create a manual backup and test a restore in a separate sandbox. A backup process that has never been restored is not fully proven.

Planning

Design decisions before changing backup, storage, automation, or restore processes.

Safe check

Read-only commands or manual checks that should not change the server state.

Caution

Checks or plans that may involve sensitive data, secrets, or careful interpretation.

Change required

Actions that create, copy, restore, move, or automate backups. Plan storage, permissions, and impact before applying.

Backup checklist

1. Understand what a complete Odoo backup includes

Planning

An Odoo backup is not only a PostgreSQL dump. Odoo stores binary files, attachments, custom modules, configuration, and deployment secrets outside the database.

Show command and interpretation

Command or manual check

A complete Odoo backup usually includes:

1. PostgreSQL database
2. Odoo filestore
3. Custom addons
4. Odoo configuration
5. Docker Compose or deployment files
6. Environment variables / secrets
7. Nginx configuration
8. Restore instructions
9. Off-server copy

How to interpret the result

If you only backup PostgreSQL, you may lose:
- uploaded documents
- product images
- invoice PDFs
- email attachments
- website media
- custom modules
- deployment configuration
- passwords, tokens, and integration settings stored outside the database

A backup is only trustworthy if you know how to restore it.

2. Identify the Odoo deployment type

Safe check

Backup commands depend on how Odoo is installed. Before backing up anything, identify whether Odoo is managed by Docker Compose, a Linux systemd service, managed hosting, or an on-premise setup.

Show command and interpretation

Command or manual check

echo "=== Docker containers ==="
sudo docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}" 2>/dev/null || echo "Docker is not installed or you do not have permission to run Docker"

echo
echo "=== Docker Compose project files in current folder ==="
pwd
ls -la docker-compose.yml compose.yml 2>/dev/null || echo "No Docker Compose file found in the current folder"

echo
echo "=== Odoo systemd services ==="
systemctl list-units --type=service --all | grep -Ei "odoo" || echo "No Odoo systemd service found"

echo
echo "=== Common Odoo paths ==="
sudo find /etc /opt /var/lib /var/log -maxdepth 3 -iname "*odoo*" 2>/dev/null | head -100

How to interpret the result

Use the output to identify the deployment type.

Docker Compose signs:
- docker ps shows Odoo and PostgreSQL containers
- a docker-compose.yml or compose.yml file exists
- common project folders look like /opt/odoo-secure, /opt/odoo, or another deployment folder
- Docker volumes may appear under /var/lib/docker/volumes

Linux package/systemd signs:
- systemctl shows an Odoo service
- common config paths may include /etc/odoo/odoo.conf
- Odoo data may be under /var/lib/odoo

Managed hosting signs:
- you may not have SSH, Docker, or systemd access
- backups may be controlled by the hosting provider

On-premise/office server signs:
- Odoo may run on a local company server
- DNS/firewall/router rules may forward public traffic to the server
- backups must consider local storage, network shares, and physical hardware risk

How to interpret common results:
- "No Odoo systemd service found" is normal for Docker Compose deployments.
- Docker volumes such as /var/lib/docker/volumes/... usually mean Docker is storing database or Odoo data.
- Nginx and Let's Encrypt paths show the public HTTPS reverse proxy setup, not the Odoo database itself.
- Do not copy backup commands blindly until you know where the database, filestore, addons, and config are stored.

3. Locate the active database name

Safe check

You need the exact database name before creating a PostgreSQL dump or testing a restore.

Show command and interpretation

Command or manual check

# Docker example:
# Replace odoo-secure-db with your PostgreSQL container name from step 2.
# Replace -U odoo with the PostgreSQL user from your odoo.conf db_user value.

sudo docker exec odoo-secure-db psql -U odoo -l

# Or inside PostgreSQL:
sudo docker exec odoo-secure-db psql -U odoo -d postgres -c "\l"

How to interpret the result

Look for the active Odoo database name.

Example:
- odoo_prod

PostgreSQL often includes a default database named postgres. Do not confuse it with your Odoo database. In this example, the real Odoo business database is odoo_prod.

Backing up only postgres would not back up the Odoo customers, products, orders, invoices, users, or settings.

4. Locate the Odoo filestore

Safe check

The filestore contains uploaded files and attachments. Without it, a restored database may show missing files, images, PDFs, and documents.

Show command and interpretation

Command or manual check

echo "=== Check Odoo data_dir from config ==="
cd /opt/odoo-secure
grep -E "^data_dir" config/odoo.conf || echo "No data_dir found in config"

echo
echo "=== Check common filestore paths inside the Odoo container ==="
sudo docker exec odoo-secure-app sh -c '
echo "--- data_dir path: /var/lib/odoo ---"
ls -la /var/lib/odoo 2>/dev/null || true

echo
echo "--- common path: /var/lib/odoo/filestore ---"
ls -la /var/lib/odoo/filestore 2>/dev/null || true

echo
echo "--- database filestore: /var/lib/odoo/filestore/odoo_prod ---"
ls -la /var/lib/odoo/filestore/odoo_prod 2>/dev/null || true

echo
echo "--- alternate path: /var/lib/odoo/.local/share/Odoo/filestore ---"
ls -la /var/lib/odoo/.local/share/Odoo/filestore 2>/dev/null || true

echo
echo "--- alternate database filestore: /var/lib/odoo/.local/share/Odoo/filestore/odoo_prod ---"
ls -la /var/lib/odoo/.local/share/Odoo/filestore/odoo_prod 2>/dev/null || true
'

echo
echo "=== Find filestore paths on the host or Docker volumes ==="
sudo find /var/lib/docker /opt /var/lib -path "*filestore*" -type d 2>/dev/null | head -50

How to interpret the result

The filestore path can differ depending on the Odoo version, Docker image, configuration, or volume mapping.

Good signs:
- you find a filestore folder matching the Odoo database name
- example: filestore/odoo_prod
- the folder contains many small subfolders with short names such as 0a, 1f, b5, cd, etc.
- in Docker setups, the host path may be inside /var/lib/docker/volumes/...

Common container paths:
- /var/lib/odoo/filestore/<database_name>
- /var/lib/odoo/.local/share/Odoo/filestore/<database_name>

Common host/Docker volume paths:
- /var/lib/docker/volumes/<project>_odoo-web-data/_data/filestore/<database_name>
- /var/lib/docker/volumes/<project>_odoo-data/_data/filestore/<database_name>
- a custom host folder mounted into the Odoo container

How to interpret the result:
- If /var/lib/odoo/filestore/odoo_prod exists inside the container, that is likely the active filestore.
- If the container path does not show the filestore but the host search finds /var/lib/docker/volumes/.../_data/filestore/odoo_prod, the filestore is stored in a Docker volume.
- If no filestore is found, confirm the database name, check data_dir in odoo.conf, and inspect Docker volume mounts.
- The filestore folder should match the Odoo database name. For example, database odoo_prod should usually have a filestore folder named odoo_prod.

Important:
The database dump and filestore backup should be taken together as a consistent pair. Backing up only PostgreSQL may restore the records, but uploaded files, images, PDFs, and attachments may be missing if the filestore is not included.

For a perfectly consistent pair on a busy instance, take the backup during low activity or briefly stop Odoo, not PostgreSQL, so no new attachments are written between the database dump and the filestore archive. On a quiet instance, taking them back-to-back is normally sufficient.

5. Locate custom addons

Safe check

Custom modules are often outside the database. If you restore the database without the custom module code, Odoo may fail to start or important features may behave incorrectly.

Show command and interpretation

Command or manual check

# Go to the folder where your Odoo deployment is managed.
# Example for a Docker Compose install:
cd /opt/odoo-secure

echo "=== Current folder ==="
pwd

echo
echo "=== Review addons path from odoo.conf ==="
grep -E "^addons_path" config/odoo.conf || echo "No addons_path found"

echo
echo "=== Project addons folder ==="
ls -la addons 2>/dev/null || echo "No local addons folder found in this project"

echo
echo "=== Extra addons folder inside the Odoo container ==="
sudo docker exec odoo-secure-app sh -c 'ls -la /mnt/extra-addons 2>/dev/null || echo "No /mnt/extra-addons folder found"'

echo
echo "=== Search common custom addons folders ==="
sudo find /opt /mnt /var/lib -maxdepth 4 -type d \( -name "addons" -o -name "custom-addons" -o -name "extra-addons" \) 2>/dev/null

How to interpret the result

How to interpret the result:

1. Check addons_path in odoo.conf.
   This tells you where Odoo expects to load modules from.

   Example:
   - addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons

2. Separate built-in addons from custom addons.
   Built-in Odoo addons are usually part of the Odoo installation or Docker image.

   Common built-in path:
   - /usr/lib/python3/dist-packages/odoo/addons

   You normally do not need to back up built-in Odoo core addons if you can recreate the same Odoo version from the image/package.

3. Identify custom addon folders.
   These are the folders you must back up.

   Common custom addon paths:
   - /opt/odoo-secure/addons
   - /mnt/extra-addons inside the container
   - /opt/odoo/custom/addons
   - /opt/odoo/extra-addons
   - a custom host folder mounted into the Odoo container

4. Empty custom addon folder is okay.
   If /opt/odoo-secure/addons exists but is empty, it means no custom addons are currently installed there. Still include the folder in backup scripts so future custom modules are covered.

5. Ignore unrelated addon folders.
   Some paths may contain the word addons but are not Odoo custom modules.

   Example:
   - /var/lib/vim/addons is related to Vim, not Odoo.

If you use third-party or custom modules, also keep a list of:
- module name
- version
- source repository or vendor
- license
- Odoo version compatibility
- whether the module is required for the database to start correctly

6. Locate configuration and secrets

Caution

Odoo needs configuration to reconnect to the database, use the correct addons path, run behind a proxy, and protect the database manager. Some files contain secrets.

Show command and interpretation

Command or manual check

# Example Docker Compose deployment:
cd /opt/odoo-secure

echo "=== Deployment files ==="
ls -la

echo
echo "=== Config files ==="
ls -la config

echo
echo "=== Show important odoo.conf keys without secrets ==="
grep -E "^(admin_passwd|db_host|db_port|db_user|db_password|proxy_mode|list_db|dbfilter|addons_path|data_dir)" config/odoo.conf \
  | sed -E 's/(admin_passwd|db_password) = .+/\1 = ***hidden***/'

echo
echo "=== Environment file exists, do not print secrets ==="
ls -la .env 2>/dev/null || echo "No .env file found"

echo
echo "=== Nginx site configs that may need backup ==="
sudo ls -la /etc/nginx/sites-available 2>/dev/null | grep -Ei "odoo|erp" || echo "No obvious Odoo Nginx config found"

How to interpret the result

Important files may include:
- docker-compose.yml
- .env
- config/odoo.conf
- Nginx site config
- custom scripts
- backup scripts
- deployment notes

How to interpret the result:

1. Deployment files
   The project folder should contain the files needed to recreate the deployment.

   Common files and folders:
   - docker-compose.yml
   - .env
   - config/
   - addons/

2. Odoo configuration
   config/odoo.conf should contain important runtime settings such as:
   - db_host
   - db_port
   - db_user
   - addons_path
   - data_dir
   - proxy_mode
   - list_db
   - dbfilter

   Sensitive values such as admin_passwd and db_password should be hidden when displayed.

3. Environment file
   .env may contain database passwords, Odoo master passwords, tokens, or other secrets.
   It should exist if your deployment depends on it, but its content should not be printed publicly.

4. Nginx configuration
   If Odoo is exposed through Nginx/HTTPS, the Nginx site config should be included in the backup plan.

   Common path:
   - /etc/nginx/sites-available/your-odoo-domain

5. File permissions
   Secret files should have restricted permissions.

   Good examples:
   - .env with 600 permissions
   - docker-compose.yml with restricted permissions if it contains secrets

Do not paste secrets publicly.

Backups containing .env, odoo.conf, database dumps, or configuration files should be protected with strict permissions and stored securely.

7. Create a manual backup folder

Change required

Create a timestamped folder so database, filestore, addons, and config backups are grouped together.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"
BACKUP_DATE="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="$BACKUP_ROOT/$BACKUP_DATE"

sudo mkdir -p "$BACKUP_DIR"
sudo chmod 700 "$BACKUP_ROOT" "$BACKUP_DIR"

echo "$BACKUP_DIR"

echo
echo "=== Verify backup folder permissions ==="
sudo ls -ld "$BACKUP_ROOT" "$BACKUP_DIR"

How to interpret the result

Expected:
- a new timestamped backup folder is created
- permissions restrict casual access
- all backup parts for this run are stored together

Important:
Copy the printed BACKUP_DIR path and reuse the same folder for the following backup steps. Do not run this folder-creation step again until the current backup run is finished, or later steps may target a new empty folder instead of the intended backup folder.

Good permission example:
- drwx------ root root /opt/odoo-backups
- drwx------ root root /opt/odoo-backups/YYYYMMDD-HHMMSS

The timestamped folder will be used for:
- PostgreSQL database dump
- Odoo filestore archive
- custom addons
- deployment configuration
- backup manifest

For production, use an off-server backup destination too. A backup stored only on the same VPS may be lost if the VPS is deleted, corrupted, encrypted by malware, or compromised.

8. Back up the PostgreSQL database

Change required

The PostgreSQL database contains Odoo business records: users, partners, products, orders, invoices, settings, module state, and more.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"
DB_NAME="odoo_prod"

# Prefer pinning the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Using backup folder ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

echo
echo "=== Create PostgreSQL dump ==="
sudo docker exec odoo-secure-db pg_dump -U odoo -Fc "$DB_NAME" | sudo tee "$BACKUP_DIR/database.dump" >/dev/null

echo
echo "=== Verify database dump ==="
sudo ls -lh "$BACKUP_DIR/database.dump"

echo
echo "=== Test dump metadata ==="
sudo docker exec -i odoo-secure-db pg_restore -l < "$BACKUP_DIR/database.dump" | head -20

How to interpret the result

The database dump should exist inside the timestamped backup folder.

Replace BACKUP_DIR with the exact folder path printed in step 7. This avoids accidentally splitting one backup across multiple timestamped folders.

Good result:
- database.dump exists
- file size is not 0
- pg_restore -l can read the dump metadata
- metadata shows the expected Odoo database name

Example:
- dbname: odoo_prod
- Format: CUSTOM

If you see "Permission denied" when finding the backup folder:
- the backup root is probably protected with 700 permissions
- use sudo when listing or finding backup folders
- do not weaken permissions just to make the command easier

If the dump is accidentally created at /database.dump:
- move it into the timestamped backup folder
- verify it with pg_restore -l
- avoid leaving backup files in /

Important:
If the server will be reset, deleted, reinstalled, or restored from scratch, copy the backup off the server before doing anything destructive. A backup stored only on the same VPS can be lost with the VPS.

For large production databases:
- schedule during lower activity
- monitor disk space
- consider compression and off-server transfer
- test restore regularly

9. Back up the Odoo filestore

Change required

The filestore contains attachments and binary content. It should be backed up with the database.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"
DB_NAME="odoo_prod"

# Use the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Using backup folder ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

echo
echo "=== Try common filestore locations ==="

# Common Docker volume / host path example.
FILESTORE_PATH="$(sudo find /var/lib/docker /opt /var/lib -path "*/filestore/$DB_NAME" -type d 2>/dev/null | head -1)"

echo "Detected filestore path:"
echo "$FILESTORE_PATH"

if [ -z "$FILESTORE_PATH" ]; then
  echo "No filestore folder found for database: $DB_NAME"
  echo "Go back to the filestore location step and confirm the correct path."
  exit 1
fi

echo
echo "=== Verify filestore folder ==="
sudo ls -ld "$FILESTORE_PATH"

echo
echo "=== Create filestore archive ==="
sudo tar -C "$(dirname "$FILESTORE_PATH")" -czf "$BACKUP_DIR/filestore.tar.gz" "$DB_NAME"

echo
echo "=== Verify filestore archive ==="
sudo ls -lh "$BACKUP_DIR/filestore.tar.gz"

echo
echo "=== Test archive content ==="
sudo tar -tzf "$BACKUP_DIR/filestore.tar.gz" | head -20

How to interpret the result

The filestore archive should exist inside the timestamped backup folder.

Replace BACKUP_DIR with the exact folder path printed in step 7. This avoids accidentally splitting one backup across multiple timestamped folders.

Good result:
- filestore.tar.gz exists
- file size is not 0
- tar -tzf can list the archive content
- archive content includes the database filestore folder

Example:
- odoo_prod/...
- odoo_prod/b5/...
- odoo_prod/22/...

If the filestore path does not exist:
- confirm the database name
- confirm data_dir in odoo.conf
- inspect Docker volumes
- check whether filestore is mounted somewhere else
- go back to the filestore location step before continuing

If you see "Permission denied":
- the backup folder is probably protected with 700 permissions
- use sudo to read, write, list, or archive backup files
- do not weaken backup folder permissions just to make commands easier

Important:
Do not assume the filestore is inside PostgreSQL. It is usually separate. A restored database without the matching filestore may have missing images, PDFs, uploads, and attachments.

For a perfectly consistent database and filestore pair on a busy instance, take the backup during low activity or briefly stop Odoo, not PostgreSQL, so no new attachments are written between the database dump and the filestore archive. On a quiet instance, taking them back-to-back is normally sufficient.

10. Back up custom addons and deployment config

Change required

Database and filestore are not enough if the Odoo instance depends on custom code, Docker Compose, Nginx, or configuration files.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"

# Use the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Using backup folder ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

echo
echo "=== Back up Odoo project config and addons ==="
cd /opt/odoo-secure

sudo tar -czf "$BACKUP_DIR/odoo-project-config-and-addons.tar.gz" \
  docker-compose.yml \
  .env \
  config \
  addons

echo
echo "=== Back up Nginx Odoo site config if it exists ==="
sudo cp -a /etc/nginx/sites-available/odoo.example.com "$BACKUP_DIR/nginx-site.conf" 2>/dev/null || echo "No matching Nginx site config found"

echo
echo "=== Verify backup folder content ==="
sudo ls -lh "$BACKUP_DIR"

echo
echo "=== Test project config archive content ==="
sudo tar -tzf "$BACKUP_DIR/odoo-project-config-and-addons.tar.gz" | head -30

How to interpret the result

The backup folder should include:
- database.dump
- filestore.tar.gz
- odoo-project-config-and-addons.tar.gz
- nginx-site.conf if applicable

Replace BACKUP_DIR with the exact folder path printed in step 7. This avoids accidentally splitting one backup across multiple timestamped folders.

The project config archive should include:
- docker-compose.yml
- .env
- config/
- config/odoo.conf
- addons/

How to interpret the result:
- database.dump is the PostgreSQL database backup
- filestore.tar.gz is the Odoo attachments/files backup
- odoo-project-config-and-addons.tar.gz contains deployment files, secrets, Odoo config, and custom addons
- nginx-site.conf contains the public reverse proxy / HTTPS configuration if Odoo is exposed through Nginx

Be careful:
- .env and odoo.conf may contain secrets
- database.dump may contain business and personal data
- backup folders should stay permission-protected
- do not publish or share these backup files publicly

If you see "Permission denied":
- the backup folder is probably protected with 700 permissions
- use sudo to list, read, or write backup files
- do not weaken backup folder permissions just to make commands easier

11. Create a backup manifest

Safe check

A manifest helps you understand what is inside the backup later, especially during a stressful restore.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"

# Use the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Using backup folder ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

{
  echo "Backup date: $(date -Is)"
  echo "Hostname: $(hostname)"
  echo "Odoo URL: https://odoo.example.com"
  echo "Database: odoo_prod"
  echo
  echo "Backup folder:"
  echo "$BACKUP_DIR"
  echo
  echo "Files:"
  sudo ls -lh "$BACKUP_DIR"
  echo
  echo "Docker containers:"
  sudo docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}" 2>/dev/null || true
  echo
  echo "Odoo config summary:"
  cd /opt/odoo-secure
  grep -E "^(db_host|db_port|db_user|proxy_mode|list_db|dbfilter|addons_path|data_dir)" config/odoo.conf 2>/dev/null || true
} | sudo tee "$BACKUP_DIR/MANIFEST.txt" >/dev/null

echo
echo "=== Manifest content ==="
sudo cat "$BACKUP_DIR/MANIFEST.txt"

How to interpret the result

The manifest should describe:
- when the backup was made
- which server it came from
- which Odoo URL it belongs to
- which database was backed up
- which backup folder was used
- which files were included
- which containers/images were running
- basic Odoo configuration context without secrets

Replace BACKUP_DIR with the exact folder path printed in step 7. This avoids accidentally creating a manifest for the wrong backup folder.

Good signs:
- MANIFEST.txt exists inside the backup folder
- it lists database.dump
- it lists filestore.tar.gz
- it lists odoo-project-config-and-addons.tar.gz
- it lists nginx-site.conf if applicable
- it references the correct database name

How to interpret the result:
- the manifest is not the backup itself
- it is a readable index of what the backup contains
- it helps during restore because you do not need to guess which database, URL, server, or files the backup belongs to

Important:
Do not print secrets in the manifest. Avoid including admin_passwd, db_password, API keys, tokens, or private environment values.

12. Copy the backup off the server

Change required

A backup stored only on the same server does not protect against server deletion, disk failure, ransomware, or serious compromise.

Show command and interpretation

Command or manual check

# Step A — Run this on the server first.
# It verifies the protected backup path and creates a temporary export archive
# that your SSH user can download.

BACKUP_ROOT="/opt/odoo-backups"

# Use the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Protected backup folder on the server ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

echo
echo "=== Verify backup content before export ==="
sudo ls -lh "$BACKUP_DIR"

BACKUP_NAME="$(basename "$BACKUP_DIR")"
EXPORT_DIR="$HOME/backup-export"
EXPORT_FILE="$EXPORT_DIR/odoo-backup-$BACKUP_NAME.tar.gz"

echo
echo "=== Create temporary export archive ==="
mkdir -p "$EXPORT_DIR"
chmod 700 "$EXPORT_DIR"

sudo tar -C "$(dirname "$BACKUP_DIR")" -czf "$EXPORT_FILE" "$BACKUP_NAME"

sudo chown "$USER:$USER" "$EXPORT_FILE"
chmod 600 "$EXPORT_FILE"

echo
echo "=== Export file ready for download ==="
ls -lh "$EXPORT_FILE"

echo
echo "Download this file from your local computer:"
echo "$EXPORT_FILE"


# Step B — Run this from your local computer, not from the server.
# Example from macOS/Linux terminal:
scp your-user@your-server-ip:/home/your-user/backup-export/odoo-backup-YYYYMMDD-HHMMSS.tar.gz ./


# Example from Windows PowerShell:
scp your-user@your-server-ip:/home/your-user/backup-export/odoo-backup-YYYYMMDD-HHMMSS.tar.gz C:\\Users\\YourName\\Backups\\


# Step C — After confirming the file exists on your local computer,
# run this on the server to remove the temporary export archive.
rm "$EXPORT_FILE"

How to interpret the result

1. Verify the protected backup folder on the server.
   Replace BACKUP_DIR with the exact folder path printed in step 7.

   The server should show the selected backup folder and list files such as:
   - database.dump
   - filestore.tar.gz
   - odoo-project-config-and-addons.tar.gz
   - nginx-site.conf
   - MANIFEST.txt

2. Create a temporary export archive.
   The export archive is created under the SSH user's home folder so it can be downloaded with scp.

   Example:
   - /home/your-user/backup-export/odoo-backup-YYYYMMDD-HHMMSS.tar.gz

3. Run scp from your local computer.
   Do not run the scp download command from the server. Run it from your laptop or workstation terminal.

   Windows PowerShell example:
   - scp your-user@your-server-ip:/home/your-user/backup-export/odoo-backup-YYYYMMDD-HHMMSS.tar.gz C:\Users\YourName\Backups\

4. Confirm the file exists on your local computer.
   After download, check your local backup folder and confirm the archive is present.

5. Delete the temporary export from the server after download.
   Keep the original protected backup in /opt/odoo-backups, but remove the temporary readable export archive from the user's home folder.

Why this extra export step is useful:
- /opt/odoo-backups may be protected with root-only permissions
- direct scp from /opt/odoo-backups can fail
- the temporary archive lets you download the backup without weakening backup folder permissions

Important:
Keep at least one protected copy on the server for quick rollback and at least one copy off the server. For sensitive production systems, use encrypted storage. Do not store unencrypted production backups in public buckets, shared folders, or insecure drives. Backups can contain customer data, employee data, invoices, passwords, API keys, and private business information.

13. Verify backup integrity

Safe check

A backup file that exists is not automatically usable. Basic checks help catch empty files, missing parts, or corrupt archives.

Show command and interpretation

Command or manual check

BACKUP_ROOT="/opt/odoo-backups"

# Use the exact backup folder printed by step 7.
# Replace this value with the real folder created for this backup run.
BACKUP_DIR="/opt/odoo-backups/YYYYMMDD-HHMMSS"

echo "=== Using backup folder ==="
echo "$BACKUP_DIR"

if [ ! -d "$BACKUP_DIR" ]; then
  echo "Backup folder not found. Run step 7 first, then paste the printed path into BACKUP_DIR."
  exit 1
fi

echo
echo "=== Backup files ==="
sudo ls -lh "$BACKUP_DIR"

echo
echo "=== Check required backup files exist ==="
for file in database.dump filestore.tar.gz odoo-project-config-and-addons.tar.gz MANIFEST.txt; do
  if sudo test -s "$BACKUP_DIR/$file"; then
    echo "OK: $file exists and is not empty"
  else
    echo "MISSING OR EMPTY: $file"
  fi
done

echo
echo "=== Test filestore archive ==="
sudo tar -tzf "$BACKUP_DIR/filestore.tar.gz" | head -20

echo
echo "=== Test config/addons archive ==="
sudo tar -tzf "$BACKUP_DIR/odoo-project-config-and-addons.tar.gz" | head -30

echo
echo "=== Test PostgreSQL dump metadata ==="
sudo cat "$BACKUP_DIR/database.dump" | sudo docker exec -i odoo-secure-db pg_restore -l | head -20

echo
echo "=== Show manifest ==="
sudo cat "$BACKUP_DIR/MANIFEST.txt"

How to interpret the result

Good signs:
- required files exist and are not empty
- filestore.tar.gz can be listed with tar -tzf
- odoo-project-config-and-addons.tar.gz can be listed with tar -tzf
- pg_restore -l can read the database dump metadata
- metadata shows the expected Odoo database name
- MANIFEST.txt exists and describes the backup

Replace BACKUP_DIR with the exact folder path printed in step 7. This avoids verifying the wrong backup folder.

Expected required files:
- database.dump
- filestore.tar.gz
- odoo-project-config-and-addons.tar.gz
- MANIFEST.txt

Optional but recommended:
- nginx-site.conf if Odoo is exposed through Nginx/HTTPS

How to interpret common results:

1. "Permission denied"
   The backup folder is probably protected with root-only permissions.
   Use sudo to list, read, and test backup files.
   Do not weaken backup folder permissions just to make commands easier.

2. tar archive cannot be listed
   The archive may be missing, incomplete, or corrupted.
   Recreate that part of the backup before trusting it.

3. pg_restore cannot read database.dump
   The database dump may be invalid or incomplete.
   Recreate the PostgreSQL dump and test again.

4. File exists but size is 0
   The backup command likely failed.
   Do not trust an empty backup file.

Important:
These checks prove that the backup files can be read at a basic level. This still does not replace a real restore test in a separate sandbox.

14. Test restore on a separate sandbox

Change required

The only reliable proof of a backup is a successful restore test. Never test restore first on production.

Show command and interpretation

Command or manual check

This step should be done in a separate sandbox, not on production.

High-level restore test:

1. Prepare a separate sandbox server or separate Docker project.
2. Restore PostgreSQL dump into a test database.
3. Restore the filestore under the matching database name.
4. Restore custom addons and config.
5. Start Odoo against the restored database.
6. Log in and verify key records, attachments, images, invoices, and custom modules.
7. Document the restore steps and issues found.

Related guide:
- /guides/odoo-sandbox-environment

How to interpret the result

A restore test should confirm:
- Odoo starts
- users can log in
- key records exist
- attachments and images load
- custom modules are available
- reports and important workflows work
- integrations are disabled or safely sandboxed

During sandbox restore, PostgreSQL ownership or ACL differences may require restore options such as --no-owner or --no-acl, especially if the sandbox uses a different database role than production. Document any restore options needed so the process is repeatable.

If you do not have a sandbox yet:
- stop here
- keep the backup safely stored
- create a sandbox first
- then come back and perform the restore test

A backup that has never been restored is only an assumption.

Current status after file-level verification:
- backup files exist
- archives can be read
- database dump metadata can be read
- off-server copy can be created
- full restore is still not proven until tested in a sandbox

15. Automate backups carefully

Caution

Automated backups are useful, but they can create a false sense of safety if they are not monitored, rotated, and tested.

Show command and interpretation

Command or manual check

A practical automation plan:

- run backup daily or according to business risk
- store backups in timestamped folders
- copy backups off-server
- keep a retention policy
- send success/failure alerts
- monitor disk usage
- periodically restore-test a backup
- protect backups containing secrets or personal data

How to interpret the result

Avoid:
- silent backup failures
- backups filling the server disk
- backups stored only on the same VPS
- backups containing secrets without protection
- no restore testing
- no notification when backup fails

For production, automation should include alerting and a documented restore process.

Final verification

  • The PostgreSQL database dump exists and can be read by pg_restore.
  • The Odoo filestore is included and matches the database name.
  • Custom addons are included or documented with their source and version.
  • docker-compose.yml, odoo.conf, .env, and Nginx config are backed up securely.
  • The backup has a manifest explaining what it contains.
  • At least one copy exists off the server.
  • Backup permissions protect secrets and customer data.
  • A restore test has been performed on a separate sandbox.
  • Backup automation includes monitoring, retention, and failure alerts.

Need a tested Odoo backup setup?

If your current Odoo backup is only a database dump, or if you are not sure whether your filestore, custom addons, configuration files, and secrets are included, I can help review or improve the backup process.

A reliable setup should include the PostgreSQL database, filestore, custom addons, deployment configuration, off-server copies, a manifest, and a restore test plan.

Contact me for Odoo backup setup →

Related guides

Backups are part of a wider safe Odoo deployment process. Harden the server first, then expose Odoo through HTTPS carefully.