Bundles
Prerequisites: Container configuration, Containerization guide
A bundle is a marketplace listing that contains multiple independent, publicly accessible applications packaged as one. Instead of a single app with a single URL, a bundle deploys several apps, each on its own subdomain, and lets them share infrastructure where appropriate.
Bundle vs single app
| Feature | Single app | Bundle |
|---|---|---|
| Public containers | 1 | 2+ |
| Subdomains | 1 | One per public app |
| Marketplace entry | One listing | One listing |
| Deployment | All containers together | All containers together |
| Use case | One service | Multiple related services |
Example: The "Education Kit" bundle deploys both Moodle (LMS) and Gibbon (school management) together. Users get https://myschool.clouve.dev for Moodle and https://gibbon.myschool.clouve.dev for Gibbon, all deployed from a single marketplace click.
When to create a bundle
Create a bundle when you are packaging two or more applications that are typically deployed together and complement each other, such as a CRM with a marketing tool or an LMS with school management software. Bundling makes sense when the apps can share infrastructure (databases, caches) and users would benefit from deploying the full suite in one click.
How bundles work
Subdomain assignment
Each public container in a bundle gets its own subdomain:
Container order in the manifest:
1st public container → default subdomain
2nd public container → named subdomain (container name)
3rd public container → named subdomain (container name)
Result (workspace: "myschool"):
moodle (1st) → https://myschool.clouve.dev
gibbon (2nd) → https://gibbon.myschool.clouve.dev
The first container in the services section becomes the "primary" app on the default subdomain. Reorder containers to control which app is primary.
Naming convention
Supporting containers (databases, caches) must be prefixed with the name of the main app they belong to:
✅ moodle (public, main app)
✅ moodle-mysql (private, database for moodle)
✅ gibbon (public, main app)
✅ gibbon-mysql (private, database for gibbon)
❌ mysql (no prefix: which app does this belong to?)
❌ database (too generic)
This convention is required: Clouve's cluster management layer uses it to associate each service with its main app during deployment.
If a shared service is truly shared between multiple apps, prefix it with the name of one of the main apps (conventionally the first alphabetically):
# Shared Redis: prefix with one main app name
gibbon-redis:
# Used by both gibbon and moodle
Bundle metadata
Each public container in a bundle needs its own metadata: title, version, description, icon, and an optional admin URL path. The marketplace UI displays this metadata, and the platform uses it to generate the attributes.json.hbs file that drives subdomain routing.
When you download a configured bundle as a Docker Compose file, these per-app fields are preserved in an x-clouve-bundle-metadata extension on each public service so that the upload step can restore them exactly:
services:
moodle:
image: moodle:4.3.0
# ... other config ...
x-clouve-bundle-metadata:
appVersion: "4.3.0"
appTitle: "Moodle LMS"
appDescription: "Open-source learning management system"
appIcon: "https://storage.googleapis.com/clouve-attachments/moodle-icon.png"
adminPath: "/login/index.php" # Optional
The fields:
| Field | Required | Purpose |
|---|---|---|
appVersion | Yes | Semantic version shown in the marketplace listing for this sub-app |
appTitle | Yes | Display name (e.g., "Moodle LMS") |
appDescription | Yes | Marketing description shown to deployers |
appIcon | Yes | URL of the icon image (typically GCS-hosted from the platform upload) |
adminPath | No | URL path that opens the app's admin UI (e.g., /wp-admin, /login) |
The extension is emitted only on public containers, and only when at least one field has a value, so private services (databases, caches) carry no irrelevant metadata.
Creating a bundle step by step
Step 1: Plan your bundle
Decide:
- Which apps will be public (one subdomain each)
- Which services will be shared vs dedicated
- Naming for all containers (following the prefix convention)
- Which app appears on the default subdomain (first in container order)
Example plan: Education Kit
| Container | Type | Public | Subdomain |
|---|---|---|---|
moodle | LMS | Yes | myschool.clouve.dev (default) |
moodle-mysql | Database | No | None |
gibbon | School management | Yes | gibbon.myschool.clouve.dev |
gibbon-mysql | Database | No | None |
Step 2: Write the manifest
A bundle manifest is a standard clv-docker-compose.yml with multiple isPublic: true containers:
version: "3.8"
services:
# First public app → default subdomain
moodle:
image: moodle:4.3.0
ports:
- "80:80"
environment:
MOODLE_DATABASE_HOST: moodle-mysql
MOODLE_DATABASE_NAME: moodle
MOODLE_DATABASE_USER: moodle
MOODLE_DATABASE_PASSWORD: changeme
MOODLE_SITE_URL: ""
MOODLE_ADMIN_USER: admin
MOODLE_ADMIN_PASSWORD: changeme
x-clouve-metadata:
containerName: moodle
purpose: Frontend
protocol: TCP
isPublic: true # ← Public app
memoryBase: 2 # 2 GB
cpuBase: 2
x-clouve-healthcheck:
enabled: true
type: HTTP
path: /
port: 80
initialDelay: 120
interval: 15
timeout: 10
failureThreshold: 5
successThreshold: 1
x-clouve-environment-types:
MOODLE_DATABASE_HOST: containerReference
MOODLE_DATABASE_NAME: static
MOODLE_DATABASE_USER: static
MOODLE_DATABASE_PASSWORD: secret
MOODLE_SITE_URL: applicationUrl
MOODLE_ADMIN_USER: applicationUsername
MOODLE_ADMIN_PASSWORD: applicationPassword
moodle-mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: moodle
MYSQL_USER: moodle
MYSQL_PASSWORD: changeme
MYSQL_ROOT_PASSWORD: changeme
volumes:
- moodle-db:/var/lib/mysql
x-clouve-metadata:
containerName: moodle-mysql
purpose: Database
protocol: TCP
isPublic: false # ← Private service, prefixed with "moodle"
memoryBase: 1 # 1 GB
cpuBase: 0.5
x-clouve-healthcheck:
enabled: true
type: TCP
path: ""
port: 3306
initialDelay: 30
interval: 10
timeout: 5
failureThreshold: 3
successThreshold: 1
x-clouve-environment-types:
MYSQL_DATABASE: static
MYSQL_USER: static
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: secret
x-clouve-volumes:
- name: moodle-db
size: 20Gi
description: Moodle MySQL database
# Second public app → named subdomain (gibbon.workspace.clouve.dev)
gibbon:
image: gibbon:26.0.0
ports:
- "80:80"
environment:
GIBBON_DB_HOST: gibbon-mysql
GIBBON_DB_NAME: gibbon
GIBBON_DB_USER: gibbon
GIBBON_DB_PASSWORD: changeme
GIBBON_SITE_URL: ""
GIBBON_ADMIN_USER: admin
GIBBON_ADMIN_PASSWORD: changeme
x-clouve-metadata:
containerName: gibbon
purpose: Frontend
protocol: TCP
isPublic: true # ← Public app
memoryBase: 1 # 1 GB
cpuBase: 1
x-clouve-healthcheck:
enabled: true
type: HTTP
path: /
port: 80
initialDelay: 60
interval: 10
timeout: 5
failureThreshold: 3
successThreshold: 1
x-clouve-environment-types:
GIBBON_DB_HOST: containerReference
GIBBON_DB_NAME: static
GIBBON_DB_USER: static
GIBBON_DB_PASSWORD: secret
GIBBON_SITE_URL: applicationUrl
GIBBON_ADMIN_USER: applicationUsername
GIBBON_ADMIN_PASSWORD: applicationPassword
gibbon-mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: gibbon
MYSQL_USER: gibbon
MYSQL_PASSWORD: changeme
MYSQL_ROOT_PASSWORD: changeme
volumes:
- gibbon-db:/var/lib/mysql
x-clouve-metadata:
containerName: gibbon-mysql
purpose: Database
protocol: TCP
isPublic: false # ← Private, prefixed with "gibbon"
memoryBase: 1 # 1 GB
cpuBase: 0.5
x-clouve-healthcheck:
enabled: true
type: TCP
path: ""
port: 3306
initialDelay: 30
interval: 10
timeout: 5
failureThreshold: 3
successThreshold: 1
x-clouve-environment-types:
MYSQL_DATABASE: static
MYSQL_USER: static
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: secret
x-clouve-volumes:
- name: gibbon-db
size: 10Gi
description: Gibbon MySQL database
volumes:
moodle-db:
gibbon-db:
Step 3: Provide bundle metadata in the UI
In the submission wizard, for each public container, provide:
| Field | Rules | Example |
|---|---|---|
| App Version | Semantic: X.Y.Z or X.Y.Z-suffix | 4.3.0, 26.0.0 |
| App Title | 2 to 100 characters | Moodle LMS, Gibbon School Platform |
| App Description | 10 to 500 characters | Open-source learning management system |
| App Icon | Image file (PNG recommended) | moodle-icon.png |
| Admin Path (optional) | URL path that opens the admin UI for this sub-app. Validated by the form. | /wp-admin, /login/index.php |
The UI warns you when metadata is missing. Missing fields fall back to defaults (the container name in title case for the title, an auto-generated description), but providing real values is strongly recommended.
Step 4: Review and submit
The UI shows a warning if private containers don't follow the naming convention. Address warnings before submitting.
Subdomain structure in detail
Given a bundle with containers in this order:
moodle(public, first → default)gibbon(public, second → named)
And a business user deploys to workspace myschool:
moodle → https://myschool.clouve.dev (default)
gibbon → https://gibbon.myschool.clouve.dev (named by container name)
The generated attributes.json.hbs structure:
{
"suiteApps": {
"default": "moodle",
"gibbon": "gibbon"
},
"suiteTitles": {
"default": "Moodle LMS",
"gibbon": "Gibbon School Platform"
},
"suiteAdminPaths": {
"default": "/login/index.php",
"gibbon": "/index.php?q=login"
}
}
The suiteApps keys (default, gibbon, ...) double as DNS subdomain labels. Strato's clv-agent apply_dns step keys the per-app Ingress hostname off these labels, so the subdomain you see in the browser comes from the suiteApps key, not directly from the container name. Renaming a public container without updating its expected subdomain key (via the bundle metadata flow) can desynchronize the hostname from the container identity. If you need a container to be addressable at a subdomain that differs from its container name, raise the requirement with the platform team rather than special-casing it on the manifest.
Converting a single app to a bundle
To convert an existing single-app submission to a bundle:
- Add additional public containers (
isPublic: true) - Ensure all supporting containers follow the naming convention
- Add bundle metadata for each new public container
- Reorder containers to set which app is the primary (default subdomain)
The platform automatically detects a bundle when more than one container has isPublic: true.
Common bundle mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Private container has no prefix | Naming validation warning | Add main app name prefix: mysql → moodle-mysql |
| Two public containers on the same port | Port conflict | Each public container needs its own port (or they serve on the same host with Nginx routing) |
| Wrong container order | Wrong app on default subdomain | Reorder tabs in the UI; the first tab is the default |
| Missing bundle metadata | Fallback values used, less polished marketplace listing | Fill in version, title, description, icon for each public container |
Next steps
- Publishing →: submit your bundle (or single app) to the marketplace
- Troubleshooting →: debug bundle deployment issues