Skip to content

services.yml - Declarative Service Definitions Guide

Introduction

services.yml is a Procfile-like YAML file that declares which processes your application needs to run as systemd services. Place it at .muppy/services.yml in your git repository, and Muppy automatically creates, configures, and manages the corresponding systemd units on your App Server.

Key Characteristics

  • Declarative: Describe what to run, Muppy handles how (systemd unit files, enable, start)
  • Idempotent sync: Re-deploying updates existing services, creates new ones, removes deleted ones
  • Auto-detected: Processed automatically during server provisioning (except dev-category servers)
  • Procfile-inspired: Minimal syntax — command is the only required key

Philosophy: Source of Truth

Private Repositories (you can commit)

Commit .muppy/services.yml directly in your repository. It is automatically processed during server provisioning. The repository is the source of truth.

Open Source / External Repositories (you cannot commit)

Use MCP tools or the GUI "Deploy Services" button to trigger sync manually. The AI agent can write the file via mgx_exec and deploy it.


Quick Start

1. Create the file

Add .muppy/services.yml to your repository:

services:
  gui:
    command: bin/mpy-srv --workers=4
  cron:
    command: bin/mpy-srv imq-worker --queues='*'
    restart: always

That's it. command is the only required key.

2. Enable "Manage Services"

In the Services tab, ensure the "Manage Services" toggle is ON. For dev-category servers, it defaults to OFF — flip it to enable service management.

3. Deploy

  • Automatic: Happens during server provisioning/setup (non-dev qualifiers)
  • Manual: Click "Deploy Services" in the Services tab, or call mgx_configure_systemd_units_from_definitions() via MCP

4. Verify

Check the Services tab — your services should appear with active_state: active and sub_state: running.

If a service failed, click "Logs" to view journalctl output and diagnose the issue.


Complete Reference

Service Keys

Each key under services: becomes a systemd unit. The key name is the unit code (e.g., gui, cron, worker).

Constraints on unit codes:

  • Only lowercase alphanumeric characters: [a-z0-9]
  • Must be unique within the file
  • Generated unit name: mpy_{type}_appsrv_{identifier}_{unit_code}.service

Supported Configuration Keys

Key Required Default Description
command Yes — Executable + arguments (relative to app directory)
type No service service (long-running) or timer (scheduled)
restart No on-failure Systemd Restart= directive
restart_sec No 1s Systemd RestartSec= directive — delay before restarting
env No EnvironmentFile=/etc/muppy.env List of Environment= or EnvironmentFile= directives

Restart Policies

Value Description
on-failure Restart only on unclean exit (default)
always Always restart, regardless of exit status
no Never restart
on-abnormal Restart on signal, timeout, or watchdog
on-success Restart only on clean exit
on-abort Restart only on signal
on-watchdog Restart only on watchdog timeout

Restart Delay (restart_sec)

restart_sec maps to systemd's RestartSec= directive — the delay before restarting a crashed service. Muppy defaults to 1s instead of systemd's native ~100ms because the native default causes restart storms when ports take longer to free (common on JVM, Python ASGI, and Node apps).

services:
  java_api:
    command: /usr/bin/java -jar build/libs/app.jar
    restart: always
    restart_sec: 20s          # JVM apps: socket often held for several seconds
  quick_worker:
    command: ./worker.sh
    restart_sec: 1s           # default — fast workers can restart quickly

Accepted values: 500ms, 1s, 20s, 2min, or any systemd time span.

Tip

If a service enters auto-restart then failed with "Address already in use", bump restart_sec to 10s or 20s to give the old socket time to release.

Environment Directives

The env key accepts a list of systemd environment directives:

services:
  myapp:
    command: python app.py
    env:
      - "EnvironmentFile=/etc/muppy.env"
      - "Environment=NODE_ENV=production"
      - "Environment=PORT=8080"

Info

EnvironmentFile=/etc/muppy.env is always included even if not explicitly listed. This provides PostgreSQL connection variables (PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGPORT) and other Muppy environment variables.

Command Rules

  • First token = launcher script, rest = options.
  • Path resolution:
    • Absolute path (/usr/bin/java): used as-is. Required for system binaries (java, python, node, ruby, …) — systemd's PATH does not reliably include /usr/bin for non-login services. Use which java on the target server to find the right path. A bare java will fail at startup with status=203/EXEC.
    • Explicit relative (./bin/start.sh): used as-is, resolved from the app directory (the repo root, set as systemd WorkingDirectory).
    • Bare relative (bin/start.sh): auto-prefixed with ./ and resolved from the app directory. Same effective behavior as the explicit form.
  • Shell metacharacters are forbidden: ;, &, `, $() — i.e. command chaining and command substitution. |, (, ) are allowed so an argument can carry a regex-alternation value (e.g. --queues='(a|b|c)'): services.yml commands become systemd ExecStart= lines, which systemd parses itself and exec()s with argv — there is no shell, so a bare |/(/) is a literal argument character, never interpreted. The real injection vectors ($(...), backticks, ;, &) stay blocked.
  • The service runs as the App Server's system user (not root)
services:
  web:
    command: bin/mpy-srv --workers=4         # bare relative -> auto ./
  worker:
    command: ./scripts/worker.sh             # explicit relative
  java_app:
    command: /usr/bin/java -jar build/libs/app.jar   # absolute (required for java)

Merge Behavior

  • Definitions from services.yml (source=services_yml) never override admin-defined units (source=manual)
  • Removing a service from the file stops and removes the corresponding systemd unit on the next sync
  • Re-adding a removed service recreates it

Timer Services

Timers are scheduled tasks — the systemd equivalent of cron jobs. Set type: timer and provide at least one scheduling directive.

Example

services:
  backup:
    command: scripts/backup.sh
    type: timer
    on_calendar: "*-*-* 02:00:00"
    persistent: true

  cleanup:
    command: scripts/cleanup.sh
    type: timer
    on_calendar: "weekly"

Timer-Specific Keys

Key Default Description
on_calendar — Systemd calendar expression (see below)
on_boot_sec — Run N time after boot (e.g., 5min)
on_unit_active_sec — Run periodically after last activation (e.g., 1h)
on_unit_inactive_sec — Run after last deactivation
persistent false Catch up missed runs when system was off
accuracy_sec 1min Timer accuracy (lower = more precise)
randomized_delay_sec — Add random delay to prevent thundering herd

Warning

At least one scheduling directive (on_calendar, on_boot_sec, on_unit_active_sec, or on_unit_inactive_sec) is required for timers.

OnCalendar Syntax

Expression Meaning
daily Every day at midnight
hourly Every hour
weekly Every Monday at midnight
*-*-* 02:30:00 Every day at 02:30
Mon *-*-* 09:00:00 Every Monday at 09:00
*-*-01 00:00:00 First day of every month

Workflow: Setting Up Services

Step 1: Analyze Your App

Identify what processes your application needs:

  • Web server: Serves HTTP requests (e.g., Odoo GUI workers, Node.js server)
  • Background workers: Process async tasks (e.g., Celery, Odoo CRON workers)
  • Scheduled tasks: Periodic jobs (e.g., backups, cleanups)

Step 2: Write services.yml

Create .muppy/services.yml in your repository. Start minimal:

services:
  web:
    command: bin/start-web.sh --port 8000

Add more services as needed.

Step 3: Enable and Deploy

First, ensure the "Manage Services" toggle is ON in the Services tab (OFF by default for dev servers).

Then choose one:

  • GUI: Open your App Server → Services tab → click "Deploy Services"
  • MCP: Call mgx_configure_systemd_units_from_definitions() on your dev server
  • Automatic: Happens during provisioning/setup (non-dev qualifiers only)

Step 4: Verify

In the Services tab:

  • Check active_state = active and sub_state = running for each service
  • Click "Refresh All" to update status from the host

Step 5: Troubleshoot

If a service is failed or inactive:

  1. Click "Logs" on the failed service to view journalctl output
  2. Fix the issue (command path, permissions, missing dependencies)
  3. Edit .muppy/services.yml if needed
  4. Click "Deploy Services" again to redeploy

AI Agent Workflow

When working with an AI assistant (via MCP), the typical workflow is:

User: "I want my app to run as a background service"

1. AI reads dev_server → checks activate_systemd_services
2. If False: write_objects(values={"activate_systemd_services": True})
3. AI checks: mgx_exec(command="cat .muppy/services.yml")
4. AI writes the file:
   mgx_exec(command="mkdir -p .muppy && cat > .muppy/services.yml << 'EOF'
   services:
     web:
       command: bin/start-web.sh --workers=4
   EOF")
5. AI deploys: mgx_configure_systemd_units_from_definitions()
6. AI verifies: reads systemd_unit_ids → checks active_state
7. If failed: mgx_journalctl() → diagnoses → fixes → redeploys

The AI can read the full format reference from mpy.template with code mpy-services-yaml-default.


UI Reference: Services Tab

Manage Services Toggle

The "Manage Services" toggle (activate_systemd_services) is the master switch. When OFF, all controls below are hidden and no service operations are possible. For dev-category servers, it defaults to OFF — enable it to start managing services.

Buttons

Button Action
Start All Start the host's units named mpy_*_appsrv_* (on a host shared by several App Servers, theirs too); the Code Server and the other registered units are left alone
Stop All Stop the host's units named mpy_*_appsrv_* (on a host shared by several App Servers, theirs too); the Code Server and the other registered units are left alone
Restart All Restart the host's units named mpy_*_appsrv_* (on a host shared by several App Servers, theirs too); the Code Server and the other registered units are left alone
Refresh All Refresh status from host (systemctl show)
Deploy Services Sync .muppy/services.yml + deploy all definitions
Import custom Services Discover and import existing host services. An imported unit belongs to the host: it is listed on every App Server of that host, and the buttons above, a deployment and a database restore leave it alone, unless its name matches mpy_*_appsrv_*
Documentation Open this guide in a new tab

Running Services List

Shows all registered mpy.systemd_unit records with:

  • Status indicators (active_state, sub_state with color badges)
  • Per-service buttons: Start, Stop, Restart, Refresh, Logs, Unregister

Service Definitions List

Shows mpy.dev_server_systemd_unit_definition records — what should be deployed:

  • Source badge: services_yml (from .muppy/services.yml) or manual (from App Definition/admin)
  • Unit code, class, launcher script, options, restart policy

The list is read-only here: it shows what will be deployed, and you change it by editing .muppy/services.yml and deploying again.

A manual definition does not listen to your services.yml

manual means the unit is declared by your App Definition, not by your repository — and on a unit name held by both, the App Definition wins (see Merge Behavior). So adding or editing that service in .muppy/services.yml changes nothing, on this deploy or any later one. Nothing fails either: your entry is skipped, and the deploy reports it only as a skipped (manual) count in its summary.

Two ways out, depending on what you want:

  • Run your own version instead — give it a different service name in .muppy/services.yml. A name the App Definition does not declare is yours, and deploys normally.
  • Change the one that is there — it belongs to the App Definition, so ask whoever operates it. Point them at the unit code shown in this list.

A service you added yourself always shows services_yml. Seeing manual on a service you thought you owned is the signal that the App Definition declares it too.


Relationship with mpy_setup.sh

Both files live in .muppy/ but serve different purposes:

mpy_setup.sh services.yml
Purpose Install system dependencies Declare running processes
When Runs once during provisioning Synced at each deploy
Examples apt-get install nodejs, pip install, Redis setup bin/mpy-srv --workers=4, python worker.py
Format Bash script (imperative) YAML (declarative)
Idempotent Script must check before acting Sync is inherently idempotent

Typical flow: mpy_setup.sh installs Node.js → services.yml declares node server.js as a service.


Examples

Odoo Application (GUI + CRON)

services:
  gui:
    command: bin/mpy-srv --workers=4
    restart: on-failure
  cron:
    command: bin/mpy-srv imq-worker --queues='*'
    restart: always

Node.js Application (Web + Worker)

services:
  web:
    command: node server.js
    restart: on-failure
    env:
      - "Environment=NODE_ENV=production"
      - "Environment=PORT=8080"
  worker:
    command: node worker.js
    restart: always

Python Application (API + Celery + Scheduled Cleanup)

services:
  api:
    command: python -m uvicorn main:app --host 0.0.0.0 --port 8000
    restart: on-failure
  celery:
    command: celery -A tasks worker --loglevel=info
    restart: always
  cleanup:
    command: python scripts/cleanup.py
    type: timer
    on_calendar: "*-*-* 03:00:00"
    persistent: true