This repository contains a custom Odoo 10.0 Docker image with a robust bootstrap/entrypoint system designed for long-running environments and legacy Odoo installations.
The image is intended for users who need:
- Odoo 10 (Python 2.7, Node 6)
- Git-aggregated source code
- Repeatable container bootstrapping
- Explicit control over upgrades and initialization
ghcr.io/<org>/odoo-10.0
Note: Replace <org> with your organization name (e.g., nuobit, mycompany, etc.)
1.0.0,1.0.1, etc. — immutable release versionsedge— latest build from main branch (moving tag, may break)
Recommended: always pin to a specific numbered tag in production (e.g., 1.0.0).
Tag strategy:
- Numbered versions (
1.0.0): Stable, tested releases that never change edge: Bleeding edge, rebuilt on every commit - use only for testing
- Base OS: Debian Stretch (EOL, using
archive.debian.org) - Python: 2.7
- Node.js: 6.x
- wkhtmltopdf: 0.12.1.4 (static Debian package)
- User: non-root (
odoo, uid99910) - Healthcheck: HTTP check on
/web/database/selectorevery 120s
This image is legacy by design and intended for environments that must keep Odoo 10 running.
The container uses a fixed UID 99910 for the odoo user, deliberately hardcoded in both the Dockerfile and docker-compose configuration.
Instead of letting Docker auto-assign UIDs, we explicitly control the user ID to ensure:
- Predictable ownership: Files created by the container always have the same UID across all environments
- Dockerfile-compose consistency: The UID in the image matches the UID specified in
user:directive - Manageable permissions: Administrators know exactly which UID to
chownon host directories
- Arbitrary but intentional: We chose a high, arbitrary UID outside typical ranges
- High UID range (90000+): Avoids collision with:
- System users (typically < 1000)
- Regular host users (typically 1000-60000)
- Service users (typically 60000-90000)
- Version indicator: The
10suffix represents Odoo version 10, making it identifiable - Consistent across deployments: Same UID in development, staging, and production
Without a fixed UID, Docker might assign UID 1000 to the container user. If your host user also has UID 1000:
- Files created by the container appear to belong to your host user
- Your host user can accidentally modify container-owned files
- Permissions become confusing and unpredictable
- Different environments might get different UIDs, breaking reproducibility
With a fixed high UID (99910), there's no collision, and ownership is always clear.
You do NOT need to create this user on the host. Linux uses numeric UIDs, not usernames. The container's odoo user (UID 99910) can access files owned by UID 99910 on the host, regardless of username.
Set ownership on host bind-mounted directories:
# Example for a container (could be odoo10-1, odoo10-customer1, etc.)
sudo chown -R 99910:99910 /srv/docker/data/odoo10-1Restrict permissions for security:
sudo chmod -R o-rwx /srv/docker/data/odoo10-1Note: Both the container name (odoo10-1) and host path (/srv/docker/data/) are arbitrary examples. You can use any naming convention and path structure that suits your environment:
- Container:
odoo10-customer1,odoo10-prod, etc. - Host path:
/srv/docker/data/,/opt/containers/,/home/user/docker/, etc.
You can run multiple Odoo 10 containers on the same host (e.g., odoo10-1, odoo10-2, odoo10-customer1), all using the same UID 99910.
This is NOT a problem as long as:
- ✅ Each container has its own separate bind-mounted directories
- ✅ Directories are named clearly to match the container (e.g.,
/srv/docker/data/odoo10-1,/srv/docker/data/odoo10-2)
What to avoid:
- ❌ Never share the same data directory between multiple containers
- ❌ Don't bind
/srv/docker/data/odoo-sharedto bothodoo10-1andodoo10-2
Why this works:
- Linux permissions are per-file, not per-user
- Multiple containers with UID 99910 can coexist peacefully
- Each container only accesses its own mounted directories
- File conflicts are impossible when directories are separate
Example multi-container setup:
# Container 1
sudo chown -R 99910:99910 /srv/docker/data/odoo10-1
sudo chmod -R o-rwx /srv/docker/data/odoo10-1
# Container 2 (different directory, same UID - no problem!)
sudo chown -R 99910:99910 /srv/docker/data/odoo10-2
sudo chmod -R o-rwx /srv/docker/data/odoo10-2This ensures:
- Container can read/write bind-mounted volumes
- Files have consistent ownership across environments
- No confusion with existing host users
- Other users cannot access sensitive data
- Multiple containers coexist without permission conflicts
Container internal paths:
| Container Path | Repository File | Purpose |
|---|---|---|
/opt/odoo/scripts/entrypoint.sh |
scripts/entrypoint.sh |
Container entrypoint |
/opt/odoo/scripts/lib/common.sh |
scripts/lib/common.sh |
Shared functions and variables |
/opt/odoo/scripts/bin/updatemodules |
scripts/bin/updatemodules.sh |
Update modules script |
/opt/odoo/scripts/bin/shell |
scripts/bin/shell.sh |
Interactive shell script |
/opt/odoo/scripts/bin/fetchbasereqs |
scripts/bin/fetchbasereqs.sh |
Fetch base requirements script |
/opt/odoo/scripts/bin/fetchreqs |
scripts/bin/fetchreqs.sh |
Fetch repo requirements script |
/opt/odoo/scripts/bin/fetchcode |
scripts/bin/fetchcode.sh |
Fetch git repositories script |
/opt/odoo/scripts/bin/genaddonspath |
scripts/bin/genaddonspath.py |
Generate addons_path from repos.yaml |
/opt/odoo/scripts/bin/db |
scripts/bin/db.sh |
Database management script |
/opt/odoo/scripts/bin/snapshot |
scripts/bin/snapshot.sh |
Snapshot create/restore script |
/opt/odoo/dist/defaults.env |
config/defaults.env |
Image default configuration |
/opt/odoo/dist/constraints.txt |
config/constraints.txt |
Python package version constraints |
/opt/odoo/scripts/assets/pfbfer.zip |
scripts/assets/pfbfer.zip |
ReportLab Type1 fonts archive |
Runtime directories (created at runtime or via volume mounts):
| Path | Purpose |
|---|---|
/opt/odoo/snapshots/ |
Snapshot storage directory (bind-mounted) |
/opt/odoo/config/ |
Instance configuration directory (bind-mounted) |
/opt/odoo/config/odoo.conf |
Odoo configuration file — at minimum set db_host, db_user, db_password and admin_passwd |
/opt/odoo/config/repos.yaml |
Git-aggregator config — contains all OCA repos with a 10.0 branch and actual Odoo modules (see Localization repos) |
/opt/odoo/config/settings.env |
Instance-level overrides (optional) |
/opt/odoo/src |
Odoo source code (git-aggregated, typically volume-mounted) |
/var/lib/odoo |
Odoo data directory (filestore, sessions, typically volume-mounted) |
Note: Paths shown are inside the container. Use volume mounts in docker-compose to map host directories to container paths.
The container uses a stateful bootstrap mechanism:
- On first container start:
- fetches base Python requirements
- fetches git repositories using
git-aggregator - generates
addons_pathfrom repos.yaml and updatesodoo.conf - fetches Odoo Python requirements
- installs ReportLab Type1 fonts
- On subsequent restarts:
- skips bootstrap
- runs Odoo directly
Bootstrap state is stored in:
/var/lib/odoo/.bootstrap/
These scripts can be executed inside the running container:
| Script | Description | Usage |
|---|---|---|
db |
Database management | docker compose exec <container> db <command> [args...] |
snapshot |
Snapshot create/restore | docker compose exec <container> snapshot <command> [args...] |
updatemodules |
Update modules | docker compose exec <container> updatemodules <database> <all|module_list|changed> |
shell |
Interactive Odoo shell | docker compose exec <container> shell <database> |
fetchbasereqs |
Fetch base requirements | docker compose exec <container> fetchbasereqs |
fetchreqs |
Fetch repo requirements | docker compose exec <container> fetchreqs <repo_path> |
fetchcode |
Fetch git repositories | docker compose exec <container> fetchcode [addon_path] [jobs] |
The db script provides database management commands:
# Create a database (with unaccent extension):
docker compose exec <container> db create <database>
# Initialize Odoo base module in database:
docker compose exec <container> db init <database>
docker compose exec <container> db init <database> demo # with demo data
# Import a SQL dump (reads from stdin, use -T flag):
bzcat dump.sql.bz2 | docker compose exec -T <container> db import <database>
zcat dump.sql.gz | docker compose exec -T <container> db import <database>
unzip -p dump.sql.zip | docker compose exec -T <container> db import <database>
7z x -so dump.sql.7z | docker compose exec -T <container> db import <database>
cat dump.sql | docker compose exec -T <container> db import <database>
# Drop a database:
docker compose exec <container> db drop <database>
# Reset (drop and recreate) a database:
docker compose exec <container> db reset <database>
# Block/unblock connections (block also terminates existing connections):
docker compose exec <container> db block all <database> # block everyone (including superusers)
docker compose exec <container> db block users <database> # block regular users only
docker compose exec <container> db unblock all <database> # unblock everyone
docker compose exec <container> db unblock users <database> # unblock regular users
# Terminate all active connections:
docker compose exec <container> db terminate <database>
# List databases owned by DB_OWNER:
docker compose exec <container> db list
# List PostgreSQL users:
docker compose exec <container> db users
# Create/drop the DB owner user:
docker compose exec <container> db createuser
docker compose exec <container> db dropuserThe drop and reset commands automatically handle active connections before dropping a database:
- Block all new connections — sets
datallowconn = falseon the database, preventing anyone (including superusers) from opening new connections - Terminate existing connections — kills all active backends connected to the database
- Drop the database
This eliminates the race condition where new connections could sneak in between termination and the actual drop. You do not need to manually stop Odoo or disconnect clients — the script handles it for you.
The import command reads a SQL dump from stdin and pipes it to psql. Decompress on the host side and pipe through docker compose exec -T:
# bzip2
bzcat dump.sql.bz2 | docker compose exec -T <container> db import <database>
# gzip
zcat dump.sql.gz | docker compose exec -T <container> db import <database>
# zip
unzip -p dump.sql.zip | docker compose exec -T <container> db import <database>
# 7z
7z x -so dump.sql.7z | docker compose exec -T <container> db import <database>
# plain SQL
cat dump.sql | docker compose exec -T <container> db import <database>Important: Always use the -T flag with docker compose exec when piping stdin — without it, Docker allocates a TTY which corrupts the data stream.
The database must already exist (use db create first). During import, user connections are blocked and existing connections are terminated to prevent interference.
The block and unblock subcommands control who can connect to a database:
| Command | SQL effect | Who's blocked |
|---|---|---|
db block all <dbname> |
datallowconn = false + terminate |
Everyone (including superusers) |
db block users <dbname> |
CONNECTION LIMIT 0 + terminate |
Regular users only (superusers can still connect) |
db unblock all <dbname> |
datallowconn = true |
Nobody |
db unblock users <dbname> |
CONNECTION LIMIT -1 |
Nobody (unlimited) |
db terminate <dbname> |
Terminates all active backends | N/A (kills existing connections only) |
Both block commands also terminate all existing connections after setting the limit, ensuring no stale sessions remain.
Use block users / unblock users when you need to prevent regular users from connecting while keeping superuser access for maintenance. Use all for a complete lockout (e.g., before dropping a database). Use terminate when you just need to kill active connections without changing connection limits.
Internally, these dispatch to two low-level helpers that can also be called directly in scripts:
| Helper | Purpose |
|---|---|
do_set_datallowconn <dbname> <true|false> |
Controls whether any connections are allowed |
do_set_user_connection_limit <dbname> <limit> |
Sets non-superuser connection limit (-1 = unlimited, 0 = blocked, N = max N) |
do_terminate_all_connections <dbname> |
Kills all active backends on the database |
do_terminate_all_connections excludes its own psql session (pg_backend_pid()) to avoid self-termination — this matters when the target database name matches DB_PGDB (e.g., both are postgres).
Connection settings are read from odoo.conf (db_host, db_user, db_password). Admin credentials come from defaults.env (or overridden in settings.env):
| Variable | Source | Description |
|---|---|---|
DB_HOST |
db_host in odoo.conf |
PostgreSQL host |
DB_PGUSER |
defaults.env |
PostgreSQL admin user |
DB_PGDB |
defaults.env |
PostgreSQL maintenance database |
DB_PGUSER_PASSWORD |
settings.env |
PostgreSQL admin password (prompted if unset) |
CONFIRM_LEVEL |
defaults.env |
Confirmation level: all, deletions, none |
Options go after the subcommand: db drop -f mydb.
| Short | Long | Description | Applies to |
|---|---|---|---|
-f |
--force |
Skip confirmation prompts (sets CONFIRM_LEVEL=none) |
drop, reset |
Interactive shell access:
docker compose exec -it <container> bashRunning Python scripts via stdin:
When piping a script to shell, use the -T flag:
docker compose exec -T <container> shell <database> < myscript.pyThe snapshot script provides named create and restore of a complete Odoo environment state: database (schema + data) and filestore (attachments, images, reports). It is designed for development workflows like "save state before testing something destructive" and for creating portable copies of an environment.
Note: The existing db import command (SQL-only, stdin-based) remains unchanged and serves a different purpose — importing external SQL dumps. snapshot is for local named create/restore workflows with full environment state (DB + filestore).
# Save current state before a risky operation
docker compose exec <container> snapshot create mydb before-migration
# Save with a note
docker compose exec <container> snapshot create -n "before upgrading account module" mydb before-migration
# Save database only (skip filestore)
docker compose exec <container> snapshot create --no-filestore mydb quick-save
# List available snapshots
docker compose exec <container> snapshot list
# Restore to the original database (dbname from metadata)
docker compose exec <container> snapshot restore before-migration
# Restore to a different database name
docker compose exec <container> snapshot restore before-migration mydb-test
# Delete a snapshot
docker compose exec <container> snapshot remove before-migration
# Skip confirmation prompts
docker compose exec <container> snapshot restore -f before-migration mydb
docker compose exec <container> snapshot remove -f old-snapshot
# Verbose output (show pg_dump/pg_restore/rsync progress)
docker compose exec <container> snapshot create -v mydb before-migration
docker compose exec <container> snapshot restore -v before-migration| Command | Description |
|---|---|
create [-v|--verbose] [-j|--jobs N] [-n|--note "text"] [--no-filestore] <dbname> <snapshot-name> |
Create a snapshot of DB + filestore |
restore [-f|--force] [-v|--verbose] [-j|--jobs N] <snapshot-name> [dbname] |
Restore a named snapshot into a database |
list |
List available snapshots with metadata |
remove [-f|--force] <snapshot-name> |
Delete a snapshot |
Argument order follows the Unix cp/rsync convention: source first, destination second. On restore, dbname is optional — if omitted, it defaults to the database name recorded at backup time.
Options go after the subcommand: snapshot create -v mydb snap-name.
| Short | Long | Description | Applies to |
|---|---|---|---|
-f |
--force |
Skip confirmation prompts (sets CONFIRM_LEVEL=none) |
restore, remove |
-v |
--verbose |
Show detailed output from pg_dump, pg_restore, and rsync (default: errors only) |
create, restore |
-j |
--jobs |
Number of parallel workers for pg_dump/pg_restore (overrides SNAPSHOT_JOBS) |
create, restore |
-n |
--note |
Optional description stored in metadata | create |
--no-filestore |
Skip filestore (database only) | create |
Snapshots are stored in a dedicated bind-mounted directory (default: /opt/odoo/snapshots). Each snapshot is a directory containing a metadata.json, a db/ subdirectory (PostgreSQL directory-format dump), and optionally a filestore/ subdirectory.
The snapshot directory is a sibling of data/ on the host, allowing each to live on a different volume, disk, or partition — e.g., data/ on a fast SSD, snapshots/ on a larger HDD.
| Variable | Default | Description |
|---|---|---|
SNAPSHOT_DIR |
/opt/odoo/snapshots |
Snapshot storage directory (bind-mounted) |
SNAPSHOT_JOBS |
2 |
Parallel workers for pg_dump/pg_restore |
Both can be overridden in settings.env.
For design rationale and directory format details, see docs/snapshot.md.
This repository is for building the Docker image, not for running containers directly. To deploy an instance, copy the template files from deploy/ to your instance directory and customize them.
# Create the instance directory and copy all deploy files
cp -r deploy/* /srv/docker/stack/odoo10-1/
# Create data directories
mkdir -p /srv/docker/data/odoo10-1/{src,data,snapshots}
# Set ownership
sudo chown -R 99910:99910 /srv/docker/stack/odoo10-1/config
sudo chown -R 99910:99910 /srv/docker/data/odoo10-1Important — config directory ownership: The config/ directory (and its contents) must be owned by UID 99910 on the host. During bootstrap, the genaddonspath script writes the generated addons_path directly into odoo.conf. If the container cannot write to this file, bootstrap will fail and Odoo will start without the correct addons path.
# Required: ensure the container can write to config files
sudo chown -R 99910:99910 /srv/docker/stack/odoo10-1/configEdit the copied files for your instance:
config/odoo.conf— at minimum setdb_host,db_user,db_passwordandadmin_passwdconfig/repos.yaml— enable/disable OCA repos as neededconfig/settings.env— setDB_PGUSER_PASSWORDand any overridesdocker-compose.yml— adjust image tag, ports, network name
cd /srv/docker/stack/odoo10-1
docker compose up -dThe deploy/ folder mirrors the target instance layout:
deploy/ /srv/docker/stack/odoo10-1/
├── docker-compose.yml → ├── docker-compose.yml
└── config/ → └── config/ (bind-mounted to /opt/odoo/config/)
├── odoo.conf ├── odoo.conf
├── repos.yaml ├── repos.yaml
└── settings.env └── settings.env
Runtime data is stored separately:
/srv/docker/data/odoo10-1/
├── src/ # Source code (git-aggregated)
├── data/ # Odoo filestore, sessions, etc.
└── snapshots/ # Named snapshots (database + filestore)
/srv/docker/stack/<container>/— Configuration (version-controlled, backed up separately)/srv/docker/data/<container>/— Runtime data (large, requires regular backups)
Note: The /srv/docker/ base path and the folder names are arbitrary conventions—use any directory structure that fits your organization's standards.
services:
odoo10-1:
container_name: odoo10-1
image: ghcr.io/<org>/odoo-10.0:1.0.0
user: "99910:99910"
ports:
- "127.0.0.1:8010:8069"
volumes:
- /srv/docker/stack/odoo10-1/config:/opt/odoo/config
- /srv/docker/data/odoo10-1/src:/opt/odoo/src
- /srv/docker/data/odoo10-1/data:/var/lib/odoo
- /srv/docker/data/odoo10-1/snapshots:/opt/odoo/snapshots
restart: unless-stopped
mem_limit: 4g
networks: [pg96-1-net]
networks:
pg96-1-net:
external: trueBaked into the image at /opt/odoo/dist/defaults.env (from config/defaults.env in the repo). Can be overridden by instance settings.env.
| Variable | Default | Description |
|---|---|---|
PYTHON_BIN |
/usr/bin/python |
Python interpreter |
DIST_DIR |
(auto) | Image dist directory (/opt/odoo/dist/) |
DIST_CONSTRAINTS |
${DIST_DIR}/constraints.txt |
pip constraints file |
INSTANCE_DIR |
${HOME}/config |
Instance config directory (bind-mounted) |
INSTANCE_SETTINGS |
${INSTANCE_DIR}/settings.env |
Instance overrides file |
INSTANCE_REPOS |
${INSTANCE_DIR}/repos.yaml |
Git-aggregator config |
SRC_DIR |
${HOME}/src |
Source code directory |
SRC_ODOO_REPO_DIR |
odoo |
Odoo repo directory name (excluded from generated addons_path) |
ODOO_DIR |
${SRC_DIR}/odoo |
Odoo source directory |
ODOO_BIN |
${ODOO_DIR}/odoo-bin |
Odoo executable |
ODOO_CONF |
${INSTANCE_DIR}/odoo.conf |
Odoo config file |
ODOO_DATA_DIR |
/var/lib/odoo |
Odoo data directory |
DB_PGUSER |
postgres |
PostgreSQL admin user |
DB_PGDB |
postgres |
PostgreSQL maintenance database |
CONFIRM_LEVEL |
all |
Confirmation level: all, deletions, none |
SNAPSHOT_DIR |
/opt/odoo/snapshots |
Snapshot storage directory (bind-mounted) |
SNAPSHOT_JOBS |
2 |
Parallel workers for pg_dump/pg_restore |
Bind-mounted at /opt/odoo/config/settings.env. Sourced after defaults.env, overrides any value. Changes take effect on next script execution or container restart — no image rebuild or container recreation needed.
A reference template is available in the deploy/ directory of the project repository.
| Variable | Description |
|---|---|
DB_PGUSER_PASSWORD |
PostgreSQL admin password (prompted if unset) |
CONFIRM_LEVEL |
Confirmation level: all, deletions, none |
docker build -t odoo-10.0:local .Note: Replace <org> with your organization name in all commands below.
# Build with edge tag
docker build -t ghcr.io/<org>/odoo-10.0:edge .
# Push to registry
docker push ghcr.io/<org>/odoo-10.0:edge# Build with version tag (always 3 numbers: MAJOR.MINOR.PATCH)
docker build -t ghcr.io/<org>/odoo-10.0:1.0.0 .
# Push the tag
docker push ghcr.io/<org>/odoo-10.0:1.0.0-
Always use semantic versioning with 3 numbers:
MAJOR.MINOR.PATCH- Example:
1.0.0,1.2.5,2.0.0 MAJOR(1st number): Breaking changes, incompatible updatesMINOR(2nd number): New features, backwards compatiblePATCH(3rd number): Bug fixes only, no new features- Never use fewer than 3 numbers - always use the format
X.Y.Z
- Example:
-
Production recommendations:
- ✅ Always pin to full 3-number version:
ghcr.io/<org>/odoo-10.0:1.0.0 - ❌ Never use
edgein production - ❌ Never use short versions like
1.0or1
- ✅ Always pin to full 3-number version:
-
Test before tagging:
- Build and test locally first
- Only push to registry after validation
- Tag releases from tested commits only
The repos.yaml file includes all OCA addon repositories that have a 10.0 branch with actual Odoo modules. Localization repos are commented out by default to avoid fetching unnecessary country-specific code.
To enable a localization, edit your instance repos.yaml and uncomment the relevant l10n-* block. For example, to enable Spanish localization:
./oca/l10n-spain:
remotes:
oca: https://github.com/OCA/l10n-spain.git
target:
oca 10.0
merges:
- oca 10.0After uncommenting, re-run fetchcode to clone the newly enabled repos.
The repos.yaml file includes a commented-out entry for OCA/OpenUpgrade. OpenUpgrade is a patched fork of Odoo that adds migration scripts for upgrading a database from a previous Odoo version (e.g. 9.0 to 10.0).
When to enable it:
- You are migrating an existing database from Odoo 9.0 (or earlier) to 10.0
- You need the OpenUpgrade migration scripts to transform data and schema
How to use:
- Uncomment the
./openupgradeblock inrepos.yaml - Run
fetchcodeto clone the OpenUpgrade repo - Run the migration following the OpenUpgrade documentation
- After a successful migration, comment it back out and restart normally with the standard Odoo/OCB source
Do not leave OpenUpgrade enabled in normal operation — it is only needed during the migration process itself.
- Debian Stretch and Python 2.7 are EOL
- TLS / CA issues may occur in restricted networks
- Internet access is required at first bootstrap (pip, fonts)
- This image is not suitable for new Odoo deployments
This image is provided as-is for legacy compatibility. Odoo is a trademark of Odoo S.A.
Use at your own risk.