Containerization guide

Prerequisites: Application requirements

This document explains how to package your application for the Clouve marketplace. You will create two files: a docker-compose.yml for local development, and a clv-docker-compose.yml, the marketplace manifest, for platform deployment.


Overview

Clouve uses a two-file system:

FilePurposeUsed by
docker-compose.ymlLocal development and testingYou, during development
clv-docker-compose.ymlMarketplace deployment manifestThe Clouve platform

The marketplace manifest is a superset of Docker Compose. It adds x-clouve-* extension fields that tell the platform how to generate Kubernetes resources. Standard Docker Compose tooling ignores these fields, so you can also use the file locally.


Dockerfile best practices

Before writing the manifest, make sure your Docker images follow these practices:

Use specific base image versions

# Good
FROM php:8.3-apache
FROM node:22-alpine
FROM python:3.12-slim

# Bad - unpredictable across rebuilds
FROM php:latest
FROM node:alpine

Keep images small

# Use multi-stage builds for compiled languages
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o app .

FROM alpine:3.20
COPY --from=builder /app/app /usr/local/bin/app
CMD ["app"]

Avoid running as root

RUN useradd -m -u 1001 appuser
USER appuser

Handle signals properly

Use the exec form for CMD/ENTRYPOINT so signals reach your process instead of a shell:

# Good - signals reach the process
ENTRYPOINT ["node", "server.js"]

# Bad - signals go to sh, not node
ENTRYPOINT node server.js

docker-compose.yml: the local development file

This file is for testing your application locally. It does not need any Clouve-specific fields.

Example: WordPress

version: "3.8"

services:
  wordpress:
    image: wordpress:6.7.1
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: wordpress-mariadb
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: devpassword
    volumes:
      - wordpressdata:/var/www/html/wp-content
    depends_on:
      - wordpress-mariadb

  wordpress-mariadb:
    image: mariadb:10.11
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: devpassword
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  wordpressdata:
  dbdata:

Test it:

docker-compose up
# Open http://localhost:8080

clv-docker-compose.yml: the marketplace manifest

The marketplace manifest extends Docker Compose with four x-clouve-* extension blocks that must be present on every service.

Extension blocks reference

x-clouve-metadata: # Required on every service
  containerName: string # Must match the service key
  purpose: string # Frontend | Backend | Database | Cache | Message Queue
  protocol: string # TCP | UDP
  isPublic: boolean # true = publicly accessible via Ingress
  memoryBase: number # Memory in GB (gibibytes). Minimum 1, decimals allowed (e.g., 0.5).
  cpuBase: number # CPU in decimal cores. Minimum 0.5 (e.g., 0.5 = half a core).

x-clouve-healthcheck: # Required on every service (set enabled: false to disable)
  enabled: boolean
  type: string # HTTP | TCP | Command
  path: string # HTTP path for HTTP, "" for TCP, command string for Command
  port: number # 0 = use containerPort
  initialDelay: number # Seconds before first check
  interval: number # Seconds between checks
  timeout: number # Seconds before check times out
  failureThreshold: number # Failures before marking unhealthy
  successThreshold: number # Successes before marking healthy

x-clouve-environment-types: # Required (use {} if no env vars)
  VAR_NAME: type # Maps each env var name to its type

x-clouve-volumes: # Required if service has volumes (omit if none)
  - name: string # Must match the volume key
    size: string # e.g., "10Gi", "100Gi". Minimum "8Gi".
    description: string # Optional

# Public-container-only: supplies bundle metadata for multi-app submissions.
# Optional on single-app submissions. Round-trips through download/upload.
x-clouve-bundle-metadata:
  appVersion: string # Semantic version of this app (e.g., "4.3.0")
  appTitle: string # Display title in the marketplace
  appDescription: string # Marketing description
  appIcon: string # URL of the icon image
  adminPath: string # Optional URL path that opens the app's admin UI

# Top-level (sibling of `services:`, not nested in a service).
# Opts the application into the platform-synthesized AI Assistant sidecar.
# See [Magneto Agent / AI Assistant](./11-magneto-agent.md).
x-clouve-agent:
  enabled: boolean
  skills:
    url: string # Skills repo URL (required when enabled)
    git:
      token: string # Optional; rendered as a `secret`-typed env var
      username: string # Optional
  advanced:
    client: string|null # e.g., "claude-code"; null = pick at runtime
    isPublic: boolean # Whether the agent has its own subdomain
    sidecarHosts: array|null # null = auto-derive from siblings
    sidecarHostsExclude: array # Container names to drop from auto-derived list
    sidecarPullTimeout: number # Seconds; max 1800

Complete example: WordPress marketplace manifest

version: "3.8"

services:
  wordpress:
    image: wordpress:6.7.1
    ports:
      - "80:80"
    environment:
      WORDPRESS_DB_HOST: wordpress-mariadb
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: changeme
      WORDPRESS_SITE_URL: ""
      WORDPRESS_ADMIN_USER: admin
      WORDPRESS_ADMIN_PASSWORD: changeme
      WORDPRESS_ADMIN_EMAIL: admin@example.com
    volumes:
      - wordpressdata:/var/www/html/wp-content

    x-clouve-metadata:
      containerName: wordpress
      purpose: Frontend
      protocol: TCP
      isPublic: true
      memoryBase: 1 # 1 GB
      cpuBase: 1

    x-clouve-healthcheck:
      enabled: true
      type: HTTP
      path: /
      port: 80
      initialDelay: 30
      interval: 10
      timeout: 5
      failureThreshold: 3
      successThreshold: 1

    x-clouve-environment-types:
      WORDPRESS_DB_HOST: containerReference
      WORDPRESS_DB_NAME: static
      WORDPRESS_DB_USER: static
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_SITE_URL: applicationUrl
      WORDPRESS_ADMIN_USER: applicationUsername
      WORDPRESS_ADMIN_PASSWORD: applicationPassword
      WORDPRESS_ADMIN_EMAIL: userConfigurable

    x-clouve-volumes:
      - name: wordpressdata
        size: 10Gi
        description: WordPress uploads and theme files

  wordpress-mariadb:
    image: mariadb:10.11
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootchangeme
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: changeme
    volumes:
      - dbdata:/var/lib/mysql

    x-clouve-metadata:
      containerName: wordpress-mariadb
      purpose: Database
      protocol: TCP
      isPublic: false
      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_ROOT_PASSWORD: secret
      MYSQL_DATABASE: static
      MYSQL_USER: static
      MYSQL_PASSWORD: secret

    x-clouve-volumes:
      - name: dbdata
        size: 10Gi
        description: MariaDB database files

volumes:
  wordpressdata:
  dbdata:

Environment variable types in context

The x-clouve-environment-types block maps each variable name to a type. The type determines how the platform handles the variable at deployment time.

When your variable is...Use type
A fixed config value (log level, debug flag)static
A password, API key, or secretsecret
Something the user should customize (email, URL)userConfigurable
Another container's hostnamecontainerReference
An admin username for the appapplicationUsername
An admin password for the appapplicationPassword
The app's own public URL (auto-generated)applicationUrl
The app's own hostname (auto-generated)applicationHost

Full reference: Environment variables →


Container ordering

The order of containers in the services section determines their deployment sequence. List them in dependency order:

services:
  # 1. Database first (others depend on it)
  wordpress-mariadb: ...

  # 2. Cache second (if any)
  wordpress-redis: ...

  # 3. Application last (depends on database being ready)
  wordpress: ...

Kubernetes does not guarantee strict ordering, but databases and caches typically start faster and will be ready by the time application containers finish initializing. Always implement retry/backoff logic in your application for external dependencies.


Building and pushing images

If your application uses a custom Docker image (not a public official image):

# Build for multiple platforms (required for Clouve)
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag myregistry/my-app:1.0.0 \
  --push \
  ./image/

# Or using the magneto build script
cd magneto
./build.sh my-application --push

The magneto/build.sh script handles multi-platform builds automatically:

# Single platform (local testing)
./build.sh my-application

# Multi-platform + push to registry
./build.sh my-application --push

Validation

Before uploading your manifest, validate it:

# Check YAML syntax
yamllint clv-docker-compose.yml

# Test that docker-compose can parse it (ignores x-clouve-* fields)
docker-compose -f clv-docker-compose.yml config

Common mistakes

MistakeSymptomFix
containerName doesn't match service keyDeployment misconfigurationSet containerName to exactly the service key
Missing x-clouve-metadata on a serviceSubmission validation errorAdd the block to every service
isPublic: true on the databaseSecurity risk + incorrect setupOnly the app frontend/backend should be public
Volume declared in service but missing in volumes: sectionDocker Compose parse errorAdd a matching entry in the top-level volumes:
Using memoryBase: 512 thinking it means Mi512 GB requested and rejectedmemoryBase is in GB: use 0.5, 1, 2
Using cpuBase: 500m instead of cpuBase: 0.5Wrong resource allocationUse decimal cores: 0.5, 1, 2. Minimum 0.5.
Using memoryBase: 0.25 thinking it's allowedValidator rejects (< minimum 1)Memory minimum is 1 (= 1 GB)
Using size: 1Gi for a volumeValidator rejects (< 8 Gi)Volume minimum is 8Gi

Native Docker healthcheck block

When health checks are enabled on a service, the downloader also emits a standard Docker Compose healthcheck: block alongside the x-clouve-healthcheck block. This lets the manifest work correctly when run locally with docker-compose up: the same probe runs against the local container. The mapping is:

Clouve typeDocker test command
HTTP["CMD", "wget", "--spider", "-q", "http://localhost:<port><path>"]
TCP["CMD", "nc", "-z", "localhost", "<port>"]
Command["CMD-SHELL", "<command from path>"]

You do not need to write this block yourself. It is generated from the x-clouve-healthcheck values during download and re-derived on upload.


Next steps