Skip to content

mpy_setup.sh - App Server Setup Script Guide

Introduction

mpy_setup.sh is the idempotent bash script that builds your project on an App Server. It lives at .muppy/mpy_setup.sh in your repository on the server, and the App Server provisioner invokes it twice during build — once with MPY_SETUP_SH_STAGE=setup (system prerequisites) and once with MPY_SETUP_SH_STAGE=build (the actual project build: ikb install, yarn build, bundle install, …).

The script is the build procedure. Muppy does not run ikb install, yarn build, or any other build step itself — it clones your repo and invokes mpy_setup.sh. Removing a build call from the script removes the build.

Key Characteristics

  • Multi-stage: dispatches on MPY_SETUP_SH_STAGE (setup / build / empty for manual mode)
  • Idempotent: each branch must be safely re-runnable
  • Repo-driven: the source of truth is .muppy/mpy_setup.sh in your git repository (or a mpy.template record when you don't control the repo)
  • Muppy-integrated: has access to environment variables from /etc/muppy.env

Two-stage execution model

Why two stages

If the build fails, the developer still needs a usable box to debug it. Splitting the script in two — system prerequisites first, project build second — guarantees that even a broken build leaves behind a server with code-server installed, repo cloned, PostgreSQL ready, and system packages in place. The dev can SSH in and fix the script in place.

The contract

The provisioner invokes mpy_setup.sh twice during a fresh provisioning:

Invocation Type of install task MPY_SETUP_SH_STAGE Typical content
1st prereq (system prerequisites) setup apt packages, language runtimes (node, redis, postgres-client, build-essential)
2nd builder (project build) build ikb install, yarn install && yarn build, bundle install, asset compilation

The mapping install_task_type → install_task_stage is set in dev_server_install_task.py:get_invocation_params(): 'prereq' → 'setup', 'builder' → 'build'.

When you re-run the script manually from the GUI (or from a shell), MPY_SETUP_SH_STAGE is empty — that's the *) branch, which should reproduce the end-to-end behaviour (both setup and build outcomes).

The dispatch pattern

case "$MPY_SETUP_SH_STAGE" in
  setup)
    # Runs DURING the 'prereq' phase. The repo is cloned, PostgreSQL is up
    # (DB Phase A: role + database already created), code-server is installed.
    # No build artifacts yet — no venv, no node_modules, no compiled assets.
    # Put system-level installs the build will need: apt packages, language
    # runtimes (node, redis), build tools (postgres-client, build-essential).
    ;;
  build)
    # Runs DURING the 'builder' phase. System prerequisites from `setup` are
    # in place. This is where YOU run the actual build:
    #   - ikb-based Odoo:   ikb install
    #   - Node / Yarn:      yarn install && yarn build
    #   - Python (uv/pip):  uv venv && uv pip install -e .
    #   - Rails / Bundler:  bundle install && bundle exec rake assets:precompile
    # Anything that produces build artifacts goes here.
    ;;
  *)
    # Manual run from the GUI (no stage). Reproduce the end-to-end outcome:
    # both setup and build blocks, in order.
    ;;
esac

Provisioning timeline

mpy_setup.sh doesn't run in isolation — it runs at specific points of the App Server provisioning sequence, with specific things already in place. Knowing the timeline tells you what you can safely assume in each stage.

1.  mkdir App Server directories
2.  Install code-server (non-fatal)             ← dev never locked out, even if build fails later
3.  git clone repo                              ← .muppy/mpy_setup.sh becomes available
4.  Write /etc/muppy.env                        ← env vars resolved before stage setup
5.  DB Init Phase A: PG role + database         ← PG available from stage setup onwards
6.  For each task in prerequisites_install_task_ids (install_task_type='prereq'):
       → invoke 'mpy_setup.sh' task
       → exports MPY_SETUP_SH_STAGE=setup, MPY_APP_DIR, MPY_REPOSITORY_ABS_PATH
       → runs the `setup)` branch of your script
7.  For each task in builder_task_ids (install_task_type='builder'):
       → invoke 'mpy_setup.sh' task
       → exports MPY_SETUP_SH_STAGE=build, MPY_APP_DIR, MPY_REPOSITORY_ABS_PATH
       → runs the `build)` branch of your script
8.  DB Init Phase B: Odoo init / upgrade        ← only for Odoo / ikb projects

What this means concretely:

  • Inside stage setup: PostgreSQL is up, code-server is up, the repo is cloned, /etc/muppy.env is sourced. No venv, no node_modules, no compiled artifacts. Don't try to import Python packages from a venv that doesn't exist yet, don't run odoo-bin, don't run ikb outside its own install path.
  • Inside stage build: system prerequisites from setup are in place. This is where you produce the artifacts. After this stage completes, provision_app_server runs DB Phase B for Odoo projects.
  • There is no separate ikb install step orchestrated by Muppy between the two stages. It's your script that decides what runs in build. A non-ikb project (Node, Rails, …) simply puts its own build there.

Available environment variables

The shell wrapper that invokes your script exports these before calling .muppy/mpy_setup.sh:

Variable Source Stage setup Stage build Manual (*))
MPY_SETUP_SH_STAGE Task wrapper setup build ''
MPY_APP_DIR dev_server.app_abs_path
MPY_REPOSITORY_ABS_PATH dev_server.repo_abs_path

Your script should also source /etc/muppy.env early to get the rest:

Variable Source Description
MPY_REPO_ABS_PATH /etc/muppy.env Absolute repo path (same value as MPY_REPOSITORY_ABS_PATH)
MPY_APP_ABS_PATH /etc/muppy.env Absolute App Server path
APP_HTTP_PORT /etc/muppy.env The only port reachable from the Internet
APP_PRIMARY_URL /etc/muppy.env Public URL of the app
MPY_APP_SERVER_QUALIFIER /etc/muppy.env The App Server qualifier's short alias (e.g. prod-mono); empty if no qualifier set
MPY_APP_SERVER_QUALIFIER_CATEGORY /etc/muppy.env Resolved qualifier category used by provisioning logic (effective_base_category: e.g. Demo → staging, Spark → test); empty if no qualifier set
PGHOST / PGPORT / PGUSER / PGPASSWORD / PGDATABASE /etc/muppy.env PostgreSQL connection (used implicitly by psql, pg_dump, ORM libs)

Plus any custom variable you defined on the App Server's Environment Variables tab — those also land in /etc/muppy.env. Read the file from the server to see the full concrete list.

IKB_DEV_MODE (ikb projects only)

For ikb-based projects, you can offer a developer override:

# At the top of your script, commented:
# IKB_DEV_MODE=1   # uncomment to reinstall ikb editable from source

if [ "${IKB_DEV_MODE:-0}" = "1" ]; then
  # Re-install ikb editable so the developer can modify it in place.
  # Idempotent: remove any prior install before reinstalling.
  ...
fi

This is a developer convenience, not a default behaviour. The user uncomments the line when they want to hack on ikb itself.


Source of truth: where the script lives

You have two options depending on whether you control the repository:

Option A — Private repository (you can commit)

Commit .muppy/mpy_setup.sh directly in your repository. The provisioner clones the repo and runs it. The repository is the source of truth.

Use the GUI editor in the App Server form for iterative development, then commit the working version back to your repo.

Option B — External / Open-source repository (you cannot commit)

Save the script as a mpy.template in Muppy (template type mpysetupscript). The script is stored in the Odoo database and uploaded to .muppy/mpy_setup.sh on the server via mgx_upload_mpy_setup_sh. Running the stages is then on you — call mgx_run_mpy_setup_sh(stage='setup') then (stage='build') after upload. The provisioner only auto-runs the stages when the script lives in the repo (Option A); when it comes from a template, that responsibility passes to the agent. The template is the source of truth. Share templates across your company's servers for consistent deployments.


How AI agents should write a mpy_setup.sh

This is the workflow for an MCP-driven agent (e.g. via the mn25 MCP) authoring a mpy_setup.sh from scratch for a fresh App Server.

Step 1 — Analyse the repo

Read what the project actually needs:

  • package.json / requirements.txt / Gemfile / pyproject.toml — language dependencies
  • buildit.jsonc — present on ikb projects, declares the Odoo build
  • Dockerfile — often lists apt packages and build steps in production form
  • Makefile / build scripts — toolchain requirements
  • README — installation instructions, sometimes the only reliable spec

Step 1b — Check for an existing template

Look for an existing template that fits the repo on mpy.template (type mpysetupscript). Search by specificity descending: source_repository_url first, then stack/tech keywords against code, name, description. Present name / code / description of matches to the user; let them pick or say "write from scratch". Fall back to mpy_setup_default (the multi-stage skeleton) only when nothing fits.

Only multi-stage templates qualify. Verify the candidate dispatches on $MPY_SETUP_SH_STAGE (preview the head if uncertain — see the head-fetching call below). Skip legacy single-stage templates and write from scratch instead.

Preview a candidate's body via the head — read_objects on template_body truncates from the end on huge fields:

read_huge_content(model='mpy.template', id=<id>, field='template_body',
                  offset=0, limit=20)

When you pick a template post-create, set mpy_setup_template_id on the server then mgx_reload_mpy_setup_template to copy the body into the server's field. The provisioner's create-time run used the default skeleton (mpy_setup_default, a no-op until you fill it) — so step 3 actually installs your chosen template.

Step 2 — Classify each step into setup or build

The decision rule:

  • Anything the build will need but doesn't producesetup. Apt packages, language runtimes (node, ruby, python, redis), system tools (postgres-client, build-essential, libxml2-dev, …).
  • The build itself, and anything that produces build artifactsbuild. ikb install, yarn install, yarn build, bundle install, npm ci, asset compilation, gem compilation, code generation steps.

When unsure, ask: "Could this step run before the prerequisites it needs are installed?" If no, it's build.

Step 3 — Write the script

Use the multi-stage skeleton. Make each branch idempotent (see Script Writing Guidelines).

Write the script content to the mpy_setup_sh field of the App Server (write_objects via MCP).

Step 4 — Upload

mgx_upload_mpy_setup_sh(force=True)

Step 5 — Test each stage individually

mgx_run_mpy_setup_sh(stage='setup')
  → poll IMQ message until terminal state
mgx_run_mpy_setup_sh(stage='build')
  → poll IMQ message until terminal state

Running stages individually is faster than re-running the whole script and makes failures attributable to the right phase.

Fast-iteration loop on failure

When debugging, you have two paths:

Path Edit Push to server Run
(a) BDD-first edit the mpy_setup_sh field via write_objects mgx_upload_mpy_setup_sh(force=True) mgx_run_mpy_setup_sh(stage=…)
(b) Server-first (faster) edit .muppy/mpy_setup.sh directly via mgx_edit_file (or code-server / ssh) — (already on the server) mgx_run_mpy_setup_sh(stage=…)

Path (b) skips the upload round-trip per iteration, which adds up when you're trying many small fixes.

With path (b), once the script is green, you MUST sync back:

mgx_download_mpy_setup_sh()
  → pulls .muppy/mpy_setup.sh from the server into the mpy_setup_sh field

Without this step, the BDD field stays stale. The next reprovisioning of any server using this template starts from the OLD field content, and your iterated improvements are lost.

The 3-step iteration loop:

1. mgx_upload_mpy_setup_sh    (push BDD field → server, initial)
2. iterate on the server      (mgx_edit_file then mgx_run_mpy_setup_sh)
3. mgx_download_mpy_setup_sh  (pull server → BDD field, once green)

The mgx_exec MCP method rejects any command that references mpy_setup.sh. This is intentional — running the script via mgx_exec would bypass IMQ tracking. Use mgx_run_mpy_setup_sh(stage=…) always.

Step 6 — Save as template

Goal: maximize template reuse across servers, agents, projects. Save whenever it's worth reusing — typically after writing one from scratch in step 3, or when the user asks. source_repository_url is the discovery key future agents search on, don't omit it.

mgx_create_template_from_script(
    name="<project> - mpy_setup.sh",
    source_repository_url="<the repo URL>",
    description="<short description>",
)

On code collision with an existing template: force_update=True for a separate lineage, or mgx_update_template_from_script() to evolve the template you started from in place.


Script writing guidelines

Script structure (multi-stage skeleton)

#!/bin/bash
set -euo pipefail

# IKB_DEV_MODE=1   # ikb projects only: uncomment to reinstall ikb editable

# === Common (runs in every invocation) ===
# Source Muppy environment (PG*, APP_HTTP_PORT, MPY_REPO_ABS_PATH, ...)
if [ -f /etc/muppy.env ]; then
    set -a
    source /etc/muppy.env
    set +a
fi

cd "${MPY_REPOSITORY_ABS_PATH:-${MPY_REPO_ABS_PATH}}"

# === Stage dispatch ===
case "${MPY_SETUP_SH_STAGE:-}" in
  setup)
    echo "=== mpy_setup.sh stage=setup ==="
    sudo apt-get update
    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
        build-essential postgresql-client redis-server
    # Install node 22.x if missing
    if ! command -v node &>/dev/null; then
        curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
        sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
    fi
    ;;
  build)
    echo "=== mpy_setup.sh stage=build ==="
    # Project build. Replace with your equivalent.
    # ikb project:
    #   ikb install
    # Node/Yarn:
    #   corepack enable && yarn install --frozen-lockfile && yarn build
    # Rails:
    #   bundle install && bundle exec rake assets:precompile
    ;;
  *)
    echo "=== mpy_setup.sh manual run (stage='') ==="
    # Reproduce both stages end-to-end.
    MPY_SETUP_SH_STAGE=setup "$0"
    MPY_SETUP_SH_STAGE=build "$0"
    ;;
esac

echo "=== mpy_setup.sh stage=${MPY_SETUP_SH_STAGE:-manual} completed ==="

The *) branch re-invokes the script with each explicit stage — that's the simplest way to keep the manual-run behaviour aligned with the provisioner.

Idempotency rules

Each branch is replayed on every re-run. Check before acting — don't assume a previous run did the work.

# Safe patterns
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y redis-server  # apt is idempotent
command -v node &>/dev/null || install_node                          # guarded install
mkdir -p /var/lib/myapp/data                                          # mkdir -p
[ -f .env ] || cat > .env <<EOF ... EOF                               # write only if missing

# Unsafe patterns (avoid)
curl -O https://...                                                   # re-downloads every time
echo "MY_VAR=x" >> /etc/some.conf                                     # appends on every run
> some-file                                                           # truncates unconditionally

No stdin — unattended runs only

The script runs in an SSH session with no TTY. Anything that waits for input hangs forever.

  • Use DEBIAN_FRONTEND=noninteractive for apt
  • Avoid bare REPLs (python, mysql, psql without -c)
  • For outbound SSH, set -o StrictHostKeyChecking=accept-new

set -euo pipefail + scoped || true

set -euo pipefail is the default. Never wrap a whole block in set +e; guard individual commands that are allowed to fail:

optional_check || true
curl -fsSL https://... > /tmp/x || { echo "[WARN] skipped"; true; }

Manual execution from the GUI

The Setup Script tab in the App Server form provides:

Button Action
Reload from Template Overwrites current script with the selected template content
Save to Template Saves current script back to the selected template (non-system only)
Upload to Server Pushes script to .muppy/mpy_setup.sh on the server
Download from Server Retrieves script from server (useful after manual edits via code-server)
Run mpy_setup.sh script Executes the script asynchronously (manual mode: MPY_SETUP_SH_STAGE='', runs the *) branch)
Save as New Template Creates a new mpy.template record from the current script content

To re-run a specific stage rather than the whole script, call the MCP method directly:

mgx_run_mpy_setup_sh(stage='setup')   # re-run just setup
mgx_run_mpy_setup_sh(stage='build')   # re-run just build
mgx_run_mpy_setup_sh()                # manual mode, runs *) branch

The GUI button always invokes stage='' (manual mode).

Template selector

Select from available mpysetupscript type templates:

  • System templates (read-only): shared reference scripts (Default, Outline, …)
  • User / company templates: your own saved scripts, editable

Warnings shown by the UI

  • The selected template differs from the Application Definition's default
  • The script content has been modified relative to the template (options to save back or reload from template)
  • A system template is selected but cannot be modified (suggests duplicating)

Known pitfalls

Five rules. Hitting any of them either hangs the IMQ task or breaks re-runs.

1. Never leave a long-running process behind

The script must return. Any command that forks a background process and doesn't wait for it (build-tool daemons, REPLs, long-polling clients, tail -f, watch) leaves the shell hanging and the IMQ task stuck in wip. Common offenders: ./gradlew (without --no-daemon), ./sbt (REPL), npm run dev / watch, docker run (without --rm), background jobs ending in &.

One-shot invocations of the same tools are safe: npm ci, npm run build, mvn package, ./gradlew build --no-daemon all run to completion and exit cleanly. The dangerous pattern is the lingering fork, not the tool itself.

Never call a build tool in a "summary / version print" tail (e.g. ./gradlew --version at the end of the script) — it's the most common cause of stuck IMQ tasks.

2. Every step must be re-runnable

See Idempotency rules above. The script gets replayed on every iteration. Check before acting — don't trust that "it already ran once".

3. No stdin — unattended runs only

See No stdin — unattended runs only above.

4. set -euo pipefail + scoped || true

See set -euo pipefail + scoped || true above.

5. Stage-specific traps

  • Don't createdb in stage setup — DB Phase A already created the role and the database. createdb will fail with database "<name>" already exists.
  • Stage setup runs BEFORE the build — no venv, no node_modules, no compiled assets. Don't import Python packages from py3x/, don't run odoo-bin, don't run ikb outside its own install path.
  • Stage build runs AFTER setup — the prerequisites are in place, but the build artifacts are what THIS branch is supposed to produce. The script is the author of the build.
  • ikb install is inside mpy_setup.sh build — Muppy doesn't call ikb itself. Removing the call from the script removes the build.
  • Manual GUI run = empty stage = the *) branch. Make it idempotent AND complete. The recommended pattern (re-invoking the script with each explicit stage) trivially achieves both.

Recovering from a stuck task

When the IMQ task sits in state='wip' with no log progress for more than ~2 minutes, rule 1 is almost always the cause. Recovery loop (all via mgx_exec on your own App Server — ACL-scoped, safe to use):

  1. Inspect: ps auxf, then pgrep -af '<tool name>' for the usual suspects.
  2. Kill with sudo: sudo kill -TERM <pid> (escalate to sudo kill -KILL after ~10s if ignored). Use sudo because setup scripts routinely fork root-owned processes via sudo apt, sudo -E bash -, etc. — plain kill fails with "Operation not permitted" on those.
  3. Fix the script: remove the offending call, wrap in timeout, or add || true.
  4. Re-run: mgx_upload_mpy_setup_sh(force=True) then mgx_run_mpy_setup_sh(stage='setup') (or stage='build' if the stuck task was in build).

Example: Outline Wiki Setup

The Outline system template (outline-mpy-setup-sh) is a complete real-world example, restructured around the two-stage model.

Stage setup (prerequisites)

  1. System packages: build-essential, postgresql-client, redis-server
  2. Node.js 22.x via NodeSource (guarded by command -v node)
  3. Yarn via corepack enable (idempotent)
  4. File storage directory at /var/lib/outline/data (mkdir -p + chown)
  5. Environment configuration (.env generated from /etc/muppy.env, written only if absent)

Stage build (project build)

  1. yarn install --frozen-lockfile — install dependencies
  2. yarn build — production build of the frontend assets
  3. yarn db:migrate — schema migrations against the PG instance already provisioned by DB Phase A

Each step includes idempotency checks (skip if already done) and clear logging. This template demonstrates best practices for writing production-ready multi-stage setup scripts.