Parts Configuration Reference
This is the complete reference for configuring parts in mpy_meta_config. Use this as the authoritative guide for writing configurations.
Configuration Structure
mpy_meta_config:
parts:
{part_code}: # ≤ 8 characters, unique identifier
type: server # server | worker | cronjob | http
stop_before_upgrade: true # read by Muppy: stopped while a migration Job runs
# Part-specific configuration sections...
# Optional global sections
requires_m2p: "3.1.0" # Minimum m2p version this Parts Config needs
volumesDefinition: [...] # Shared volume definitions
vault_objects: [...] # Vault object declarations
user_manifests: [...] # Custom Kubernetes manifests
Part Types Overview
| Type | Purpose | Use Cases | Key Sections |
|---|---|---|---|
server |
Web applications with HTTP endpoints | Web apps, APIs, frontends | deployment, ports, routes |
worker |
Background processing services | Job processors, message consumers | deployment only |
cronjob |
Scheduled tasks | Cleanup jobs, reports, backups | cronjob |
http |
Route existing services | Expose existing K8s services | routes only |
Server Parts
Server parts are long-running processes that expose network ports and handle HTTP traffic.
Basic Structure
parts:
{part_code}:
type: server # Optional, default type
deployment: # Required - container configuration
# Container settings...
ports: # Required if exposing network ports
# Network port definitions...
host_prefix: "api-" # FQDN prefix for multi-service apps
expose_publicly: true # (Since muppy 14.88) will trigger Public DNS Records generation
routes: # Optional - HTTP routing rules
# Traffic routing configuration...
# Optional sections
volumes: [...] # Volume mounts
envFrom: [...] # ConfigMap/Secret references
securityContext: {...} # Pod security settings
debug_mode_available: ... # Debug configuration
dashboard_name: "..." # Dashboard display name
Host Prefix for Multi-Service Routing
The host_prefix attribute allows you to create unique subdomains for parts when deploying multiple services under a single Installed Package.
How It Works
- Each Installed Package receives one base FQDN (e.g.,
myapp.example.com) - This FQDN is defined in
package_release.main_fqdn - Parts can define
host_prefixto create subdomains by prepending a prefix to the base FQDN
Example: Basic Usage
parts:
api:
host_prefix: "api-" # Results in: api-myapp.example.com
routes:
- name: main
pathPrefix: PathPrefix(`/`)
port_name: http
With base FQDN myapp.example.com, this creates api-myapp.example.com.
Example: Multiple Services
parts:
api:
host_prefix: "api-" # Creates: api-example.com
routes: [...]
admin:
host_prefix: "admin-" # Creates: admin-example.com
routes: [...]
frontend:
# No host_prefix # Uses base: example.com
routes: [...]
Location
The host_prefix is defined at the part level and applies to all routes within that part.
Note:
host_prefixcannot be overridden at the route level. All routes within a part share the same FQDN prefix.
Backward Compatibility
Note: The snake_case
host_prefixis the current standard. The camelCasehostPrefixis still supported for backward compatibility. -host_prefix(snake_case) - recommended ✅ -hostPrefix(camelCase) - supported for legacy configs - If both are present,host_prefixtakes priority
See Also
Deployment Section
The deployment section configures the container runtime:
deployment:
# Container image (optional - can be set in values.docker_image).
# Its TAG decides when this part is replaced - see "When a Part Rolls" below.
docker_image: "myregistry/myapp:v1.2.3"
# Command and arguments
command: ["/app/start"] # Optional - overrides Docker ENTRYPOINT
args: ["--port=8080"] # Optional - container arguments
# Environment variables
env_vars:
- name: LOG_LEVEL
value: "info"
- name: DATABASE_URL
value: "postgresql://..."
# Resource requirements
resources:
requests:
cpu: "100m" # CPU request (millicores)
memory: "128Mi" # Memory request
limits:
cpu: "500m" # CPU limit
memory: "512Mi" # Memory limit
# Replica count (can be overridden by dashboard)
replica_count: 2
Important: The deployment section must contain at least one property, even if empty (e.g., resources: {}).
Ports Section
Define network ports the container exposes:
ports:
- name: http # Port identifier for routing
port: 8080 # Container port number
- name: metrics
port: 9090
- name: grpc
port: 50051
Routes Section
Configure HTTP traffic routing through Traefik:
expose_publicly: true # [OPTIONAL - since muppy 14.88] will trigger Public DNS Records generation. Default = false
host_prefix: "crm-" # [OPTIONAL] Creates subdomain crm-{main-fqdn} for all routes in this part
routes:
- name: main # Route identifier
pathPrefix: PathPrefix(`/`) # Traefik path matching
port_name: "http" # References port name above
middlewares: # Available middleware (not activated by default)
ipwhitelist: true # IP filtering capability
basicauth: true # Basic authentication capability
sticky: # Session stickiness (optional)
cookie:
httpOnly: true
name: SESSIONID
secure: true
sameSite: none
- name: api
pathPrefix: PathPrefix(`/api`) || PathPrefix(`/v1`) # Multiple path prefixes
port_name: "http"
middlewares:
ipwhitelist: true
Complete Server Example
parts:
webapp:
type: server
host_prefix: "app-" # Creates app-{main-fqdn} routing
deployment:
docker_image: "myapp:latest"
args: ["--web-server"]
env_vars:
- name: PORT
value: "8080"
- name: ENV
value: "production"
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "1Gi"
replica_count: 3
ports:
- name: http
port: 8080
- name: metrics
port: 9090
expose_publicly: true # [OPTIONAL - since muppy 14.88] will trigger Public DNS Records generation. Default = false
host_prefix: "crm-" # [OPTIONAL] Creates subdomain crm-{main-fqdn} for all routes in this part
routes:
- name: web
pathPrefix: PathPrefix(`/`)
port_name: "http"
middlewares:
ipwhitelist: true
basicauth: true
- name: metrics
pathPrefix: PathPrefix(`/metrics`)
port_name: "metrics"
middlewares:
ipwhitelist: true
debug_mode_available: basic
debug_config:
args: ["sleep", "infinity"]
dashboard_name: "Web Application"
Worker Parts
Worker parts are background services that don't expose network ports.
Basic Structure
parts:
{part_code}:
type: worker
deployment: # Required - same as server deployment
# Container configuration...
# Optional sections (same as server, except no ports/routes)
volumes: [...]
envFrom: [...]
securityContext: {...}
debug_mode_available: ...
dashboard_name: "..."
Worker Example
parts:
processor:
type: worker
deployment:
docker_image: "myapp:latest"
args: ["--worker", "--queues=high,normal"]
env_vars:
- name: WORKER_CONCURRENCY
value: "4"
- name: REDIS_URL
value: "redis://redis-service:6379"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
replica_count: 2
volumes:
- name: temp-storage
mountPath: "/tmp/processing"
emptyDir: {}
debug_mode_available: basic
debug_config:
args: ["sleep", "infinity"]
resources:
requests:
cpu: "50m"
memory: "128Mi"
dashboard_name: "Background Processor"
CronJob Parts
CronJob parts run scheduled tasks using Kubernetes CronJobs.
Basic Structure
parts:
{part_code}:
type: cronjob # Required for cronjobs
cronjob: # Required - cronjob configuration
# Scheduling and execution settings...
# Optional sections
volumes: [...]
envFrom: [...]
securityContext: {...}
debug_mode_available: basic # Only 'basic' mode supported
dashboard_name: "..."
CronJob Section
cronjob:
# Scheduling
suspend: false # Whether job is suspended (can be controlled by dashboard)
schedule: "0 2 * * *" # Cron expression (daily at 2 AM)
# History management
successfulJobsHistoryLimit: 3 # Keep last 3 successful jobs
failedJobsHistoryLimit: 5 # Keep last 5 failed jobs
# Execution policy
concurrencyPolicy: Forbid # Forbid | Allow | Replace
restartPolicy: OnFailure # OnFailure | Never
# Container settings (same as deployment section)
docker_image: "myapp:latest" # Optional - can be set globally
command: ["/app/cleanup"] # Optional
args: ["--mode=daily"] # Optional
env_vars:
- name: CLEANUP_DAYS
value: "30"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
CronJob Example
parts:
cleanup:
type: cronjob
cronjob:
schedule: "0 3 * * 0" # Weekly on Sunday at 3 AM
suspend: false
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid
restartPolicy: OnFailure
docker_image: "myapp:cleanup"
command: ["/usr/local/bin/cleanup.sh"]
args: ["--verbose", "--dry-run=false"]
env_vars:
- name: RETENTION_DAYS
value: "90"
- name: LOG_LEVEL
value: "info"
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
volumes:
- name: cleanup-logs
mountPath: "/var/log/cleanup"
emptyDir: {}
debug_mode_available: basic
debug_config:
command: ["sleep"]
args: ["infinity"]
dashboard_name: "Weekly Cleanup Job"
HTTP Parts
HTTP parts create routing rules for existing Kubernetes services without deploying new containers.
Basic Structure
parts:
{part_code}:
type: http
host_prefix: "dashboard-" # [OPTIONAL] Creates subdomain dashboard-{main-fqdn} for all routes
expose_publicly: false # [OPTIONAL - since muppy 14.88] will trigger Public DNS Records generation. Default = false
routes: # Required - routing configuration
# Route definitions...
# Optional
dashboard_name: "..." # Dashboard display name
HTTP Routes
expose_publicly: false # [OPTIONAL - since muppy 14.88] will trigger Public DNS Records generation. Default = false
host_prefix: "dashboard-" # [OPTIONAL] Part-level: Creates subdomain dashboard-{main-fqdn}
routes:
- name: external-service
pathPrefix: PathPrefix(`/external`)
service: # External service reference
name: "external-api-service" # K8s service name
port: 8080 # Service port
namespace: "other-namespace" # Optional, defaults to current namespace
middlewares:
ipwhitelist: true
basicauth: true
HTTP Example
parts:
dbadmin:
type: http
expose_publicly: true # [OPTIONAL - since muppy 14.88] will trigger Public DNS Records generation. Default = false
host_prefix: "admin-" # [OPTIONAL] Creates subdomain admin-{main-fqdn}
routes:
- name: database-ui
pathPrefix: PathPrefix(`/dbadmin`)
service:
name: "postgresql-admin"
port: 8080
middlewares:
ipwhitelist: true
basicauth: true
- name: monitoring
pathPrefix: PathPrefix(`/grafana`)
service:
name: "grafana"
port: 3000
namespace: "monitoring"
middlewares:
ipwhitelist: true
dashboard_name: "Database Administration"
Routing to Internal Service Parts
HTTP parts can create additional routes to services from other parts within the same package. This is useful for exposing different ports with different security policies or subdomains.
Use Case: Expose an API on a public subdomain and its metrics endpoint on a separate, secured subdomain.
Pattern:
parts:
api:
type: server
deployment:
docker_image: "myapi:latest"
ports:
- name: http
port: 8000 # Main API endpoint
- name: metrics
port: 9090 # Prometheus metrics (same process)
routes:
- name: api
pathPrefix: PathPrefix(`/`)
port_name: http
# Public access - no middleware restrictions
metrics:
type: http
host_prefix: "metrics-"
# NO ports section - HTTP parts only create routing rules
routes:
- name: prometheus
pathPrefix: PathPrefix(`/`)
service_name: "@{ obj.key }@-api-svc" # Routes to api's service
port_name: metrics # Must match port name in api
middlewares:
ipwhitelist: true # Secured access
Result:
- myapp.example.com → api:8000 (public API)
- metrics-myapp.example.com → api:9090 (secured metrics)
How It Works:
- The
apipart deploys a single-process container that listens on multiple ports - Port 8000: REST API
-
Port 9090:
/metricsendpoint (typical for apps with Prometheus instrumentation) -
The
metricsHTTP part creates an additional IngressRoute - Routes to the same pod as
api(viaservice_name) - Uses a different subdomain (via
host_prefix) - Applies different security (via
middlewares)
Key Points:
- HTTP parts should NOT define
ports - They only create IngressRoutes, not Services or Deployments
-
If
portsis defined, an empty Service is created with no backend -
Use template variables for service names
- Pattern:
service_name: "@{ obj.key }@-{part-code}-svc" - Works because template variables are allowed in quoted strings
-
The package key is injected by Muppy at render time
-
Port name must exist in target service
port_namemust match a port name from the target part'sportslist- Uses Kubernetes service port names, not port numbers
Additional Use Cases: - Admin panels with stricter authentication - Health check endpoints on internal-only routes - Debug endpoints accessible only from specific IPs
Note: This pattern also works incidentally with multi-process containers (e.g., a dev container running both an app and code-server), though single-process containers following the "one process per container" principle are the recommended practice.
See also: - Multi-Service Deployments - Advanced Routing Patterns
Common Configuration Sections
When a Part Rolls
The full picture, label by label, is on Labels and Rollouts.
Kubernetes replaces a pod when its pod template changes. Since m2p 3.2.0 the pod template carries the tag of the image that part runs, and nothing else that moves:
A part is replaced when its own image tag changes. A part whose docker_image did not
change keeps its pods across a Release upgrade - including its memory. That is what lets a
Release carry a part holding state a restart would lose (a key holder, a warmed cache)
next to parts redeployed several times a day.
Image tags must be immutable
Two different images published under the same tag are one version to Kubernetes: the pod template does not change, and the part is not replaced. Pushing over a tag was always a bad idea; from 3.2.0 on it is also a deployment that silently does nothing. Publish a new tag per build.
An image with no tag renders latest - which is what the runtime would pull anyway. A
digest reference (repo@sha256:...) renders sha256-<hex>, truncated to the 63
characters a label value allows.
The first upgrade to 3.2.0 rolls every pod, once
Every pod template changes when a Release moves from 3.1.0 to 3.2.0, so every part is replaced - once. After that, only the rules above apply.
env_change_trigger - rolling on a secret change
A part reading its configuration from a Secret must be replaced when that Secret changes:
envFrom is read at pod start and never again. A part declares what it follows:
parts:
api:
type: server
env_change_trigger: app-config # the vault object this part follows
envFrom:
- name: app-config
secretRef:
deployment:
resources: {}
worker:
type: worker
env_change_trigger: secrets_env_file # the Release-wide {app}-env-{qualifier} envfile
deployment:
resources: {}
holder:
type: worker # follows nothing: never rolled by an env change
deployment:
resources: {}
The part then gets MPY_ENV_CHANGE_TRIGGER in its environment, carrying the hash of what
it follows, so a change of that Secret changes its pod template and replaces it.
- Opt-in, per part. A part that declares nothing carries no trigger and is not rolled when an unrelated secret changes. Before 3.2.0 the hash of the Release envfile sat in every part's environment, so any change to it rolled the whole Release.
- Two forms: the name of a
vault_objectsentry - the same name the part uses inenvFrom- or the literalsecrets_env_filefor the Release-wide envfile Secret. - Naming something that does not exist fails the render, with the part and the object in the message. A trigger that silently resolved to nothing would be a part that never rolls on an env change again, and nothing on screen would say so.
- Requires m2p >= 3.2.0: declare it with
requires_m2p: "3.2.0". An older chart ignores the key, and the part goes back to never rolling on a secret change.
stop_before_upgrade - stopping a part for a migration Job
Read by Muppy, ignored by the chart, like requires_m2p. It says whether this part
must be stopped while a database migration Job runs:
parts:
gui:
type: server
stop_before_upgrade: true # holds a database connection: stopped during the Job
mkeyd:
type: worker
stop_before_upgrade: false # takes a rollout: stays up
A migration must run with no other connection on the database - a running worker holds a
registry the migration invalidates, cron threads take locks, a request served mid-migration
writes against half-migrated tables - and the only reliable way to close every connection
of a part is to have no pod of it. So Muppy renders the flagged parts with a replica count
of 0 (a cronjob part with suspend: true), applies that with a helm upgrade, waits
until their pods are gone, runs the Job, then restores them on the new image in a single
rollout.
- Absent means
true. A part that says nothing is stopped: that is what every Parts Config written before this key already got, and a forgotten part holding a connection open during a migration is the failure this default exists to prevent. - Declare
falsefor a part that opens no database connection - a key holder, a tailscale sidecar, a static file server. It keeps its pod, and whatever it holds in memory, across the upgrade. - The chart renders nothing from this key; it reaches the chart only as the replica count
(or
suspend) Muppy computes, so an older chart is not affected by it.
Environment Variables
Direct environment variables:
deployment:
env_vars:
- name: NODE_ENV
value: "production"
- name: PORT
value: "8080"
- name: FEATURE_FLAG_X
value: "true"
From ConfigMaps and Secrets:
envFrom:
- configMapRef:
name: app-config # References vault object
- secretRef:
name: app-secrets # References vault object
Volumes
Persistent volume:
volumes:
- name: app-data # Must match volumesDefinition name
mountPath: "/data"
readOnly: false # Optional, default false
Debug-only volume (since 3.1.0):
volumes:
- name: dev-workspace
mountPath: "/opt/app"
debug_mode: true # mounted ONLY while this part's dashboard is in debug mode
seed_from: "/opt/app" # optional: seed the volume by copying this image path
debug_mode: true follows the same rule as ports and routes: the entry is rendered only
when dashboards.<part>.debug_mode is true. Because a debug container runs its
debug_config.command (code-server, sleep infinity) rather than the image entrypoint,
mounting over a directory the image populates is harmless in debug mode - and never happens
in run mode.
A second, independent flag exists at Release scope:
volumes:
- name: dev-workspace
mountPath: "/opt/app"
dev_mode: true # mounted only while the RELEASE has dev_mode on
seed_from: "/opt/app"
debug_mode is scoped to one part (its dashboard); dev_mode is scoped to the whole
Release (.Values.dev_mode). Use dev_mode when the volume must be there while the
application runs normally - a long-lived development Release, for instance - and debug_mode
when it is only needed while you are inside the part with a shell or an IDE.
Both may appear on the same entry: every flag present must be satisfied. An entry with no flag at all is always mounted, which is what keeps pre-existing Parts Configs rendering unchanged.
dev_mode: true also works on a volumesDefinition entry, where it makes the PVC itself
conditional - an ordinary Release then provisions no storage at all.
The debug_mode flag has exactly two useful states. The third row below is the one that
misleads:
debug_mode on the entry |
Dashboard in debug | Mounted? |
|---|---|---|
| absent | off / on | yes, always |
false |
off / on | yes, always - identical to absent, NOT "run mode only" |
true |
off | no |
true |
on | yes |
- absent or
falsemeans always mounted. This is what keeps existing Parts Configs rendering exactly as they did before 3.1.0. - There is deliberately no "mounted in run mode only".
- A part with no dashboard entry never mounts a
debug_mode: truevolume - the lookup yields an empty dict, sodebug_modereads as false.
seed_from: <path> adds an initContainer that mounts the volume on /mnt/seed (NOT on
mountPath, so the image content at <path> stays visible) and copies <path> into it.
It seeds once. A stamp file .m2p-seeded marks a populated volume, and a populated volume
is never overwritten. The volume is state, not a cache of the image: whatever you do inside it
cannot be destroyed by a deploy. Refreshing it from a newer image is therefore a deliberate act
- remove .m2p-seeded and restart the pod.
It is safe to declare seed_from on every part sharing the volume. They start together, so
each seeder takes an atomic mkdir lock: exactly one copies while the others wait for the
stamp, giving up after 30 minutes with instructions.
A failed attempt heals itself. No stamp means no successful seed, so the winner of the lock
wipes the volume before copying — a previous attempt killed mid-copy leaves read-only
directories behind (git keeps .git/objects/pack at 0555) that nothing could write into
afterwards. Only a seeder killed hard enough to skip its own cleanup leaves a stale
.m2p-seed-lock; remove it and restart the pods.
The copy archives the directory from its parent and strips that component on extraction, so
no archive entry ever maps to the destination root. Both cp -a and a plain tar -cf - . try
to restore the root's own mode and timestamps, which the pod cannot do on most storage classes
(a hostPath root belongs to root) — and that single failure aborts the whole copy.
A PVC's spec is immutable, and
resource-policy: keepmakes that permanent. Kubernetes refuses to patchaccessModes,storageClassNameorvolumeModeon an existing claim (onlyresources.requestscan grow). Because the annotation keeps the PVC alive acrosshelm uninstalland across a Release leaving dev mode, changing any of those involumesDefinitionon an already deployed Release makes the next upgrade fail:Error: UPGRADE FAILED: cannot patch "<key>-pvc-<name>" with kind PersistentVolumeClaim: spec: Forbidden: spec is immutable after creation except resources.requestsThe only way out is deleting the claim by hand — which destroys whatever it holds:
kubectl -n <namespace> delete pvc <key>-pvc-<name>. SettleaccessModesandstorageClassNamebefore the first deployment; they are not decisions you can revisit cheaply afterwards.Sharing one volume across parts: two ways, and RWX is not always the right one.
ReadWriteOncemeans one node, not one pod — several pods can share an RWO claim as long as they all land on the same node. Nothing corrupts if they do not: alocalPV pins pods to its node through the PV's ownnodeAffinity, and a networked RWO volume refuses the second attachment. The failure is aPendingpod, never a double writer.
Works on Cost accessModes: [ReadWriteMany]only storage classes that implement RWX — elsewhere the PVC stays Pendingforevernone, parts may spread across nodes accessModes: [ReadWriteOnce]+co_locate: trueevery storage class the whole Release sits on one node Pick RWX when the cluster's storage class provides it and you want the parts spread; pick RWO +
co_locatewhen you want the Parts Config to deploy anywhere.
EmptyDir volume:
volumes:
- name: temp-storage
mountPath: "/tmp"
emptyDir: {} # Basic emptyDir
- name: memory-storage
mountPath: "/memory"
emptyDir:
medium: "Memory" # In-memory storage
ConfigMap as file:
volumes:
- name: app-config # ConfigMap vault object
mountPath: "/etc/app/"
subPath: "app.conf" # Mount as single file
Secret as files:
Security Context
securityContext:
fsGroup: 1001 # File system group
runAsUser: 1001 # Run as specific user
runAsGroup: 1001 # Run as specific group
runAsNonRoot: true # Require non-root user
Debug Configuration
Basic debug mode (shell access):
debug_mode_available: basic
debug_config:
command: ["sleep"] # Override container command
args: ["infinity"] # Keep container running
env_vars: # Debug-specific environment
- name: DEBUG
value: "true"
resources: # Debug-specific resources
requests:
cpu: "50m"
memory: "64Mi"
Advanced debug mode (VSCode Server):
debug_mode_available: codr
debug_config:
debug_fqdn_prefix: "code-" # Creates code-{main-fqdn} routing
args: [
"/usr/bin/code-server",
"--bind-addr=0.0.0.0:8765",
"--auth=password"
]
env_vars:
- name: PASSWORD
value: "debug123" # Better to set via dashboard
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
Kubernetes Probes
Readiness probe:
readinessProbe:
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
Liveness probe:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
Startup probe:
startupProbe:
httpGet:
path: /startup
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30 # Allow 150s for startup (30 * 5s)
Disabled probe:
Dashboard for a CronJob
Muppy generates the dashboard values a CronJob part is driven by. Example of what a valid generated block looks like:
dashboards:
test-cron:
suspend: true
command: ["sleep"]
args: ["7"]
schedule: "*/1 * * * *"
resources:
requests:
cpu: 400m
memory: 400M
limits:
cpu: 800m
memory: 800M
Global Configuration Sections
Minimum m2p Version
Declare the oldest m2p chart able to render this Parts Config correctly:
Why this exists. A chart cannot protect you against an older chart. When a Parts Config
uses a key introduced in a later version, older charts do not fail - they silently ignore
the key and render something subtly different. debug_mode on a volume is the textbook
case: on m2p < 3.1.0 the key is unknown, so the volume is mounted unconditionally, masking
the mount path in run mode and leaving the pod in CrashLoopBackOff.
requires_m2p moves that check to the only place that can perform it: Muppy, before it
deploys. Muppy compares the declared floor with the Release's m2p package and refuses the
install/upgrade when it is not met. It also surfaces a warning on the Release form as soon
as the Parts Config is synced.
Rules:
- Absent means no constraint. Parts Configs written before this key keep working.
- Only checked for m2p packages; other Helm packages use unrelated version schemes.
- An empty or non-semver package version warns and passes - the check never blocks on data it cannot interpret.
Set it whenever you use a key newer than the oldest chart your Releases might run. The
keys that need a floor today: debug_mode / dev_mode / seed_from on a volume and
co_locate on a volume definition (3.1.0), and env_change_trigger on a part (3.2.0).
stop_before_upgrade needs none: the chart never reads it.
Volume Definitions
Define shared volumes used by parts.
An entry may carry co_locate: true: every part mounting that volume is then pinned to the
same node, through a podAffinity on kubernetes.io/hostname matching the Release. That is
what makes a shared ReadWriteOnce claim correct, and it is the only way to run a shared volume
on a storage class that cannot do RWX.
The constraint is hard (requiredDuringSchedulingIgnoredDuringExecution): if no single node
can fit every part, they stay Pending rather than spreading and failing on the volume. It also
concentrates the whole Release on one node, so it removes failure tolerance — acceptable for a
development Release, not for production.
volumesDefinition:
- name: app-data
description: "Application data storage"
claim:
storageClassName: "fast-ssd" # Optional, uses default if not set
accessModes:
- ReadWriteOnce # RWO | ROX | RWX | ReadWriteOncePod
size: "10Gi"
- name: shared-cache
description: "Shared cache volume"
claim:
accessModes:
- ReadWriteMany # Multi-node access
size: "5Gi"
Vault Objects
Declare ConfigMaps and Secrets managed by Muppy Vault:
vault_objects:
- name: app-config
scope: qualifier # qualifier | instance
type: configmap # configmap | secret
- name: app-secrets
scope: qualifier
type: secret
- name: instance-config
scope: instance # Unique per deployment instance
type: configmap
Naming Convention:
- Qualifier scope: {app-code}-{qualifier}-{type}-{name}
- Instance scope: {app-code}-{qualifier}-{instance}-{type}-{name}
Complete Working Examples
Full Web Application with Worker
mpy_meta_config:
# Vault object declarations
vault_objects:
- name: app-config
type: configmap
- name: app-secrets
type: secret
# Volume definitions
volumesDefinition:
- name: app-uploads
description: "User uploaded files"
claim:
size: "20Gi"
accessModes: [ReadWriteOnce]
# Application parts
parts:
web:
type: server
deployment:
docker_image: "mycompany/webapp:v2.1.0"
args: ["--mode=web"]
env_vars:
- name: PORT
value: "8080"
- name: WORKER_QUEUE
value: "redis://redis:6379/0"
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
replica_count: 3
ports:
- name: http
port: 8080
routes:
- name: main
pathPrefix: PathPrefix(`/`)
port_name: http
middlewares:
ipwhitelist: true
basicauth: true
volumes:
- name: app-uploads
mountPath: "/app/uploads"
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
debug_mode_available: codr
debug_config:
debug_fqdn_prefix: "dev-"
args: [
"/usr/bin/code-server",
"--bind-addr=0.0.0.0:8765",
"--auth=password"
]
resources:
requests:
cpu: "500m"
memory: "1Gi"
dashboard_name: "Web Frontend"
worker:
type: worker
deployment:
docker_image: "mycompany/webapp:v2.1.0"
args: ["--mode=worker", "--concurrency=4"]
env_vars:
- name: WORKER_QUEUE
value: "redis://redis:6379/0"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
replica_count: 2
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
debug_mode_available: basic
debug_config:
args: ["sleep", "infinity"]
dashboard_name: "Background Workers"
cleanup:
type: cronjob
cronjob:
schedule: "0 2 * * *" # Daily at 2 AM
suspend: false
docker_image: "mycompany/webapp:v2.1.0"
args: ["--mode=cleanup", "--days=30"]
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
envFrom:
- secretRef:
name: app-secrets
dashboard_name: "Daily Cleanup"
Quick Reference
Part Type Decision Matrix
Need HTTP endpoints? → server
Background processing only? → worker
Scheduled execution? → cronjob
Route existing service? → http
Required Sections by Type
server: deployment + ports (if routing) + routes (if external access)
worker: deployment only
cronjob: cronjob only
http: routes only
Common Gotchas
- Part codes must be ≤ 8 characters
- Deployment section needs at least one property
mpy_meta_configcannot use Helm templating- Volumes persist after chart deletion via
helm.sh/resource-policy: keep- but that annotation is emitted only when thevolumesDefinitionentry has adescription. Omit the description and the PVC, with everything in it, is deleted as soon as the volume leaves the manifest. Always set adescription. - Debug mode requires dashboard configuration
- CronJobs only support
basicdebug mode
Resource Units
CPU: "100m" (millicores), "0.1" (cores), "2" (cores)
Memory: "128Mi", "1Gi", "512M", "2G"
Storage: "10Gi", "100Mi", "1Ti"
Access Modes
ReadWriteOnce (RWO): Single node read-write
ReadOnlyMany (ROX): Multi-node read-only
ReadWriteMany (RWX): Multi-node read-write
ReadWriteOncePod: Single pod read-write (K8s 1.22+)
Troubleshooting
Traefik logs an IPAllowList middleware "not found" that actually works
Traefik may log middleware not found for an IPAllowList that is in fact loaded and enforcing.
This is a known upstream behaviour, not an m2p misconfiguration:
https://community.traefik.io/t/traefik-reporting-middleware-not-found-but-it-works/21424
Check the Traefik Dashboard to confirm the middleware is loaded before chasing it in your Parts Config.
host_prefix Issues
Both host_prefix and hostPrefix Defined
If both naming conventions are present in your configuration, host_prefix (snake_case) takes priority:
parts:
mypart:
host_prefix: "new-" # ✅ This will be used
hostPrefix: "old-" # ❌ This will be ignored
Resolution: Use only host_prefix (snake_case) - it's the current standard for m2p.
Prefix Not Applied to Routes
If your routes aren't using the expected subdomain:
Check:
1. ✅ You have defined host_prefix at the part level (not route level)
2. ✅ The prefix ends with a hyphen (e.g., "api-" not "api")
3. ✅ You clicked "Sync 'Parts Config.'" in the Muppy GUI after changing the configuration
4. ✅ The part has routes defined
5. ✅ expose_publicly: true is set if you need public DNS records
Example:
parts:
api:
host_prefix: "api-" # ✅ Correct: at part level, with trailing hyphen
routes:
- name: main
pathPrefix: PathPrefix(`/`)
port_name: http
Invalid Characters in Prefix
DNS names have character restrictions. Ensure your prefix: - Contains only lowercase letters, numbers, and hyphens - Starts with a letter or number - Ends with a hyphen (connects to the base FQDN) - Total FQDN length (prefix + base) doesn't exceed 253 characters
# ✅ Valid
host_prefix: "api-"
host_prefix: "admin-panel-"
host_prefix: "v2-"
host_prefix: "app1-"
# ❌ Invalid
host_prefix: "API-" # No uppercase letters
host_prefix: "_private-" # No underscores
host_prefix: "admin" # Missing trailing hyphen
host_prefix: "-api-" # Cannot start with hyphen
This reference covers all configuration options available in mpy-metapackage. For advanced features like user manifests and complex integrations, see Advanced Features.