Skip to content

Odoo 18 on Docker

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.

Native modules enabled:

ModuleUsage
ProjectProjects and tasks
Timesheetsaccount.analytic.line — fed by TimeTrackr
WebsiteLanding page and hub
CRMLeads and conversion (to be extended)

Custom modules maintained in odoo-stack/addons/:

ModuleVersionUsage
project_github_syncv18.0.5.6.0GitHub ↔ tasks sync + Claude Code telemetry + AI double-triage + estimation
website_experimentsv18.0.1.1.0Interactive snippets (Flip Display, Glass Splat) for the Website Builder
powered_by_odoo_removethird-partyRemoves the “Powered by Odoo” branding on public pages

CV / business card · tracked link

Odoo Website

Contact form

Cal.com booking

Odoo CRM · Lead

Project + Quote · estimated via x_estimated_hours / x_complexity

GitHub issue · synced via project_github_sync

Claude Code · x_claude_* telemetry

To understand why Odoo was chosen over the alternatives, see Why Odoo.


ProblemOdoo solution
Scattered dataUnified base: projects, time, contacts
No GitHub ↔ task traceabilityTwo-way sync via project_github_sync
Quote estimation by guessworkx_estimated_hours / x_complexity fields + real actual_hours from timesheets
Manual issue triageAI double-triage (x_ai_project_triaged_at + x_ai_personal_triaged_at)
Invisible Claude Code effortPer-session telemetry (project.task.claude.session) with cost, tokens, LOC, TLDR, category
Fuzzy marketing attributionSource tracking via Website (CV, business card, LinkedIn)
  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.

port 8069

odoo-internal

Odoo 18 · :8069 · Web UI · XML-RPC · /mnt/extra-addons

odoo-postgres · :5432 · DB guig_db

Caddy · webproxy · odoo.guigpap.com:443

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]

The module adds many fields to project.task (plus a dedicated model for Claude Code session history). The fields fall into four families.

FieldTypeUsage
x_github_sync_enabledBoolean (on project.project)Enables sync on the project
x_github_repoCharowner/repo source
x_github_issue_idIntegerIssue / PR number
x_github_urlCharCanonical GitHub URL
x_github_commit_idsOne2many → project.task.commitLinked commits (sha, message, author, url)
x_github_commit_countInteger (compute)Number of linked commits
x_github_milestone_idIntegerGitHub milestone number
x_github_parent_issue_idIntegerParent issue (sub-issues)

Fed by the SessionEnd hooks (see Claude Code Telemetry).

FieldTypeUsage
x_claude_time_totalFloat (h)Aggregated Claude Code time
x_claude_cost_totalFloat ($)Aggregated cost
x_claude_token_totalIntegerTotal tokens (in + out + cache)
x_claude_tokens_input / _output / _cacheInteger × 3Per-category detail
x_claude_sessionsIntegerNumber of sessions
x_claude_lines_added / _removedInteger × 2Aggregated LOC
x_claude_last_sessionDatetimeLast recorded session
x_claude_cost_by_modelText (JSON)Breakdown per model (Sonnet/Opus/Haiku)
x_claude_session_idsOne2many → project.task.claude.sessionSession-by-session history
x_claude_categorySelectionAuto 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.

FieldTypeUsage
x_estimated_hoursFloatA priori estimate
x_complexitySelectionComplexity (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.

-- 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;
FieldTypeUsage
x_ai_project_triaged_atDatetimeCache of last project-side triage (stage)
x_ai_personal_triaged_atDatetimeCache 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.

Daily Trigger / SessionStart hook

Fetch issues without triage cache or expired

AI Issue Triage SW · codex-yolo

Update x_ai_*_triaged_at

triage-pending.json

Interactive triage via /triage

triage-interactive-api · POST update

Odoo · stage_id + x_personal_stage_id

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.

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.

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.
Fenêtre de terminal
# 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
// 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"
}]
]
}
}
Fenêtre de terminal
# 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
Fenêtre de terminal
# 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;"

LimitImpactMitigation
Community without vendor supportNo Odoo hotlineDocumentation + community
Single PostgreSQL instanceNo native HADaily backups + monitoring
Custom modules to maintainFuture Odoo 19+ compatibilityVersioned migrations in migrations/
Effective single userAll telemetry is attributed to a single employeeMulti-user mapping planned via timetrackr_user_mapping if needed

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.

  • Glossary — ERP, XML-RPC, ir_attachment