M3 Plugin Runner Service
Container lifecycle management service for M3 Forge plugins.
Overview
M3 Plugin Runner is a Python/FastAPI service that manages Docker containers for user-deployed plugins. It provides a “push code, it runs” experience similar to Hugging Face Spaces or Lightning AI.
Key Features
- Multi-App Support: Gradio, Streamlit, FastAPI, static HTML, custom Docker
- Git Integration: Clone and deploy directly from Git repositories
- Zip Upload: Deploy from uploaded zip files
- Dynamic Routing: Traefik integration for automatic subdomain routing
- Container Lifecycle: Create, start, stop, redeploy, delete
- Log Streaming: Access container logs via API
- Health Checks: Monitor runner and Docker daemon health
Architecture
The frontend never talks to the runner directly. Lifecycle operations flow through the Node.js backend, while the rendered webapp is embedded back into the Studio UI via iframe session access.
Communication Flow
- User creates webapp via m3-forge UI
- Frontend calls tRPC
webapp.createprocedure - Backend stores metadata in PostgreSQL and calls m3-plugin-runner HTTP API
- M3 Plugin Runner clones code, builds Docker image, starts container
- Traefik automatically routes
{slug}.apps.marie.localto the container - User accesses webapp via subdomain URL
Quick Start
Prerequisites
- Python 3.11+
- Docker with Docker Compose
- Access to Docker socket (for container management)
Development
From monorepo root
pnpm runner:install # Install Python dependencies in virtualenv
pnpm runner:dev # Run development server with hot reloadDocker Deployment
From monorepo root
pnpm runner:build # Build Docker image
pnpm runner:up # Start services (runner + Traefik)
pnpm runner:logs # View logs
pnpm runner:down # Stop servicesStudio Runner Registration
Create a pending app runner registration in Studio, copy the one-time token, and start the runner with
MARIE_STUDIO_URL plus MARIE_RUNNER_REGISTRATION_TOKEN.
For managed deployments, the runner control URL is generated from deployment metadata:
| Deployment | Control URL source |
|---|---|
| Local Python process | http://localhost:<PORT> when MARIE_STUDIO_URL is local |
| Standalone runner Compose | Compose injects MARIE_RUNNER_CONTROL_HOST=localhost and the published runner port |
| M3 Forge all-in-one Compose | Compose injects the Docker service host m3-plugin-runner:8080 |
| Kubernetes/Helm | Helm injects the runner Service name, namespace, and service port |
| External host | Set explicit MARIE_RUNNER_CONTROL_URL |
PUBLIC_BASE_URL and PUBLIC_ROUTE_PREFIX are different. They describe the browser-facing app route, such as
http://localhost:8880/apps/my-app. The control URL is only for Studio API to call the runner API.
Configuration
Environment variables (can be set in .env file):
| Variable | Default | Description |
|---|---|---|
HOST | 0.0.0.0 | Server bind address |
PORT | 8080 | Server port |
DEBUG | false | Enable debug mode and hot reload |
APPS_DIR | /data/apps | Directory for app data storage |
TEMPLATES_DIR | ./templates | Path to Dockerfile templates |
DOMAIN | apps.marie.local | Base domain for webapp subdomains |
DOCKER_NETWORK | marie-network | Docker network for containers |
TRAEFIK_ENTRYPOINT | web | Traefik entrypoint name |
PUBLIC_BASE_URL | http://localhost:8880 | Public base URL for deployed webapps |
PUBLIC_ROUTE_PREFIX | /apps | Public route prefix under PUBLIC_BASE_URL |
MARIE_STUDIO_URL | “ | Studio API URL used for registration and heartbeat |
MARIE_RUNNER_REGISTRATION_TOKEN | “ | One-time registration token copied from Studio |
MARIE_RUNNER_CONTROL_URL | “ | Explicit control API URL for standalone/external installs |
MARIE_RUNNER_CONTROL_HOST | “ | Runtime-fabric, Compose, or Helm generated control host |
MARIE_RUNNER_CONTROL_PORT | PORT | Runtime-fabric, Compose, or Helm generated control port |
MARIE_RUNNER_SERVICE_NAME | “ | Compose or Kubernetes Service name used to derive the control URL |
MARIE_RUNNER_SERVICE_NAMESPACE | “ | Kubernetes namespace used with MARIE_RUNNER_SERVICE_NAME |
RUNNER_API_KEY | “ | API key for authentication (empty = disabled) |
DEFAULT_MEMORY_LIMIT | 2g | Default container memory limit |
DEFAULT_CPU_LIMIT | 1.0 | Default container CPU limit (cores) |
API Reference
Create Webapp
POST /apps
Content-Type: multipart/form-dataForm Fields:
id(required): Unique identifier (UUID)name(required): Display nameapp_type:gradio|streamlit|fastapi|static|docker(default:gradio)git_url: Git repository URL (required if nocodefile)git_branch: Git branch (default:main)port: Container port (default:7860)env: JSON-encoded environment variables (default:{})code: Uploaded zip file (required if nogit_url)
Response:
{
"id": "abc123",
"name": "My Webapp",
"status": "running",
"url": "https://abc123.apps.marie.local"
}List Webapps
GET /appsGet Webapp Status
GET /apps/{app_id}Delete Webapp
DELETE /apps/{app_id}Start / Stop / Redeploy
POST /apps/{app_id}/start
POST /apps/{app_id}/stop
POST /apps/{app_id}/redeployGet Logs
GET /apps/{app_id}/logs?tail=100Health Check
GET /healthApp Types
Gradio
Python apps using Gradio .
Expected structure:
my-gradio-app/
├── app.py # Must contain Gradio interface
└── requirements.txt # Optional dependenciesExample:
import gradio as gr
def greet(name):
return f"Hello, {name}!"
demo = gr.Interface(fn=greet, inputs="text", outputs="text")
demo.launch()Troubleshooting
If RUNNER_API_KEY is set, all requests need X-API-Key header. Set RUNNER_API_KEY= (empty) to disable auth for development.
Container fails to start
- Check logs:
pnpm runner:logsorGET /apps/{id}/logs - Verify Docker network exists:
docker network ls | grep marie-network - Check port conflicts:
docker psto see running containers
Build fails
- Ensure
requirements.txthas valid dependencies - Check for syntax errors in app code
- For custom Dockerfiles, ensure they’re valid
Cannot access webapp URL
- Verify Traefik is running:
docker ps | grep traefik - Check DNS/hosts file for
*.apps.marie.local - Verify container is running:
GET /apps/{id}