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 —
commandis 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 --queue='*'
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'sPATHdoes not reliably include/usr/binfor non-login services. Usewhich javaon the target server to find the right path. A barejavawill fail at startup withstatus=203/EXEC. - Explicit relative (
./bin/start.sh): used as-is, resolved from the app directory (the repo root, set as systemdWorkingDirectory). - Bare relative (
bin/start.sh): auto-prefixed with./and resolved from the app directory. Same effective behavior as the explicit form.
- Absolute path (
- Shell metacharacters are forbidden:
;,|,&,`,$() - 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 andsub_state= running for each service - Click "Refresh All" to update status from the host
Step 5: Troubleshoot¶
If a service is failed or inactive:
- Click "Logs" on the failed service to view journalctl output
- Fix the issue (command path, permissions, missing dependencies)
- Edit
.muppy/services.ymlif needed - 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 all registered systemd units |
| Stop All | Stop all registered systemd units |
| Restart All | Restart all registered systemd units |
| 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 |
| Documentation | Open this guide in a new tab |
Running Services List¶
Shows all registered mpy.systemd_unit records with:
- Status indicators (
active_state,sub_statewith 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) ormanual(from App Definition/admin) - Unit code, class, launcher script, options, restart policy
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-workert --queue='*'
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