# stateless-container-for-onebspace-development 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 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 * **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 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 | |---|---| | `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 ### 4.1 Prerequisites on the host * 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). ### 4.2 Run the prepare command ```bash 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: " ``` Optional sanity checks: ```bash 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 # 1. Just edit code. vim /root/Documents/trae_projects/one_b_os/os-dev/odoo/addons/... # 2. Refresh the page. --dev=reload re-imports modified modules on # next request; --dev=qweb re-renders templates; --dev=assets serves # unbundled JS. # 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) | 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) | ## 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.