Skip to content

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_prefix to 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_prefix cannot be overridden at the route level. All routes within a part share the same FQDN prefix.

Backward Compatibility

Note: The snake_case host_prefix is the current standard. The camelCase hostPrefix is still supported for backward compatibility. - host_prefix (snake_case) - recommended ✅ - hostPrefix (camelCase) - supported for legacy configs - If both are present, host_prefix takes 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:

  1. The api part deploys a single-process container that listens on multiple ports
  2. Port 8000: REST API
  3. Port 9090: /metrics endpoint (typical for apps with Prometheus instrumentation)

  4. The metrics HTTP part creates an additional IngressRoute

  5. Routes to the same pod as api (via service_name)
  6. Uses a different subdomain (via host_prefix)
  7. Applies different security (via middlewares)

Key Points:

  1. HTTP parts should NOT define ports
  2. They only create IngressRoutes, not Services or Deployments
  3. If ports is defined, an empty Service is created with no backend

  4. Use template variables for service names

  5. Pattern: service_name: "@{ obj.key }@-{part-code}-svc"
  6. Works because template variables are allowed in quoted strings
  7. The package key is injected by Muppy at render time

  8. Port name must exist in target service

  9. port_name must match a port name from the target part's ports list
  10. 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_objects entry - the same name the part uses in envFrom - or the literal secrets_env_file for 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 false for 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 false means 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: true volume - the lookup yields an empty dict, so debug_mode reads 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: keep makes that permanent. Kubernetes refuses to patch accessModes, storageClassName or volumeMode on an existing claim (only resources.requests can grow). Because the annotation keeps the PVC alive across helm uninstall and across a Release leaving dev mode, changing any of those in volumesDefinition on 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.requests

The only way out is deleting the claim by hand — which destroys whatever it holds: kubectl -n <namespace> delete pvc <key>-pvc-<name>. Settle accessModes and storageClassName before 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. ReadWriteOnce means 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: a local PV pins pods to its node through the PV's own nodeAffinity, and a networked RWO volume refuses the second attachment. The failure is a Pending pod, never a double writer.

Works on Cost
accessModes: [ReadWriteMany] only storage classes that implement RWX — elsewhere the PVC stays Pending forever none, parts may spread across nodes
accessModes: [ReadWriteOnce] + co_locate: true every 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_locate when 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:

volumes:
  - name: ssl-certs           # Secret vault object  
    mountPath: "/etc/ssl/"
    readOnly: true

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:

disabled_livenessProbe:       # Prefix with 'disabled_' to disable
  httpGet:
    path: /health
    port: 8080


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:

mpy_meta_config:
  requires_m2p: "3.1.0"

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_config cannot use Helm templating
  • Volumes persist after chart deletion via helm.sh/resource-policy: keep - but that annotation is emitted only when the volumesDefinition entry has a description. Omit the description and the PVC, with everything in it, is deleted as soon as the volume leaves the manifest. Always set a description.
  • Debug mode requires dashboard configuration
  • CronJobs only support basic debug 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.