Odoo Docker Volumes: Filestore, PostgreSQL and Backups

Set up Odoo Docker volumes for the filestore and PostgreSQL, then back up and restore both with docker compose exec, pg_dump and tar.

Updated . 7 min read

An Odoo database lives in two places: the PostgreSQL database and the filestore, the folder where Odoo keeps attachments, images and documents. Lose either one and the backup is useless. This guide covers Odoo Docker volumes end to end with odoo:19 and postgres:16: which paths need persistent storage, named volumes versus bind mounts, and how to back up and restore both the database and the filestore with docker compose exec, pg_dump and tar.

If you are building the stack from scratch, start with the Odoo Docker Compose guide.

What Odoo and PostgreSQL store, and where

A container's own filesystem is thrown away when the container is removed, and docker compose up recreates containers whenever the image or configuration changes. Anything that must survive goes in a volume.

Path in containerServiceWhat's in itStorage
/var/lib/postgresql/datadbAll Odoo databasesNamed volume
/var/lib/odooodooFilestore (filestore/<database>), sessions, apps installed from the Apps menuNamed volume
/mnt/extra-addonsodooYour custom and third-party modulesBind mount
/etc/odooodooodoo.confBind mount

The official Odoo image declares /var/lib/odoo and /mnt/extra-addons as volumes and uses /var/lib/odoo as its data_dir. If you change data_dir in your config, the volume's mount path must change with it.

Named volumes vs bind mounts for Odoo

Named volumes are managed by Docker. You refer to them by name, Docker decides where they live on disk, and on first use Docker copies the image's content and ownership into them. Use them for data: the database and the filestore.

Bind mounts map a folder on the host into the container. You see and edit the files directly. Use them for things you manage yourself: modules and configuration.

The Odoo image runs as the unprivileged odoo user. A named volume at /var/lib/odoo gets the right ownership automatically. A bind mount there would have the host folder's ownership, and Odoo fails with permission errors when it tries to write the filestore. That's the main reason to keep data in named volumes.

compose.yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: odoo
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  odoo:
    image: odoo:19
    depends_on:
      db:
        condition: service_healthy
        restart: true
    ports:
      - "8069:8069"
    environment:
      HOST: db
      USER: odoo
      PASSWORD: odoo
    volumes:
      - odoo-data:/var/lib/odoo
      - ./config:/etc/odoo:ro
      - ./addons:/mnt/extra-addons:ro
    restart: unless-stopped

volumes:
  db-data:
  odoo-data:

The :ro suffix mounts modules and config read-only, so nothing inside the container can change them. Leave it off ./addons if you use odoo scaffold inside the container to create modules.

Volume names and the project name

Compose prefixes volume names with the project name, which defaults to the folder name. A project in ~/odoo creates odoo_db-data and odoo_odoo-data. Check with:

docker volume ls

Renaming the folder or passing a different -p makes Compose create new, empty volumes, and Odoo appears to have lost its data. To pin the real name, set it explicitly:

compose.yaml (fixed volume names)
volumes:
  db-data:
    name: odoo-prod-db
  odoo-data:
    name: odoo-prod-filestore

To use a volume that already exists and that Compose should never create or remove, add external: true next to name.

Back up Odoo in Docker: database and filestore

A complete backup is a PostgreSQL dump plus the matching filestore folder. Run these from the folder that contains compose.yaml. Replace mydb with your database name (the one you pick in Odoo's database selector).

1. Dump the database in PostgreSQL's custom format, which is compressed and lets you restore selectively:

mkdir -p backups
docker compose exec -T db pg_dump -U odoo -Fc mydb > backups/mydb.dump

2. Archive that database's filestore folder:

docker compose exec -T odoo tar czf - -C /var/lib/odoo filestore/mydb > backups/mydb-filestore.tar.gz

-T turns off the pseudo-terminal so the binary output streams cleanly into the file on the host. Both commands run inside the existing containers, so you don't need PostgreSQL tools on the host and you don't need to know where Docker keeps the volume.

Odoo can keep writing attachments between the two steps. For a routine daily backup that window is acceptable. For a backup you'll rely on for a migration, stop Odoo first so nothing changes, and use docker compose run for the filestore step, which starts a temporary container with the same volumes:

docker compose stop odoo
docker compose exec -T db pg_dump -U odoo -Fc mydb > backups/mydb.dump
docker compose run --rm -T odoo tar czf - -C /var/lib/odoo filestore/mydb > backups/mydb-filestore.tar.gz
docker compose start odoo

Copy the backups folder off the server. A backup on the same disk as the volumes doesn't protect you from losing that disk.

Database manager backups. Odoo's own database manager at /web/database/manager can also download a zip with the dump and filestore together, protected by the master password. It's convenient for small databases but slow for large ones, and production setups usually disable the manager with list_db = False. The command-line method above works at any size.

Restore the database and filestore

To restore into the same stack, stop Odoo, create an empty database, load the dump, and unpack the filestore:

docker compose stop odoo
docker compose exec -T db createdb -U odoo mydb
docker compose exec -T db pg_restore -U odoo -d mydb --no-owner < backups/mydb.dump
docker compose run --rm -T odoo tar xzf - -C /var/lib/odoo < backups/mydb-filestore.tar.gz
docker compose start odoo

Notes:

  • createdb fails if mydb already exists. Drop it first with docker compose exec db dropdb -U odoo mydb if you mean to replace it.
  • --no-owner makes the restored objects belong to the user running the restore, which matters when the dump came from a server with a different PostgreSQL user.
  • The filestore is unpacked by the odoo user inside the Odoo image, so file ownership is correct without a chown.
  • The filestore folder name must match the database name. If you restore the dump under a new name, say mydb_copy, rename the folder too: docker compose run --rm odoo mv /var/lib/odoo/filestore/mydb /var/lib/odoo/filestore/mydb_copy. Otherwise Odoo starts but every image and attachment is missing.
  • Restore with the same Odoo major version that made the backup. Moving to a newer version is an upgrade, not a restore.

Restoring production data into a test copy? Run docker compose run --rm odoo odoo neutralize -d mydb_copy afterwards. It disables outgoing mail servers, scheduled actions and other integrations so the copy can't email customers. The Odoo dev, staging and production guide builds that into a staging workflow.

Copy a whole volume

Sometimes you want the entire /var/lib/odoo volume, for example when moving a server. Mount the volume read-only into a throwaway container and tar it:

docker run --rm -v odoo_odoo-data:/data:ro -v "$PWD/backups":/backup alpine \
  tar czf /backup/odoo-data.tar.gz -C /data .

Don't do this for the PostgreSQL volume while the database is running: copying live data files can produce a backup that won't start. Use pg_dump for the database, or stop the db service before copying its volume.

Automate it

The commands above fit in a short shell script run by cron each night: dump, archive, copy off the server, delete local copies older than a few days. Test a restore into a separate project (docker compose -p odoo-restore-test ...) on a schedule as well. If you pinned volume names with name:, give the test project its own names, or it will mount the production volumes. A backup you've never restored is a guess.

Removing volumes safely

  • docker compose down removes containers and networks but keeps named volumes. Your data survives.
  • docker compose down -v also deletes the named volumes declared in the file: every database and the whole filestore. Only use it on throwaway stacks.
  • docker volume prune removes unused anonymous volumes; add --all and it removes unused named volumes too. Check docker volume ls before running it on a server.

Keep database passwords out of compose.yaml with an env file or secrets; see Odoo Docker environment variables.

Managed backups instead

On Easy Instance, every instance is backed up automatically each day, you can take a manual backup any time, restore in one click, and download any backup to keep off-site. See Odoo backups in Easy Instance for how it works. Moving an existing Docker install? Our team helps restore your database and filestore onto an instance running the same Odoo version. You can try it free for 10 days with no credit card.

Rather not run the server yourself?

Get managed Odoo hosting with backups, SSL, monitoring and updates handled for you. Free for 10 days.