feat(codespace): replace broken odoo-image blueprint with python:3.12-slim pattern

The previous blueprint (upstream odoo/docker:19.0 + COPY + deploy.command)
systematically broke inside Easypanel's inline-dockerfile build context:
  - COPY files not available
  - wkhtmltopdf/libldap-2.5-0 package names absent from Debian bookworm
  - Easypanel wrapped deploy.command with 'python -c …' → exit 100
  - postgres password / app env set separately → mismatch auth failure
  - cat > /etc/odoo/odoo.conf without parent dir → ENOENT

The new blueprint is what actually ran in production for 34h+ stable:
  - FROM python:3.12-slim with an inline ENTRYPOINT (no CMD wrapping)
  - init-odoo.sh: apt install build deps then pip install /mnt/os-dev
    (idempotent — skipped if import odoo works, restarts < 1 s)
  - --dev=reload,qweb,assets,xml,werkzeug baked into exec odoo
  - mkdir -p /etc/odoo /var/lib/odoo /var/log/odoo before writing odoo.conf
  - Single createFromSchema call → shared password for postgres + app
  - Bind-mount os-dev → /mnt/os-dev → hot-reload on any host edit

deploy.py prepare writes easypanel.schema.json + init-odoo.sh +
inline.Dockerfile + .db_password, all ready for TRAE Easypanel MCP
createFromSchema + deployAppService. Verified end-to-end (HTTP 200 on
/web/database/manager, hot-reload on release.py product_name edit,
CREATE DATABASE via psycopg2 + odoo UI Create Database).

Docs:
  - README.md rewritten with new layout table, troubleshooting
    playbook, daily workflow, verified-state reference table,
    old-vs-new blueprint comparison table.
This commit is contained in:
1bspace-os rebrand committed 2026-09-14 22:59:46 +00:00
1 parent fff19a9e17
commit 6e7be43dc8
4 files changed
+467 -336

No files matched your search

+161 -122
View File
@@ -1,153 +1,192 @@
# stateless-container-for-onebspace-development
Stateless hot-reload **Odoo 19** development workspace for the
`1b-agent/os-dev` repo, deployed on the **Easypanel** project
`onebspace`.
Blueprint (and reusable scripts) for spinning up a **stateless hot-reload
`os-dev` codespace** for the `1b-agent/os-dev` Odoo 19 fork, deployed on
the Easypanel project `onebspace`.
This repo is the **blueprint** — the live state is on the
[Easypanel onebspace project](http://45.137.70.123:3000)
(`odoo-dev` + `odoo-dev-db` services) and the bind-mounted workspace
at `/root/Documents/trae_projects/one_b_os/os-dev/`.
---
This blueprint was **battle-tested Sep 2026** on a real deployment: the
container stayed **Up 34 hours+** stably running Odoo 19 from a bind-mount
of the live source tree with all five `--dev=*` flags active, and we
proved end-to-end hot-reload by editing `release.py product_name` and
watching the change propagate without a restart.
## 1. What this gives you
- **No image rebuilds for code edits.** Edit any `.py` in
`os-dev/odoo/addons/...` and `--dev=reload` (watchdog) restarts
Odoo in milliseconds.
- **Full hot-reload flags** baked into `odoo.conf`:
```ini
dev_mode = reload,qweb,assets,xml,werkzeug
```
- `reload` → restart on `.py` changes
- `qweb` → recompile QWeb per request
- `assets` → unbundle JS/CSS
- `xml` → re-read XML views from disk
- `werkzeug` → in-browser tracebacks
- **Image built by Easypanel from an inline Dockerfile** on every
deploy (`source.type=dockerfile`), no Gitea Container Registry
needed.
- **Bind-mount** of the entire `1b-agent/os-dev` workspace at
`/mnt/os-dev` so the upstream addons, your custom addons, and
the core Odoo code are all editable in place.
* **Zero image rebuilds on code edits.** Edit any `.py` / `.xml` / `.js` in
`os-dev/` and the change is live within milliseconds because Odoo is
launched with `--dev=reload,qweb,assets,xml,werkzeug`.
* **Bind-mounted source.** The whole os-dev repository is mounted from
the host into the container at `/mnt/os-dev`. First boot takes ~5 min
while `pip install /mnt/os-dev` builds native extensions; every
subsequent boot is < 1 s (idempotent install).
* **Tiny base image.** `python:3.12-slim` (Debian bookworm, ~45 MB)
instead of the bloated upstream `ubuntu:noble` Odoo image.
* **Trivial inline Dockerfile** (80 lines, 2 `RUN` instructions total,
zero `COPY` instructions — every file is a heredoc). This is crucial
because Easypanel's `source.type=dockerfile` build context contains
only the Dockerfile; no repo files are available to `COPY`.
* **Single schema call.** Passwords for postgres AND the app env are
generated from the same `createFromSchema` payload, so the "FATAL:
password authentication failed for user odoo" mismatch bug cannot
happen again.
* **2 UI extras on postgres:** `pgWeb` + `dbGate` web panels are
enabled out of the box on the companion db service.
---
## 2. What changed from the old blueprint
## 2. Components
The previous blueprint tried (and failed) to replicate the upstream
`odoo/docker:19.0` Dockerfile with COPY'd files. Every single step of
that approach broke inside Easypanel's inline-dockerfile build context:
| Old approach (broken) | New approach (works) |
|---|---|
| Upstream `odoo:19.0` base (1 GB, ubuntu) | `python:3.12-slim` (45 MB, bookworm) |
| COPY entrypoint.sh / COPY odoo.conf in Dockerfile | Inlined as `RUN cat > /file <<'MARKER'` heredocs |
| `wkhtmltopdf` and `libldap-2.5-0` apt packages (names don't exist in debian bookworm) | Omitted (we don't use PDFs in dev) / renamed to `libldap2-dev` |
| `deploy.command="bash entrypoint.sh"` (Easypanel wraps it as `python -c "bash…"` → exit 100) | Explicit `ENTRYPOINT ["/usr/local/bin/init-odoo.sh"]` in inline Dockerfile |
| Updated app env password AFTER postgres service was already running → password mismatch | Shared password in single `createFromSchema` payload |
| `cat > /etc/odoo/odoo.conf` without creating the parent dir → ENOENT → `odoo.conf doesn't exist or is not readable` | `mkdir -p /etc/odoo /var/lib/odoo /var/log/odoo` first |
| Manual tRPC calls (`services.app.createService` etc.) bypassing TRAE MCP | Easypanel MCP `createFromSchema` (handles ordering + postgres) |
## 3. Repo layout
| File | Purpose |
|------|---------|
| `Dockerfile` | Verbatim copy of [`github.com/odoo/docker/19.0/Dockerfile`](https://github.com/odoo/docker/blob/master/19.0/Dockerfile) + `pip install -r requirements.txt --break-system-packages`. SHA-pinned release: `ODOO_RELEASE=20260817`, `ODOO_SHA=7e6c102efc7e2ad56801432fef7e2e0d2f703e5a` |
| `entrypoint.sh` | Postgres env-var → `--db_host/--db_user/...` propagation (copied from upstream) |
| `wait-for-psql.py` | Waits for Postgres before starting Odoo (copied from upstream) |
| `odoo.conf` | Bake-time Odoo config; full hot-reload; `addons_path` includes our `/mnt/os-dev/{addons,odoo/addons}` |
| `requirements.txt` | Python deps from the upstream `odoo/19.0` requirements.txt |
| `easypanel.schema.json` | Easypanel service payload using **`source.type=dockerfile`** + inline Dockerfile |
| `deploy.py` | Calls Easypanel\'s native tRPC API directly (bypasses the MCP wrapper that only supports `source.type=image`) to push the inline Dockerfile, set env, and deploy |
| `.gitea/workflows/build.yml` | Optional: auto-build & push `os-dev:19.0` to a Gitea Container Registry on every push to `1b-agent/os-dev`'s `19.0` branch |
|---|---|
| `deploy.py` | **One-stop script.** `python3 deploy.py prepare` writes all artifacts + a fresh DB password. `dockerfile` subcommand prints the generated inline Dockerfile for inspection. |
| `init-odoo.sh` | Runtime entrypoint. Written by `deploy.py prepare`. Installs apt deps, then pip-installs the bind-mounted `/mnt/os-dev` if odoo is not yet importable, writes odoo.conf from env vars, waits for postgres, then `exec`s odoo with all 5 dev flags. |
| `inline.Dockerfile` | The 80-line inline Dockerfile (written by `prepare`). Uses heredocs to drop init-odoo.sh and sets `ENTRYPOINT` explicitly. |
| `easypanel.schema.json` | Ready-to-use `createFromSchema` payload (written by `prepare`). Contains both services, mounts, ports, env vars, and a fresh shared password. |
| `.db_password` | Plaintext copy of the shared password (written by `prepare`). Git-ignored. |
| `wait-for-psql.py` | Legacy artifact, no longer used (init-odoo.sh's socket-poll loop replaced it). Kept for reference. |
| `entrypoint.sh` | Legacy artifact, no longer used (replaced by init-odoo.sh). Kept for reference. |
---
## 4. Step-by-step: deploy a new codespace
## 3. How it was built (research summary)
### 4.1 Prerequisites on the host
### 3.1 The official `odoo/docker` image
- Base: `ubuntu:noble` (not Debian)
- Pre-installs `python3-watchdog` (what makes `--dev=reload` work)
- Entrypoint waits for Postgres (`wait-for-psql.py --timeout=30`)
- Default `odoo.conf` has `addons_path = /mnt/extra-addons`,
`data_dir = /var/lib/odoo` — both declared as `VOLUME`
* Easypanel project `onebspace` exists.
* A source checkout of `os-dev` at
`/root/Documents/trae_projects/one_b_os/os-dev/` (this is what gets
bind-mounted — live edits inside it take effect immediately).
* TRAE MCP Easypanel server is authenticated against
`http://45.137.70.123:3000` with a bearer token from
`/root/.trae-server/data/Machine/mcp.json`.
* Docker swarm mode (Easypanel runs swarm).
### 3.2 dev_mode flags (Odoo 19 CLI reference)
| Flag | Effect |
|------|--------|
| `reload` | restart `odoo-bin` on `.py` change (needs `python3-watchdog`) |
| `qweb` | recompile QWeb templates per request |
| `assets` | serve JS/CSS unbundled → edit-and-refresh |
| `xml` | re-read XML view files on update |
| `werkzeug` | show full Python tracebacks in browser |
### 4.2 Run the prepare command
### 3.3 Why `source.type=dockerfile` (not image pull)
Easypanel\'s `services.app.createFromSchema` (the MCP wrapper) only
supports `source.type=image`, and Easypanel couldn\'t pull from
the Gitea Container Registry due to TLS cert mismatch + missing
package pre-creation.
Easypanel itself supports `source.type=dockerfile` (inline
Dockerfile build) — but the MCP wrapper doesn\'t surface it. We
**bypassed the MCP** and called the native tRPC API directly:
```
POST /api/trpc/services.app.createService
POST /api/trpc/services.app.updateSourceDockerfile
POST /api/trpc/services.app.updateEnv
POST /api/trpc/services.app.deployService
```
The Dockerfile is stored on Easypanel\'s filesystem at
`/etc/easypanel/projects/<project>/<service>/code/Dockerfile` and
rebuilt on every deploy.
### 3.4 Self-contained inline Dockerfile
Easypanel\'s `source.type=dockerfile` has **no `COPY` access**
(per docs: *"Files from a repository or upload are not
automatically available to this source type"*), so all `COPY`-ed
files are inlined as heredocs (`RUN cat > /file <<\'MARKER\'`).
This produces a single ~17 KB Dockerfile with zero `COPY`
instructions.
---
## 4. Reproducing this setup on a fresh Easypanel
### 4.1 Prereqs
- An Easypanel instance with a project (we used `onebspace`)
- A clone of `1b-agent/os-dev` at `/root/Documents/trae_projects/one_b_os/os-dev/`
- An Easypanel bearer token (Settings → API → Generate Token,
or grab the MCP token from `/root/.trae-server/data/Machine/mcp.json`)
### 4.2 Run the deploy script
```bash
cd stateless-container-for-onebspace-development
python3 deploy.py deploy
cd /root/Documents/trae_projects/one_b_os/stateless-container-for-onebspace-development
python3 deploy.py prepare
# → writes easypanel.schema.json, inline.Dockerfile, init-odoo.sh, .db_password
# → prints "Password: <fresh 24 chars>"
```
This will:
1. Generate the inline Dockerfile from `../os-dev/*` files
2. Generate a fresh DB password and save to `.db_password`
3. Patch `CHANGE_ME_DB_PASSWORD` in the env block
4. Call the four Easypanel tRPC procedures in order
5. Build the image in-place and start the container
Verify:
Optional sanity checks:
```bash
curl -sS -o /dev/null -w "HTTP %{http_code} bytes=%{size_download}\\n" \
http://127.0.0.1:18069/web/database/manager
# expected: HTTP 200 bytes=70194
python3 deploy.py dockerfile | head -10 # confirm FROM python:3.12-slim
jq '.schema.services[0].data.password' easypanel.schema.json # same password
jq '.schema.services[1].data.env' easypanel.schema.json # PASSWORD=same value
```
---
### 4.3 Apply via TRAE Easypanel MCP
Inside a TRAE agent chat, do:
```
1. call mcp_Easypanel : execute_mutation / createFromSchema
payload = the json.read(easypanel.schema.json)
2. call mcp_Easypanel : execute_destructive / deployAppService
{projectName: "onebspace", serviceName: "os-dev-codespace"}
3. wait 5-7 min. First boot runs pip install /mnt/os-dev → ~5 min
native build (lxml, pillow, python-ldap, psycopg2-c).
4. curl -sS -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:18069/web/database/manager
expect: HTTP 200 (70,194 bytes)
5. Visit http://127.0.0.1:18069/web/database/manager
Master password = admin. Click "Create Database".
```
### 4.4 Troubleshooting the container
If the service flips to **Desired: Running, Current: exit 100 / exit 2**,
catch it in its 10-second window:
```bash
CID=$(docker ps --filter "name=onebspace_os-dev-codespace" --format '{{.ID}}' | head -1)
docker exec $CID cat /tmp/init.log 2>&1 | tail -50
```
The init script redirects everything to `/tmp/init.log` with `set -x`.
Common fixes:
* `exit 100` on boot and no log written → the Dockerfile ENTRYPOINT was
stripped / Easypanel wrapped it with `python`. Re-apply
`inline.Dockerfile` via `updateAppSourceDockerfile`.
* `apt-get install had errors` → check for package name drift (e.g.
`libldap-2.5-0` vs `libldap2-dev`). The script continues on apt
errors; only a failed `pip install` matters.
* `config file '/etc/odoo/odoo.conf' doesn't exist or is not readable` →
you forgot `mkdir -p /etc/odoo` before the `cat >` redirect. It's in
the current init script; if you see this, the script was not
updated.
* `FATAL: password authentication failed for user odoo` → you updated
the app env but not the live postgres role. Don't do that. Always
deploy both sides from the same `createFromSchema` payload, or if you
must update the password live:
```
docker exec -it $PG_CID psql -U odoo -d postgres \
-c "ALTER USER odoo WITH PASSWORD '$NEW_PASS';"
```
* Odoo installs but you can't create a database → verify your
postgres user was CREATED with CREATEDB. The schema uses the
`postgres` service type in Easypanel, which does this for you.
## 5. Daily workflow
```bash
# edit code in place — auto-reload picks it up
vim /root/Documents/trae_projects/one_b_os/os-dev/odoo/addons/web/controllers/main.py
# 1. Just edit code.
vim /root/Documents/trae_projects/one_b_os/os-dev/odoo/addons/...
# restart Odoo manually if needed
docker restart $(docker ps --filter "name=onebspace_odoo-dev" --format "{{.ID}}" | head -1)
# 2. Refresh the page. --dev=reload re-imports modified modules on
# next request; --dev=qweb re-renders templates; --dev=assets serves
# unbundled JS.
# rebuild the image (only when requirements.txt or Dockerfile changes)
python3 deploy.py redeploy
# 3. Only if you edit requirements.txt / setup.py's install_requires AND
# want to test the new deps WITHOUT waiting for a fresh image:
CID=$(docker ps --filter name=onebspace_os-dev-codespace --format '{{.ID}}' | head -1)
docker exec -it $CID bash -lc 'pip install --break-system-packages /mnt/os-dev'
# 4. Restart (NOT needed for .py edits, only for stale workers):
docker restart $CID
# 5. Destroy the whole stack when done:
# (via TRAE MCP) destroyAppService + destroyPostgresService
```
---
## 6. Verified working state (reference)
## 6. Verified working state
| Item | Value (from the Sep 14 2026 live deployment) |
|---|---|
| Container image | `python:3.12-slim@sha256:6fb6b091a…` (bookworm) |
| Container uptime | Up 34 h (stable, no flapping) |
| Odoo PID 1 cmdline | `/usr/local/bin/python3 /usr/local/bin/odoo -c /etc/odoo/odoo.conf --dev=reload,qweb,assets,xml,werkzeug` |
| Bind mount | `/root/Documents/trae_projects/one_b_os/os-dev` → `/mnt/os-dev` (rw) |
| Filestore volume | `onebspace_os-dev-codespace-filestore` → `/var/lib/odoo` |
| HTTP /web/database/manager | **200 OK** (70 194 bytes) |
| CREATE DATABASE test | psycopg2 `CREATE DATABASE test_codespace_db OWNER odoo` — OK |
| Hot-reload test | Edit `release.py product_name='Odoo'` → `PRODUCT_NAME=1BOSDEV_HOTRELOAD_TEST` inside running container within 10 s → revert → back to `Odoo`. No container restart. |
| Postgres | `postgres:17`, user `odoo`, port 5432 (swarm DNS `onebspace_os-dev-codespace-db`) |
| Host port | 18069 → container 8069 |
| First-boot install time | ~5 min (native lxml, pillow, python-ldap builds) |
- `Odoo version 19.0-20260817` (matches `ODOO_SHA=7e6c102e…`)
- HTTP 200 (70,194 bytes) on `/web/database/manager`
- Addons loaded from `/mnt/os-dev/addons`, `/mnt/os-dev/odoo/addons`,
plus upstream `/usr/lib/python3/dist-packages/odoo/addons`
- `AutoReload watcher running with watchdog` + `HTTP service (werkzeug) running`
- Bind-mount `/root/Documents/trae_projects/one_b_os/os-dev/` → `/mnt/os-dev/`
verified (24 addons in both views)
## 7. Customizing
Open `deploy.py` and edit the block at the top:
```python
EASYPANEL_PROJECT = "onebspace"
APP_SERVICE = "os-dev-codespace"
HOST_PORT = 18069
OS_DEV_HOST_PATH = "/root/Documents/trae_projects/one_b_os/os-dev"
BASE_IMAGE = "python:3.12-slim"
PG_IMAGE = "postgres:17"
```
Then run `prepare` again to regenerate all artifacts.