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.
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.
runtime: base Odoo runtime + PostgreSQL client + uv toolingruntime-devtools:runtimeplus Playwright-managed Chromium test tooling
Both images default to the ubuntu user for compatibility with existing
restore and SSH mount workflows.
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"
)
PYKeep 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.
runtimestays runtime-first and does not write IDE-oriented Python path entries.runtime-devtoolswrites a minimalodoo_paths.pthfor generic non-core source and addon roots used in local development tooling:/odoo/opt/project/addons/opt/extra_addons
runtime-devtoolsexposes Playwright-managed Chromium throughCHROME_BINat/usr/local/bin/chromium-playwright.
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.
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.jsonLaunchplane'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.
/odoo/odoo-binis 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/addonsin the effective addons path andlaunchplane_runtime_healthin the server-wide module list while preservingbase,web. - A nonempty
ODOO_DB_PASSWORDis forwarded asPGPASSWORDfor every subcommand, overriding an inheritedPGPASSWORD. Odoo reads it ahead of config-file passwords; the wrapper never generates a password argument. When it is empty or absent, an existingPGPASSWORDor config-file password keeps upstream behavior. Downstream callers should use these environment/config paths instead of password flags.
- Downstream images inherit a single shared Python environment at
/venv. /venvis 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/addonsfor image-owned Launchplane runtime addons. Downstream images must not replace this directory. - Layout 2 is selected by
/opt/runtime/.odoo-python-sync-layoutcontaining2. Each populated dependency root must contain bothpyproject.tomlanduv.lock, plus.odoo-python-source.jsonwith anowner/repositoryidentity, exact lowercase 40-character source commit, and optional repository-relativelock_path. Legacy two-field markers default the lock path touv.lock. /opt/runtimeowns the support/runtime lock./opt/projectowns the tenant uv workspace lock. Both root pyprojects are static dependency catalogs withtool.uv.package = falseand 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-depsand no build isolation. A nested.odoo-python-source.jsonapplies to projects beneath that directory, so staged shared-addon projects retain their own repository and commit attribution instead of inheriting the tenant root marker. Ownedrequirements*.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_ADDONSremains 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.shfetches external addon repositories declared inODOO_ADDON_REPOSITORIESinto/opt/extra_addons. Every entry must useowner/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-systemmetadata and an__manifest__.pyare loaded directly from their verified source tree. Their repository must providerequirements.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-levelrequirements-dev.txtwhen 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 checkand 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/repositoryidentities for GitHub sources and sanitized HTTPS or SSH URLs for other hosts, with any terminal.gitsuffix removed.packages_sha256hashes 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
prodordevdependencies.
- 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 itslinux/amd64andlinux/arm64child digests are scanned. Stable, nightly, sourcesha-*, and validatedbuild-*aliases move only after that evidence passes and each alias is verified against the scanned manifest digest. - Trivy skips only
/usr/share/java/gettext.jarand/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) promotesnightly-*,sha-*, andbuild-*tags.pushtomainpromotes stable19.0-*,sha-*, andbuild-*tags.- Manual
workflow_dispatchruns promotesha-*andbuild-*tags, and also stable19.0-*tags unlesspublish_stableis turned off (it defaults on). - Pushes to
launchplane/train/**merge-train branches verify only, like pull requests: they reportimage-verificationand publish nothing. - After a publishing run, the workflow dispatches the
odoo-enterprise-dockerbuild 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_requestruns verify-only (no image publishing) and reports one stableimage-verificationmerge 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.
- Verify jobs run on the
chris-testing-buildself-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-cacheself-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.
- A scheduled
Runner Healthworkflow tracks root filesystem and Docker root usage onchris-testingdaily. - The health workflow runs on
chris-testing-publish-cacheso it does not consume thechris-testing-buildverification 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.
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.
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 \
.bash scripts/test-check-requirements-overrides.shchecks exact pins, explicitly allowed unpinned requirements, non-exact constraints, and removed requirements for the override freshness gate.bash scripts/test-image-dependency-health.shchecks the scanner's--skip-filesarguments 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.shchecks 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.
- 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.txtcarries reviewed secure or shared downstream compatibility pins when upstream Odoo requirements cannot yet resolve the required release. Keep those pins narrow and letscripts/check-requirements-overrides.pyreject 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/pythonshipspiporensurepip. Image builds and downstream dependency synchronization useuv; restoringpipwould add unused vendored packages and expand the runtime vulnerability and supply-chain surface. Standard-library virtual environment bootstrapping with bundledpipis intentionally unavailable; useuv venvinstead.