Operate an everjust.app (Odoo 19) tenant from the BOX SHELL (SSH + docker), the deploy-pipeline and box-ops layer. Use when the task needs bulk DB writes, DB-only page publishing, clearing the sitemap ir.attachment cache, creating an ir.cron, recovering a CI deploy whose rsync silently did not land, avoiding deploy-collisions across concurrent agents (gh run idle check, scoped git add), restarting Odoo to compile a QWeb edit, or fixing an nginx stale-inode after a config redeploy. Covers the odoo shell invocation, the COW-fork-overrides-generic gotcha, and the Deploy to Production pipeline. The deploy/ops layer around [[everjust-website-customization]] (which owns durable view/CSS/behavior customization from the shell). Prefer the MCP ([[everjust-agent-mcp]], [[everjust-website]]) for ordinary edits; drop to the shell when it cannot. Cross-references [[everjust-platform]], [[github-actions-ec2-deploy]].
Operate an everjust.app (Odoo 19) tenant from the BOX SHELL (SSH + docker), the deploy-pipeline and box-ops layer. Use when the task needs bulk DB writes, DB-only page publishing, clearing the sitemap ir.attachment cache, creating an ir.cron, recovering a CI deploy whose rsync silently did not land, avoiding deploy-collisions across concurrent agents (gh run idle check, scoped git add), restarting Odoo to compile a QWeb edit, or fixing an nginx stale-inode after a config redeploy. Covers the odoo shell invocation, the COW-fork-overrides-generic gotcha, and the Deploy to Production pipeline. The deploy/ops layer around [[everjust-website-customization]] (which owns durable view/CSS/behavior customization from the shell). Prefer the MCP ([[everjust-agent-mcp]], [[everjust-website]]) for ordinary edits; drop to the shell when it cannot. Cross-references [[everjust-platform]], [[github-actions-ec2-deploy]].
EVERJUST Odoo Shell Ops (Agent Skill)
Drive a live everjust.app tenant (Odoo 19 CE fork, one Postgres DB per tenant) from the box shell: SSH to the tenant's EC2 instance, then docker exec into the deployment-odoo-1 container. This is the infra and fallback lane, the things the everjust_agent_mcp MCP deliberately cannot do: bulk ORM writes, editing infra and layout views (header/footer), clearing the QWeb and attachment caches, creating ir.cron records DB-only, and landing addon file changes. The MCP path is preferred for ordinary content and SEO (see the boundary below); reach for the shell only when you have SSH and need raw ORM or file and deploy control.
This skill is about mechanics on the box, not the platform rules. The invariants (one DB per tenant, /odoo debrand, never self-escalate, group_ids not groups_id, custom everjust.mail.* stack, re-sweep on -u) live in [[everjust-platform]] and still apply here. A shell does not exempt you from them. On the tenant the site is , domain , container , DB .
connectdomain
website
id 1
https://connectdomain.app
deployment-odoo-1
connectdomain
When to use this skill
Run a bulk or scripted DB write the MCP can't express in one call, e.g. rewrite every published page's website_meta_*, backfill hundreds of rows, run an ORM migration. Pipe a Python script into odoo shell.
Edit an infra or layout view: header, footer, website.layout xpaths, custom_code_head. These are structural QWeb the MCP does not surface as content.
Publish a page DB-only: create the ir.ui.view plus website.page pair directly (no git, no deploy) when you want it live on the next request.
Fix a header or footer that "won't change": a website_id=1 copy-on-write fork is overriding the generic view; you must find and edit the fork (see Recipe 3).
Clear a cache the ORM won't clear on its own: the compiled QWeb template cache (needs a container restart) or the /sitemap.xmlir.attachment (needs an unlink).
Create an ir.cron DB-only for a pure-ORM recurring job (Odoo 19 shape; no numbercall; safe_eval code only).
Deploy an addon FILE change and, especially, recover a green-but-stale CI deploy where the rsync silently didn't land the file.
Fix an nginx config on the box (config replaced by CI but the running container is pinned to the stale inode).
Do NOT use this skill for, and stop if the task is really:
Ordinary content or SEO writes the MCP handles cleanly: set a page's copy, publish/menu/redirect, or per-page SEO metadata. Use [[everjust-website]] and [[everjust-website-seo]] through [[everjust-agent-mcp]]. The MCP is COW-safe, audited, and role-bounded; the shell is not. Only drop to it when the MCP genuinely can't.
The platform rules or model shape: what everjust.mail.* is, send gating, secret namespaces, ACL boundaries. That's [[everjust-platform]]; read it first.
The full GEO and AI-discoverability content program: llms.txt, JSON-LD, IndexNow, the sitemap content strategy. That's [[everjust-website-seo]]; this skill only covers the shell-level cache mechanics those depend on.
Authoring the CI pipeline itself (the workflow YAML, secrets, health checks). That's [[github-actions-ec2-deploy]]; here we only use and recover the existing "Deploy to Production" run.
The box map (real, non-secret facts)
Thing
Value on connectdomain
Notes
Odoo container
deployment-odoo-1
docker exec -i into it to run odoo shell; docker restart it to flush the compiled-QWeb cache.
Nginx container
deployment-nginx-1
Reverse proxy; config bind-mounted from the box.
Compose dir on box
/opt/everjust/platform/deployment
docker compose runs from here; nginx configs under nginx/*.conf.
Present, but does not carry working DB creds on this box, pass --db_* inline (Recipe 1).
Tenant DB
connectdomain
One DB per tenant; db_host=db (the compose Postgres service).
Website
website id 1, https://connectdomain.app
One website per tenant.
Deploy workflow
GitHub Actions "Deploy to Production", concurrency group deploy-production
Push to master triggers it; cancel-in-progress:false so runs queue, not cancel.
Model and record types you touch from the shell: ir.ui.view (QWeb templates, incl. COW forks), website.page (URL-to-view binding plus publish/index), ir.attachment (the cached /sitemap.xml), ir.cron (scheduled ORM jobs), ir.config_parameter, ir.model (to resolve model_id for a cron).
Recipes
Placeholders: <box-ip> (tenant EC2 IP), ~/.ssh/<deploy-key>.pem (SSH key), <db-user> (Postgres role), $ODOO_DB_PW (export the DB password in your local env, never inline the literal). Everything runs as ubuntu@<box-ip>.
1. Open an Odoo shell and WRITE (the commit is on you)
Pipe a local Python file straight into odoo shell inside the container. Conf-only does not connect on this box, you must pass --db_host / --db_user / --db_password inline even though you also pass -c:
Inside the shell you get env (an Odoo Environment). The shell does NOT auto-commit, nothing you write persists until you call env.cr.commit():
# script.py, piped into odoo shell
pages = env['website.page'].search([('website_published', '=', True)])
for p in pages:
ifnot p.website_meta_title:
p.website_meta_title = f"{p.name} | Connect Domain"
env.cr.commit() # REQUIRED, or the whole write is silently rolled backprint("updated", len(pages))
For large arch or multi-line scripts, transfer as base64 to dodge shell-quoting hell (nested single and double quotes across ssh to docker exec to Python will otherwise mangle your <, >, ", $):
# encode locally
B64=$(base64 -i big_arch_script.py)
# decode and run on the box (single-quoted heredoc keeps it intact through both hops)
ssh -i ~/.ssh/<deploy-key>.pem ubuntu@<box-ip> \
"echo $B64 | base64 -d | docker exec -i deployment-odoo-1 odoo shell -d connectdomain \
--no-http -c /etc/odoo/odoo.conf --db_host=db --db_user=<db-user> --db_password=\"\$ODOO_DB_PW\""
Inside a script, when you build an arch string that contains quotes or newlines, base64 the arch itself and base64.b64decode(...).decode() it in Python rather than embedding the raw XML in the command line.
2. Publish a page DB-only (view plus page, live on next request)
Create the QWeb view, then the website.page that binds a URL to it. A brand-new view or page compiles and renders on the next HTTP request, no restart needed:
Fetch https://connectdomain.app/pricing to confirm. New render is immediate; EDITS are not, see Recipe 4.
3. Fix a header or footer that "won't change", the COW-fork gotcha
A view can exist twice under the same key: a generic one (website_id = False) AND a website-specific copy-on-write fork (website_id = 1). The fork overrides the generic and is what actually renders. Editing the generic view changes nothing on the live site. Before editing ANY layout view, search for the website_id=1 fork of that key and edit THAT:
key = 'website_cd_footer.cd_footer'
generic = env['ir.ui.view'].search([('key','=',key),('website_id','=',False)])
fork = env['ir.ui.view'].search([('key','=',key),('website_id','=',1)])
print('generic', generic.ids, 'fork', fork.ids)
target = fork or generic # the fork wins if it exists, edit the fork
target.write({'arch': '<the new arch>'})
env.cr.commit()
Real case on connectdomain: the footer existed as generic view 3753 AND COW fork view 4091; the live footer only changed once 4091 was edited. Hours were lost editing 3753 and seeing no change. Always resolve the fork first.
4. Make an EDIT actually render, restart the container
A brand-new view renders on next request, but an edit to an already-compiled template does NOT render until the compiled-QWeb cache is dropped. Cache-clear calls and registry signals are unreliable here; the reliable fix is a container restart:
Rule of thumb: created a view, fetch it; edited a view's arch, restart, then fetch. If you edited and the page looks unchanged, restart before you debug anything else.
5. Clear the /sitemap.xml cache (it's a cached ir.attachment)
/sitemap.xml is served from a cached ir.attachment (its url matches sitemap, TTL about 12h). Pages you just created do NOT appear in it until that attachment is cleared:
env['ir.attachment'].search([('url','ilike','sitemap')]).unlink()
env.cr.commit()
# then re-fetch https://connectdomain.app/sitemap.xml to regenerate it
If new pages keep needing this, schedule it as an ir.cron (Recipe 6). The full sitemap and GEO content strategy is [[everjust-website-seo]]; this is just the cache-eviction mechanic it relies on.
6. Create an ir.cron DB-only (Odoo 19 shape)
Resolve model_id via ir.model._get(...). The code runs under safe_eval: no imports, no outbound HTTP, keep it to pure ORM:
cron = env['ir.cron'].create({
'name': 'Clear sitemap cache',
'model_id': env['ir.model']._get('ir.attachment').id,
'state': 'code',
'code': "env['ir.attachment'].search([('url','ilike','sitemap')]).unlink()",
'interval_number': 12,
'interval_type': 'hours',
'active': True,
'nextcall': fields.Datetime.now(),
# NOTE: do NOT set 'numbercall', the field was removed in Odoo 19.
})
env.cr.commit()
Odoo 19: ir.cronno longer has numbercall, setting it raises. Because code is safe_eval (no import, no network), an outbound-HTTP job (e.g. an IndexNow POST) cannot live in an ir.cron, put that in a system cron on the box (a shell script plus crontab) instead, not here.
7. Deploy an addon FILE change, and recover a green-but-stale CI run
Normal path: push to master, GitHub Actions "Deploy to Production" ([[github-actions-ec2-deploy]]) rsyncs to the box and runs a scoped module upgrade. GOTCHA: the CI rsync sometimes silently does not land the file, the run is green and the module "loads," but the on-box file is still the old one. ALWAYS verify the actual on-box file after an addon deploy:
Manual recovery (rsync the addon up, scoped upgrade, restart to flush QWeb):
# 1) push the addon dir to the box (sudo on the far side so it can write into /opt/everjust)
rsync -az --rsync-path="sudo rsync" -e "ssh -i ~/.ssh/<deploy-key>.pem" \
./addons/website_cd_footer/ \
ubuntu@<box-ip>:/opt/everjust/platform/addons/website_cd_footer/
# 2) scoped upgrade of just that module, no other services
ssh -i ~/.ssh/<deploy-key>.pem ubuntu@<box-ip> '
cd /opt/everjust/platform/deployment &&
sudo docker compose run --rm --no-deps odoo \
odoo -d connectdomain -u website_cd_footer --stop-after-init --no-http'# 3) restart the live container so the compiled cache picks up the new arch
ssh -i ~/.ssh/<deploy-key>.pem ubuntu@<box-ip> 'docker restart deployment-odoo-1'
Then re-grep the on-box file (step above) to confirm the change actually landed before you call it done.
8. Avoid a deploy collision (multiple agents share this box)
Concurrent "Deploy to Production" runs race and can revert each other's changes and reset module state. BEFORE any DB or nginx mutation, check for an in-flight deploy and wait for idle:
gh run list --workflow "Deploy to Production" --limit 5
# proceed only when none are status=in_progress or status=queued
On the shared/opt/everjust/platform checkout, scope git add to specific files, never git add -A or git add <dir> (you will stage other sessions' uncommitted work). Prefer an isolated worktree off origin/master:
git -C /opt/everjust/platform fetch origin
git -C /opt/everjust/platform worktree add /tmp/wt-$$ origin/master
# work in /tmp/wt-$$, add only your files, then remove the worktree when done
9. Reload nginx after a config change (restart, don't just reload)
Nginx configs live at /opt/everjust/platform/deployment/nginx/*.conf, bind-mounted into deployment-nginx-1. GOTCHA: after a CI deploy replaces the config file (new inode), the running container stays pinned to the stale inode, an nginx -s reload reads the old file. Test then restart:
nginx -t first so you never restart into a broken config.
Pitfalls
Forgetting env.cr.commit(). The odoo shell does NOT auto-commit. Your writes look applied inside the script (you can even read them back), then vanish when the shell exits, the transaction rolls back. Every mutating shell script must end with env.cr.commit(). This is the single most common shell-ops mistake.
Editing the generic view instead of the COW fork. A layout view often exists twice: generic (website_id=False) and a copy-on-write fork (website_id=1) that OVERRIDES it and is what renders. Editing the generic one changes nothing live (the footer 3753-vs-4091 case cost real hours). Before editing any header, footer, or layout view, search both and edit the website_id=1 fork if it exists (Recipe 3).
Expecting an EDIT to render without a restart. A brand-new view renders on the next request; an edit to an already-compiled template does NOT until the compiled-QWeb cache is dropped, and cache-clear or signals are unreliable here. Fix: docker restart deployment-odoo-1, then re-fetch (Recipe 4).
Trusting a green CI deploy without verifying the box file. The "Deploy to Production" rsync sometimes silently doesn't land the file, the run is green, the module "loads," and the box file is still stale. Always docker exec ... grep the on-box file for your change before declaring the deploy done (Recipe 7).
Running a DB or nginx op during a concurrent deploy. Multiple agents share this box; concurrent "Deploy to Production" runs race, revert each other's changes, and reset module state. Check gh run list for in_progress or queued first and wait for idle. On the shared checkout, never git add -A or git add <dir> (you'll stage other sessions' work), scope to your files or use a git worktree (Recipe 8).
Setting numbercall on an Odoo 19 ir.cron. The field was removed in Odoo 19; including it in create({...}) raises. Omit it. Use interval_number / interval_type / nextcall / active only (Recipe 6).
Trying outbound HTTP (or any import) from an ir.cron code block. Cron code runs under safe_eval, no import, no network. An IndexNow POST or any HTTP call cannot run there; it silently isn't allowed. Put outbound-HTTP jobs in a system cron on the box (shell script plus crontab), and keep ir.cron code to pure ORM.
Conf-only invocation that never connects. On this box, odoo shell -c /etc/odoo/odoo.conf alone does not reach Postgres, you must pass --db_host=db --db_user=<db-user> --db_password="$ODOO_DB_PW" inline as well (Recipe 1). If the shell hangs or errors on connect, this is almost always why.
Quoting hell across ssh to docker exec to Python. Nested quotes and <, >, $ in an inline arch get mangled across the hops. Transfer scripts or arch as base64 and decode on the far side or in Python (Recipe 1). Don't try to escape a multi-line XML arch by hand on the command line.
nginx -s reload after a CI config swap. The running deployment-nginx-1 is pinned to the replaced file's old inode; a reload reads stale config. nginx -t then docker restart deployment-nginx-1 (Recipe 9).
Dropping to the shell for something the MCP does cleanly. The shell is unaudited, not COW-safe by default, and easy to get wrong (see every pitfall above). For page content, publish/menu/redirect, and per-page SEO, prefer [[everjust-agent-mcp]] via [[everjust-website]] / [[everjust-website-seo]]. Use the shell only for what the MCP truly can't do: bulk writes, infra and layout views, cache eviction, cron creation, and addon file deploys.
Editing a branded or swept record by hand and expecting it to stick. Some mail.template bodies and branded views are re-swept on the next module -u ([[everjust-platform]] invariant). A live shell edit to a swept record is not durable, put the change in the module data and deploy it (Recipe 7), don't fight the sweep.
See also
[[everjust-agent-mcp]]: the PREFERRED path: the search/get/create/update/call plus website_* MCP tool surface, confirm gates, role-bounding. Use this first; drop to the shell only when it can't.
[[everjust-platform]]: the invariants that still bind you on the shell: one-DB-per-tenant, /odoo debrand, group_ids not groups_id, everjust.mail.*, re-sweep on -u, never self-escalate.
[[everjust-website]]: page CONTENT and layout via the MCP (Tailwind-QWeb arch, COW, publish/menu/redirect). Same COW concept as Recipe 3, done the COW-safe MCP way.
[[everjust-website-seo]]: per-page SEO metadata, sitemap membership, social and JSON-LD, IndexNow, plus the GEO and AI-discoverability content program (llms.txt), all via the MCP. Prefer it over shell-writing website_meta_*, and it owns the sitemap-cache strategy behind Recipe 5.
[[everjust-website-snippets]]: the on-brand s_cd_* content snippets used inside page arch; reach for these before hand-writing raw QWeb sections in Recipe 2.
[[github-actions-ec2-deploy]]: the "Deploy to Production" pipeline this skill uses and recovers; go there to author or debug the workflow itself.
[[everjust-website-geo-content]]: what to BUILD with the shell publishing recipes here, GEO content clusters and per-page schema, and the sitemap-freshness cron this skill installs.
[[everjust-website-customization]] — durable view/CSS/behavior customization FROM the shell (COW inherit views, website.custom_code_head and its Markup trap, base_automation). THIS skill is the DEPLOY-PIPELINE and BOX-OPS layer around it: the CI deploy plus manual-rsync recovery, deploy-collision avoidance, the nginx stale-inode restart, ir.cron creation, the sitemap cache. Use that for WHAT to change in a view, this for HOW to deploy and operate safely.