Skip to content

About

Base Odoo runtime image

Topics

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

odoo-docker

Base Odoo runtime image.

This repository owns the base runtime build for Odoo 19. It compiles a deterministic runtime from the upstream Odoo source, then layers in uv, PostgreSQL 17 client tools, and compatibility paths used by downstream deployment tooling. The runtime also includes the Launchplane substrate needed by Launchplane-managed Odoo lanes.

This repository provides a stable base runtime for downstream project images.

Direction and Contributions

The Director's overall DIRECTION.md sets priorities and stop boundaries. This repository has no separate DIRECTION.md. AGENTS.md is the agent-instruction entry point; .github/github.json records validation commands and repository workflow metadata.

Use the maintained executing loop for issue-backed work. Changes land through Launchplane's merge train after required checks and reviews. Image publishing follows the CI release model below; tenant runtime deployment is owned by Launchplane.

Images

  • runtime: base Odoo runtime + PostgreSQL client + uv tooling
  • runtime-devtools: runtime plus Playwright-managed Chromium test tooling

Both images default to the ubuntu user for compatibility with existing restore and SSH mount workflows.

Local Python Inspections

uv sync --locked installs the development-only dependencies for the Python helpers and native PyCharm inspections. This environment does not determine image dependencies. The Docker build excludes .venv, pyproject.toml, and uv.lock from its context.

The inspection helper prepares an ignored project model and interpreter for each worktree using .github/github.json. Shared inspection profiles, scopes, and repository-local VCS mappings remain versioned; sibling checkouts and SDK paths do not.

The runtime health addon also needs real Odoo 19 source for import resolution. Choose a clean external checkout at an exact reviewed commit, then expose that source to this worktree's interpreter before running the inspection helper:

ODOO_IDE_SOURCE=/absolute/path/to/odoo \
ODOO_IDE_COMMIT='<full-40-character-commit>' \
uv run --locked python - <<'PY'
import os
from pathlib import Path
import subprocess
import sysconfig

source = Path(os.environ["ODOO_IDE_SOURCE"]).resolve()
expected = os.environ["ODOO_IDE_COMMIT"]
assert len(expected) == 40 and all(c in "0123456789abcdef" for c in expected)
assert not source.is_relative_to(Path.cwd().resolve())
assert (source / "odoo/http.py").is_file()
assert subprocess.check_output(
    ["git", "-C", str(source), "rev-parse", "HEAD"], text=True
).strip() == expected
assert not subprocess.check_output(
    ["git", "-C", str(source), "status", "--porcelain"], text=True
)
Path(sysconfig.get_path("purelib"), "odoo-ide-source.pth").write_text(
    str(source) + "\n", encoding="utf-8"
)
PY

Keep the source checkout outside the inspected project. This supplies the actual framework API without copying upstream source or adding import stubs to the image repository.

Devtools Addon Paths

  • runtime stays runtime-first and does not write IDE-oriented Python path entries.
  • runtime-devtools writes a minimal odoo_paths.pth for generic non-core source and addon roots used in local development tooling:
    • /odoo
    • /opt/project/addons
    • /opt/extra_addons
  • runtime-devtools exposes Playwright-managed Chromium through CHROME_BIN at /usr/local/bin/chromium-playwright.

Launchplane Runtime Substrate

The image reserves /opt/launchplane/addons for image-owned Odoo addons that support Launchplane-managed runtime behavior. These addons are separate from upstream Odoo source, downstream project addons in /opt/project/addons, and shared business addons in /opt/extra_addons.

launchplane_runtime_health is loaded as a server-wide module by default. It exposes GET /launchplane/health with auth="none" and save_session=False so Launchplane can verify the exact runtime serving a public lane without depending on tenant database or Website state. The endpoint returns JSON shaped like:

{"runtime_identity":null,"status":"pass"}

When LAUNCHPLANE_RUNTIME_IDENTITY_JSON is present and valid, the parsed object is returned as runtime_identity. If the variable is present but malformed, the endpoint returns a failing response instead of echoing raw environment text.

Downstream runtime overrides must preserve /opt/launchplane/addons in the effective addons path and base,web,launchplane_runtime_health in the effective server-wide modules. /odoo/odoo-bin normalizes server-mode invocations to keep those image-owned defaults present.

Private database readiness

POST /launchplane/readiness is distinct from liveness. It accepts only direct loopback probes inside the candidate; forwarded requests and peers in separate network namespaces receive 403. A proxy sharing the candidate's network namespace must exclude this route from public ingress or preserve forwarded headers; a loopback peer alone cannot identify an unmarked local relay. Run the image-owned launchplane-readiness helper through the candidate's existing private execution channel. No new credential is needed. Responses contain only status and a fixed failure reason, use no-store, and never enumerate databases, configurations or credentials.

The versioned contract and validation are owned by controllers/readiness.py. scripts/fixtures/readiness/setup.py provides a complete isolated example. Launchplane supplies the intended Host, database and UUID, Website, expected installed/loaded modules, immutable runtime identity (artifact, image digest, source commit, slot and release), exact update receipt, and representative homepage/login/public paths with content assertions. There are no tenant route or content defaults. The request sends both the real Host and X-Odoo-Database; database filtering and Website selection must agree, with no Website fallback or session cookie. All pending install/update/removal states and Odoo's partially-updated marker fail readiness. Odoo writes that marker when module actions remain pending after an update, and registry startup consumes it to force another update.

The maintenance producer records launchplane.readiness.release in ir.config_parameter after successful exact-release updates and the committed credential boundary. Its receipt binds the full expected runtime identity and resolved update list, and attests the mail/integration fences and committed credential sanitizing. An empty update list is valid for a compatible release, but still needs a fresh release receipt. This endpoint never strips keys, updates modules, loads a registry deliberately, or changes a fence. Odoo startup/request dispatch can load the registry before any controller runs: the candidate must start only after the existing pre-registry credential boundary is committed. Launchplane owns proving those fences and starting the candidate with crons disabled (--max-cron-threads=0), isolated outbound networking and one writer owner. A receipt is an attestation by that trusted coordinator, not independent proof of host networking or arbitrary addon side effects.

Preparation and readiness are separate commands:

# Controlled preparation: can render and generate bundles/attachments.
launchplane-readiness prepare --contract /private/contract.json \
  --evidence /private/browser.json > /private/observations.json
# Observational readiness: no page fetches, warming or database writes.
launchplane-readiness check --contract /private/contract.json \
  --evidence /private/observations.json

Launchplane's controlled browser smoke provides the browser receipt, bound to the contract hash and every configured path, after successful navigation, expected content, script and response checks. The fixture's browser.py demonstrates the receipt with Chromium. Preparation first checks the candidate's database/release/fence state through readiness; only its missing-observation response permits warming. It then renders the configured pages, asserts content, and fetches referenced CSS, JS and media, including CSS resources. Redirects, missing/empty resources and unexpected asset content types fail preparation. Cross-origin assets require explicit HTTPS asset_origins from runtime records or scoped input. There are no origin defaults. Their requests carry no candidate Host/database headers, cookies or credentials; redirects and ambient proxy settings remain disabled. The coordinator must permit only the required public asset destinations through its outbound fence. The isolated fixture uses local system fonts and an internal Docker network; it never contacts a public CDN. Readiness requires successful render/asset/browser evidence no older than 30 seconds, bound to the exact contract. Re-run preparation immediately before a switch. These are observations at their recorded times, not a promise that a later request will succeed. Launchplane coordinates the final switch and public post-switch verification; HTTP 200 from liveness or /web/health is insufficient.

CLI Contract

  • /odoo/odoo-bin is a compatibility wrapper over upstream /usr/local/bin/odoo-source-bin.
  • The wrapper must preserve Odoo subcommands (server, shell, db, etc.).
  • Runtime defaults (--db_host, --addons-path, etc.) are injected only for server-style invocations so non-server commands keep upstream argument parsing semantics.
  • Server-style invocations always include /opt/launchplane/addons in the effective addons path and launchplane_runtime_health in the server-wide module list while preserving base,web.
  • A nonempty ODOO_DB_PASSWORD is forwarded as PGPASSWORD for every subcommand, overriding an inherited PGPASSWORD. Odoo reads it ahead of config-file passwords; the wrapper never generates a password argument. When it is empty or absent, an existing PGPASSWORD or config-file password keeps upstream behavior. Downstream callers should use these environment/config paths instead of password flags.

Downstream Build Contract

  • Downstream images inherit a single shared Python environment at /venv.
  • /venv is not configurable; downstream images must extend it additively and must not recreate it.
  • The image reserves these downstream layout paths:
    • /opt/runtime
    • /opt/project
    • /opt/project/addons
    • /opt/extra_addons
  • The image reserves /opt/launchplane/addons for image-owned Launchplane runtime addons. Downstream images must not replace this directory.
  • Layout 2 is selected by /opt/runtime/.odoo-python-sync-layout containing 2. Each populated dependency root must contain both pyproject.toml and uv.lock, plus .odoo-python-source.json with an owner/repository identity, exact lowercase 40-character source commit, and optional repository-relative lock_path. Legacy two-field markers default the lock path to uv.lock.
  • /opt/runtime owns the support/runtime lock. /opt/project owns the tenant uv workspace lock. Both root pyprojects are static dependency catalogs with tool.uv.package = false and no build system. The helper checks and exports the support lock first and the tenant lock second with frozen semantics, then installs both exports in one dependency-free operation that cannot replace packages inherited from the base image.
  • Layout 2 tenant workspace members must exactly match owned addon projects under /opt/project/addons. Owned projects use static package metadata, tool.uv.package = false, exact build-tool versions supplied by a lock, and code and package-metadata installation with --no-deps and no build isolation. A nested .odoo-python-source.json applies to projects beneath that directory, so staged shared-addon projects retain their own repository and commit attribution instead of inheriting the tenant root marker. Owned requirements*.txt, mutable VCS references, and local or archive dependency paths fail closed.
  • Support-only and tenant-only layout 2 roots are accepted for staging and tests but emit non-publishable evidence. Support-only layouts may still carry ordinary addon code when it has no Python dependency metadata. Publishable artifacts require both lock scopes.
  • ODOO_PYTHON_SYNC_SKIP_ADDONS remains a legacy-layout compatibility option. Layout 2 rejects it because skipping installation while exporting the full workspace lock would produce misleading dependency evidence.
  • Without the layout marker, odoo-python-sync.sh <prod|dev> preserves the bounded legacy single-root behavior and writes non-publishable evidence. The legacy path remains available only while downstream producers migrate.
  • odoo-fetch-addons.sh fetches external addon repositories declared in ODOO_ADDON_REPOSITORIES into /opt/extra_addons. Every entry must use owner/repository@<exact-commit>; branch and tag refs fail closed. The helper verifies the fetched Git commit before removing repository metadata and stores authoritative source sidecars beside checkouts rather than trusting marker files committed inside external repositories.
  • Layout 2 treats external dependency files as an explicit compatibility lane. Their repository commit, repo-relative path, format, and SHA-256 are recorded; they may add compatible packages but cannot replace base- or lock-owned package identities.
  • In layout 2, external Odoo addons with only Whool's build-system metadata and an __manifest__.py are loaded directly from their verified source tree. Their repository must provide requirements.txt; layout 2 installs that file under the same compatibility constraints and records it alongside each addon's packaging metadata. Development sync also includes repository-level requirements-dev.txt when present. Whool is not installed or executed for this source-only addon path. Other external Python projects still require static package names and exact build-tool requirements.
  • Every layout 2 sync finishes with uv pip check and writes deterministic platform-specific package evidence to /opt/launchplane/evidence/dependency-provenance.json. The producer combines one sidecar per target platform before publishing artifact provenance.
  • VCS package evidence uses owner/repository identities for GitHub sources and sanitized HTTPS or SSH URLs for other hosts, with any terminal .git suffix removed. packages_sha256 hashes the exact canonical package array embedded in the evidence.
  • Neither helper bakes in project-specific policy; downstream images choose which external repositories to fetch and whether to sync prod or dev dependencies.

CI Release Model

  • Every run builds test images first and executes smoke checks.
  • Pull-request builds resolve one Ubuntu snapshot, exact PostgreSQL package set, Python/npm cutoff, scanner image, and Trivy database for the comparison. Exact base and head commits use those same inputs. The merge gate blocks introduced or worsened high/critical findings without blaming a pull request for unchanged inherited findings. Causal comparison runs only when the base has native resolver support and its workflow, scanner, and evaluator match the head. The comparison uses the base copies of that tooling. Otherwise, verification retains the stricter candidate absolute gate, using base scanner tooling and its pinned scanner image when available. Scanner upgrades are therefore validated by the prior scanner on the upgrade pull request and become active for absolute default-branch health after merge.
  • Default-branch, scheduled, and dispatch runs retain absolute high/critical health. Multi-architecture images are pushed first under a unique candidate-<run>-<repository-sha>-* tag, then the exact registry manifest and its linux/amd64 and linux/arm64 child digests are scanned. Stable, nightly, source sha-*, and validated build-* aliases move only after that evidence passes and each alias is verified against the scanned manifest digest.
  • Trivy skips only /usr/share/java/gettext.jar and /usr/share/java/libintl-0.21.jar, which are owned by Ubuntu's gettext packages, so comparison scans do not require a separate mutable Java database; the installed gettext packages remain covered by the Ubuntu OS advisory scan.
  • schedule (weekly) promotes nightly-*, sha-*, and build-* tags.
  • push to main promotes stable 19.0-*, sha-*, and build-* tags.
  • Manual workflow_dispatch runs promote sha-* and build-* tags, and also stable 19.0-* tags unless publish_stable is turned off (it defaults on).
  • Pushes to launchplane/train/** merge-train branches verify only, like pull requests: they report image-verification and publish nothing.
  • After a publishing run, the workflow dispatches the odoo-enterprise-docker build with the exact published runtime and devtools digests. It passes on the same stable decision, and asks for nightly tags only on scheduled runs.
  • pull_request runs verify-only (no image publishing) and reports one stable image-verification merge gate after lint, source resolution, override validation, both image builds when resolver parity is available, smoke tests, and dependency-health evaluation pass.
  • Fork pull requests fail closed before using trusted self-hosted runners. A maintainer must recreate an accepted fork change on a branch in this repository before it can pass the image-verification merge gate.

This lets us keep a weekly canary stream while protecting stable tags behind the same verification gate.

CI Cache Policy

  • Verify jobs run on the chris-testing-build self-hosted lane and reuse a persistent per-runner Buildx builder for single-platform smoke images. Pull request verification uses a per-run apt refresh epoch so Ubuntu package layers are rehydrated for each verification run.
  • Publish jobs run on the chris-testing-publish-cache self-hosted lane and reuse a persistent per-runner Buildx builder, with GHCR registry cache as the portable fallback.
  • After each run, the verify and publish jobs each prune builder cache older than 7 days while their builder holds more than 60 GB, so local BuildKit state stays warm without growing forever.

This keeps the expensive multi-arch publish path warm on the self-hosted runner while still giving us a recoverable remote cache when a builder is recreated.

The GHCR retention workflow keeps stable, nightly, and registry-cache tags, preserves the newest 10 sha-*, validated build-*, and candidate-* tags per image suffix, and prunes untagged versions older than 7 days.

GHCR retention is repo-local package hygiene. This repo owns the package tags it publishes, so the retention workflow may delete only this repo's old package versions; it does not own Launchplane runtime state, tenant deployments, provider targets, product profiles, managed secrets, or artifact evidence.

Runner Health Checks

  • A scheduled Runner Health workflow tracks root filesystem and Docker root usage on chris-testing daily.
  • The health workflow runs on chris-testing-publish-cache so it does not consume the chris-testing-build verification lane.
  • The check fails when usage crosses the configured thresholds so operators get a visible GitHub Actions alert before the runner reaches saturation.

Runner health is repo-local image-build lane telemetry. It tracks the self-hosted cache lane used by this image build workflow and does not replace Launchplane-owned runner-host hygiene, runtime inventory, or provider health records.

Source Pinning

The workflow resolves the current odoo/odoo 19.0 commit and pins that exact revision into the build. This gives repeatable artifacts per run and makes scheduled updates explicit.

The Dockerfile pins artifact-affecting base and build images by digest, uses an exact uv-managed Python patch release, and accepts workflow-resolved Ubuntu snapshot, PostgreSQL package, and package-index cutoff inputs. Trivy snapshots record repository/source identity, producer and database revisions, scan scope, configuration hash, and platform. Publication evidence additionally records the exact manifest-list digest, platform child digests, and snapshot hashes.

uv is copied from Astral's official container image and pinned by tag+digest in the Dockerfile. Dependabot checks that image reference and the workflow actions weekly, waits 7 days after a release, and groups version updates into one infrastructure pull request.

Build

docker build \
  -t ghcr.io/cbusillo/odoo-docker:19.0-runtime \
  --target runtime \
  .
docker build \
  -t ghcr.io/cbusillo/odoo-docker:19.0-devtools \
  --target runtime-devtools \
  .

Validation Commands

  • bash scripts/test-check-requirements-overrides.sh checks exact pins, explicitly allowed unpinned requirements, non-exact constraints, and removed requirements for the override freshness gate.
  • bash scripts/test-image-dependency-health.sh checks the scanner's --skip-files arguments without a Docker daemon, deterministic Trivy normalization, fail-closed provenance matching, regression policy, target advisory handling, and multi-architecture artifact binding.
  • scripts/test-odoo-bin-wrapper.sh checks wrapper argument handling without a container build.
  • scripts/smoke-runtime.sh <image-reference> checks the runtime image helper contract and Odoo CLI availability.
  • scripts/smoke-devtools.sh <image-reference> checks the runtime contract before checking the devtools image browser tooling and addon path setup.
  • scripts/smoke-db-init.sh <image-reference> checks database-backed Odoo initialization, authenticated shell and database dump commands, and that disposable Odoo process listings omit database password values and flags.
  • scripts/smoke-readiness.sh <runtime-image> [browser-image] creates a fresh Website database on a private internal Docker network, exercises Chromium and real page/asset preparation, and proves readiness failures. All database keys and runtime identities in this fixture are inert, and no ports are published.
  • scripts/test-downstream-helpers.sh <image-reference> checks downstream Python sync and external addon fetch behavior, including legacy compatibility, strict partial and layered locks, stale and incomplete lock failures, owned workspace enforcement, exact external source handling, conflict isolation, final environment checks, and deterministic evidence.

Security Notes

  • Do not add credentials or access tokens in this repo.
  • Proprietary addons should be fetched by downstream builds using BuildKit secrets via odoo-fetch-addons.sh.
  • requirements-overrides.txt carries reviewed secure or shared downstream compatibility pins when upstream Odoo requirements cannot yet resolve the required release. Keep those pins narrow and let scripts/check-requirements-overrides.py reject removed upstream requirements, unexpected unpinned or ranged requirements, and upstream exact pins that make an override removable.
  • Neither the runtime virtual environment nor its uv-managed base interpreter under /opt/uv/python ships pip or ensurepip. Image builds and downstream dependency synchronization use uv; restoring pip would add unused vendored packages and expand the runtime vulnerability and supply-chain surface. Standard-library virtual environment bootstrapping with bundled pip is intentionally unavailable; use uv venv instead.

About

Base Odoo runtime image

Topics

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages