--- title: Odoo 18 on Docker url: https://blog.guigpap.com/en/infrastructure/odoo-18-setup/ url_md: https://blog.guigpap.com/en/infrastructure/odoo-18-setup.md category: infrastructure date: '2026-01-19' maturite: production techno: - odoo - docker - postgresql application: - business - infrastructure --- # Odoo 18 on Docker > Deploying Odoo 18 Community with PostgreSQL and custom modules (project_github_sync, website_experiments) ## 1. What? — Definition and context **Odoo** is an open-source ERP (Enterprise Resource Planning) that centralises business data into a single database. This Odoo 18 Community instance runs on Docker, on its own dedicated PostgreSQL, with three custom modules that adapt the ERP to the technical workflow: GitHub sync, Claude Code telemetry, and web experiments for the website. > **Note - ERP** > > An **ERP** replaces the sprawl of spreadsheets and disparate tools with a unified database. Projects, contacts, invoices, time spent: everything is interconnected and traceable from one place. ### Modules in use **Native** modules enabled: | Module | Usage | |--------|-------| | **Project** | Projects and tasks | | **Timesheets** | `account.analytic.line` — fed by TimeTrackr | | **Website** | Landing page and hub | | **CRM** | Leads and conversion (to be extended) | **Custom** modules maintained in `odoo-stack/addons/`: | Module | Version | Usage | |--------|---------|-------| | `project_github_sync` | v18.0.5.6.0 | GitHub ↔ tasks sync + Claude Code telemetry + AI double-triage + estimation | | `website_experiments` | v18.0.1.1.0 | Interactive snippets (Flip Display, Glass Splat) for the Website Builder | | `powered_by_odoo_remove` | third-party | Removes the "Powered by Odoo" branding on public pages | > **Caution - Database name: `guig_db`** > > The PostgreSQL database is called **`guig_db`** (not `odoo` like the image default). Every `pg_dump`, `psql`, `--database`, `-d` command must use it. The historical filestore stays under `/var/lib/odoo/filestore/odoo/` because it was created under the previous DB name — do not rename it or `ir_attachment` references will break. ### Business vision ```mermaid flowchart TD Source["CV / business card · tracked link"] Site["Odoo Website"] Form["Contact form"] Cal["Cal.com booking"] Lead["Odoo CRM · Lead"] Deal["Project + Quote · estimated via x_estimated_hours / x_complexity"] Code["GitHub issue · synced via project_github_sync"] Claude["Claude Code · x_claude_* telemetry"] Source --> Site Site --> Form Site --> Cal Form --> Lead Cal --> Lead Lead --> Deal Deal --> Code Code --> Claude ``` To understand why Odoo was chosen over the alternatives, see [Why Odoo](/en/infrastructure/why-odoo/). --- ## 2. Why? — Stakes and motivations ### Problems solved | Problem | Odoo solution | |---------|---------------| | **Scattered data** | Unified base: projects, time, contacts | | **No GitHub ↔ task traceability** | Two-way sync via `project_github_sync` | | **Quote estimation by guesswork** | `x_estimated_hours` / `x_complexity` fields + real `actual_hours` from timesheets | | **Manual issue triage** | AI double-triage (`x_ai_project_triaged_at` + `x_ai_personal_triaged_at`) | | **Invisible Claude Code effort** | Per-session telemetry (`project.task.claude.session`) with cost, tokens, LOC, TLDR, category | | **Fuzzy marketing attribution** | Source tracking via Website (CV, business card, LinkedIn) | ### Why Odoo 18 Community (vs Enterprise)? 1. **Cost** — Enterprise is billed per user/month. Community is free and self-hosted. 2. **Custom modules unrestricted** — No restriction on installing `project_github_sync` or `website_experiments` on Community. 3. **Functional sufficiency** — The native Project, Timesheets, Website and CRM modules cover current needs. 4. **Migration possible** — To Enterprise later if advanced modules (eCommerce, Manufacturing) are needed. > **Tip - Data-driven strategy** > > End-to-end traceability (lead → project → GitHub issue → Claude + human time spent → real cost) lets future quotes be calibrated on data rather than rough estimates. --- ## 3. How? — Technical implementation ### Network architecture ```mermaid flowchart TD Caddy["Caddy · webproxy · odoo.guigpap.com:443"] subgraph Internal["odoo-internal"] direction LR Odoo["Odoo 18 · :8069 · Web UI · XML-RPC · /mnt/extra-addons"] PG["odoo-postgres · :5432 · DB guig_db"] Odoo <--> PG end Caddy -->|port 8069| Internal ``` ### Docker Compose configuration (excerpt) ```yaml services: odoo: image: odoo:18 volumes: - odoo-data:/var/lib/odoo - odoo-config:/etc/odoo - ./addons:/mnt/extra-addons environment: - HOST=odoo-postgres - USER=odoo - PASSWORD=${ODOO_DB_PASSWORD} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8069/web/health"] interval: 30s timeout: 10s retries: 3 networks: [odoo-internal, webproxy] odoo-postgres: image: postgres:15 volumes: [odoo-db:/var/lib/postgresql/data] environment: - POSTGRES_USER=odoo - POSTGRES_PASSWORD=${ODOO_DB_PASSWORD} - POSTGRES_DB=guig_db networks: [odoo-internal] ``` > **Tip - Health check** > > The `/web/health` endpoint returns `{"status": "pass"}` when Odoo is healthy. Used by Docker for container health and the [Monitoring Stack](/en/infrastructure/monitoring-stack/) for alerts. ### `project_github_sync` module (v18.0.5.6.0) The module adds many fields to `project.task` (plus a dedicated model for Claude Code session history). The fields fall into four families. #### GitHub fields (8) | Field | Type | Usage | |-------|------|-------| | `x_github_sync_enabled` | Boolean (on `project.project`) | Enables sync on the project | | `x_github_repo` | Char | `owner/repo` source | | `x_github_issue_id` | Integer | Issue / PR number | | `x_github_url` | Char | Canonical GitHub URL | | `x_github_commit_ids` | One2many → `project.task.commit` | Linked commits (sha, message, author, url) | | `x_github_commit_count` | Integer (compute) | Number of linked commits | | `x_github_milestone_id` | Integer | GitHub milestone number | | `x_github_parent_issue_id` | Integer | Parent issue (sub-issues) | #### Claude Code telemetry fields (13) Fed by the SessionEnd hooks (see [Claude Code Telemetry](/en/workflows/claude-code-telemetry/)). | Field | Type | Usage | |-------|------|-------| | `x_claude_time_total` | Float (h) | Aggregated Claude Code time | | `x_claude_cost_total` | Float ($) | Aggregated cost | | `x_claude_token_total` | Integer | Total tokens (in + out + cache) | | `x_claude_tokens_input` / `_output` / `_cache` | Integer × 3 | Per-category detail | | `x_claude_sessions` | Integer | Number of sessions | | `x_claude_lines_added` / `_removed` | Integer × 2 | Aggregated LOC | | `x_claude_last_session` | Datetime | Last recorded session | | `x_claude_cost_by_model` | Text (JSON) | Breakdown per model (Sonnet/Opus/Haiku) | | `x_claude_session_ids` | One2many → `project.task.claude.session` | Session-by-session history | | `x_claude_category` | Selection | Auto category for generic tasks (no issue) | Each `project.task.claude.session` row captures the detail of a session: ID, timestamps, tokens, cost, LOC, **TLDR** (short summary generated by the hook) and category. This lets us drill from the aggregate down to the source session. #### Effort estimation fields (#188) | Field | Type | Usage | |-------|------|-------| | `x_estimated_hours` | Float | A priori estimate | | `x_complexity` | Selection | Complexity (low / medium / high) | The **ground truth** comes from timesheets: `actual_hours = SUM(account.analytic.line.unit_amount) WHERE task_id = X`. The `x_claude_time_total` attribute is **not** a substitute — it measures Claude machine time, not human effort. See the `Effort Estimator V1` workflow for the cohort-based estimation logic. ```sql -- Paired samples (estimate + actual) for re-calibration SELECT t.id, t.name, t.x_estimated_hours, COALESCE(SUM(a.unit_amount), 0) AS actual_hours, t.x_claude_time_total, t.x_claude_sessions FROM project_task t LEFT JOIN account_analytic_line a ON a.task_id = t.id WHERE t.x_estimated_hours IS NOT NULL GROUP BY t.id, t.name, t.x_estimated_hours, t.x_claude_time_total, t.x_claude_sessions HAVING COALESCE(SUM(a.unit_amount), 0) > 0; ``` #### AI double-triage fields (#296) | Field | Type | Usage | |-------|------|-------| | `x_ai_project_triaged_at` | Datetime | Cache of last project-side triage (stage) | | `x_ai_personal_triaged_at` | Datetime | Cache of last personal-horizon triage (Today / This week / …) | The N8N workflow `Daily GitHub Issue Triage` proposes stage + horizon assignments via `codex-yolo`, then stores the timestamp to skip re-triaging for **7 days** (configurable TTL). The user validates via Telegram (`/triage`), which POSTs to an internal API that updates Odoo. ```mermaid flowchart TD Cron["Daily Trigger / SessionStart hook"] Fetch["Fetch issues without triage cache or expired"] AI["AI Issue Triage SW · codex-yolo"] Cache["Update x_ai_*_triaged_at"] Pending["triage-pending.json"] TG["Interactive triage via /triage"] API["triage-interactive-api · POST update"] Odoo["Odoo · stage_id + x_personal_stage_id"] Cron --> Fetch --> AI --> Cache AI --> Pending --> TG --> API --> Odoo ``` ### `website_experiments` module Adds three drag-and-drop snippets to the Website Builder: - **Flip Display**: animated split-flap display (airport-style). - **Glass Splat Desktop**: 3 side-by-side panels with a fragmented-glass 3D render. - **Glass Splat Mobile**: equivalent mobile carousel. Asset side: CSS + IIFE JavaScript loaded through `web.assets_frontend`. ### Filestore and `ir_attachment` The filestore lives in the `odoo-data` volume, under `/var/lib/odoo/filestore/odoo/` (sub-folder named `odoo` historically, even though the DB is now `guig_db`). It stores every binary referenced by `ir_attachment.store_fname`. > **Caution - Don't blindly clean the filestore** > > Any unverified purge breaks the website rendering. Four critical categories: > - `/_custom/` URLs → theme SCSS assets (broken pages if removed). > - `/web/content/` → documents embedded in pages. > - `res_model = 'ir.ui.view'` → website building blocks. > - `res_model = 'ir.attachment'` + `type = 'binary'` → downloadable files. ### Backup Backups are handled by `scripts/backup-databases.sh` (daily cron at 03:00) which produces: - `pg_dump -U odoo guig_db` → SQL archive - `tar czf - -C /var/lib/odoo filestore` → filestore archive - Local rotation (7 d) + GDrive push (30 d) via rclone. ```bash # Manual backup docker exec odoo-postgres pg_dump -U odoo guig_db > odoo_backup.sql # Restore docker exec -i odoo-postgres psql -U odoo guig_db < odoo_backup.sql # Manual filestore backup docker exec odoo tar czf - -C /var/lib/odoo filestore > odoo-filestore.tar.gz ``` ### N8N integration via XML-RPC ```javascript // Task creation from github-project-sync { "url": "http://odoo:8069/xmlrpc/2/object", "method": "POST", "body": { "service": "object", "method": "execute_kw", "args": [ "guig_db", // database (NOT "odoo") 2, // uid after authentication "{{ $env.ODOO_PASSWORD }}", "project.task", "create", [{ "name": "Issue #123 — Fix bug", "project_id": 1, "x_github_issue_id": 123, "x_github_url": "https://github.com/owner/repo/issues/123" }] ] } } ``` > **Note - XML-RPC** > > **XML-RPC** is Odoo's native protocol for programmatic access. Older than REST but very well supported on the N8N side (a single HTTP Request node is enough). Prefer it over `/jsonrpc` endpoints for consistency with existing workflows. ### Module management ```bash # Install a new module cp -r my_module odoo-stack/addons/ docker restart odoo # UI: Apps → Update Apps List → Install # Update an existing module in place docker exec odoo odoo -u project_github_sync -d guig_db --stop-after-init ``` ### Troubleshooting commands ```bash # Health check curl http://localhost:8069/web/health # Odoo logs docker logs odoo --tail 100 # PostgreSQL connection docker exec odoo-postgres pg_isready -U odoo docker exec odoo-postgres psql -U odoo -d guig_db -c "SELECT count(*) FROM project_task;" # Odoo shell for debug docker exec -it odoo odoo shell -d guig_db # Look at recent Claude sessions docker exec odoo-postgres psql -U odoo -d guig_db \ -c "SELECT id, name, tldr, category FROM project_task_claude_session ORDER BY id DESC LIMIT 10;" ``` --- ## 4. What if? — Perspectives and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|------------| | **Community without vendor support** | No Odoo hotline | Documentation + community | | **Single PostgreSQL instance** | No native HA | Daily backups + monitoring | | **Custom modules to maintain** | Future Odoo 19+ compatibility | Versioned migrations in `migrations/` | | **Effective single user** | All telemetry is attributed to a single employee | Multi-user mapping planned via `timetrackr_user_mapping` if needed | ### Evolution scenarios **If the estimation pipeline becomes critical**: - Calibrate the V1 → V2 model on paired samples (`x_estimated_hours` vs `actual_hours`). - Extend telemetry to issue-less tasks (already addressed via `x_claude_category`). **If AI triage gets noisy**: - Lower the cache TTL (currently 7 d) to re-triage more often. - Filter at the workflow level by project or label. **If invoicing is needed**: - Activate the Invoicing module. - Connect `account.analytic.line` to invoices via tasks/projects. **If migrating to Enterprise**: - Audit custom fields and existing migrations. - Verify `project_github_sync` compatibility (LGPL-3, should be fine). - Budget: per-user/month pricing. --- ## Related pages ### Infrastructure - [VPS Architecture](/en/infrastructure/architecture-vps/) — Overview - [Why Odoo](/en/infrastructure/why-odoo/) — ERP comparison and choice - [TimeTrackr Stack](/en/infrastructure/timetrackr-stack/) — Source of `account.analytic.line` ### Workflows - [GitHub-Odoo Sync](/en/workflows/github-odoo-sync/) — Issues / PRs / commits sync - [Claude Code Telemetry](/en/workflows/claude-code-telemetry/) — SessionStart / SessionEnd hooks - [Cal.com → Odoo bookings](/en/workflows/cal-rdv-odoo/) — Calendar integration ### Reference - [Glossary](/en/reference/glossary/) — ERP, XML-RPC, ir_attachment ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0