Volumes and storage

Prerequisites: Container configuration

A container's filesystem is ephemeral: when the container restarts, everything written to it is lost. Persistent volumes solve this by mounting external storage into the container at a specified path. Data written to that path survives container restarts, upgrades, and node failures.


When do you need a volume?

You need a volume if your application stores any of the following:

  • Database files (PostgreSQL, MySQL, MariaDB data directories)
  • User uploads (WordPress media, Moodle course files)
  • Application-generated files (PDFs, exports, caches)
  • Configuration that is written at runtime
  • Log files you want to retain

You do not need a volume for:

  • Static application code (baked into the image)
  • Temporary files that can be regenerated
  • In-memory state (Redis data that can be lost)
  • Build artifacts

How volumes work

When you declare a volume in the marketplace manifest, the platform creates a PersistentVolumeClaim (PVC) in Kubernetes. Kubernetes then provisions the actual storage from the underlying cloud provider (GCP Persistent Disk, AWS EBS, Azure Disk, etc.) and mounts it into the container at the specified path.

The storage survives container restarts, node replacements, and updates. It is not shared between deployments, and it can be expanded (but not shrunk) after deployment.


Declaring volumes

A volume is declared in three places in your manifest:

  1. A volume reference under the service's volumes: key
  2. A top-level volumes: entry (Docker Compose convention)
  3. An x-clouve-volumes entry with the size and description
services:
  postgres-db:
    image: postgres:16
    volumes:
      - postgres-data:/var/lib/postgresql/data # Mount declaration

    x-clouve-volumes:
      - name: postgres-data # Must match volume key
        size: 20Gi
        description: PostgreSQL database files

volumes:
  postgres-data: # Top-level declaration

Volume fields

FieldRequiredRulesExamples
nameYesLowercase, alphanumeric + hyphens, start/end alphanumericpostgres-data, wp-uploads
sizeYes\d+(Gi|Mi), minimum 8Gi8Gi, 10Gi, 100Gi
descriptionNoHuman-readable label"PostgreSQL database files"

Size format

  • Use Gi for gibibytes: 8Gi, 10Gi, 50Gi, 100Gi
  • Mi is parsed, but values below 8 Gi (8192 Mi) are rejected
  • The minimum size is 8Gi (8 gibibytes), enforced by the Tropo validator
  • A bare integer such as 10 is read as gibibytes (10Gi); values below 8 are rejected
  • No maximum is enforced in configuration, but cloud billing applies

Standard mount paths

Follow these conventions for mount paths; they match what popular Docker images expect:

Database/ServiceMount Path
PostgreSQL/var/lib/postgresql/data
MySQL / MariaDB/var/lib/mysql
MongoDB/data/db
Redis (persistence)/data
Elasticsearch/usr/share/elasticsearch/data
ApplicationMount Path
WordPress files/var/www/html/wp-content
Moodle data/var/moodledata
Odoo filestore/var/lib/odoo
Generic app data/data or /app/data
Application logs/var/log/app or /logs

Sizing guide

Undersizing volumes is a common mistake. Once a volume fills up, the application typically crashes and needs manual intervention to expand storage, so plan for growth.

Databases

Database SizeSuggested Volume
Development / small org8-20 Gi
Small production (<10k records)20-50 Gi
Medium production50-100 Gi
Large production100-500 Gi

Add a 50% buffer to your estimated current size to allow for growth.

Application data (file uploads)

Usage PatternSuggested Volume
Internal tools, few users8 Gi (minimum)
Small org with media uploads10-50 Gi
Medium org with media uploads50-100 Gi

Logs

Retention PeriodVolume Size
Short-term (days)8 Gi
Medium-term (weeks)10-20 Gi
Long-term50+ Gi

Complete examples

PostgreSQL with appropriate sizing

services:
  postgres-db:
    image: postgres:16.4
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: changeme
    volumes:
      - postgres-data:/var/lib/postgresql/data

    x-clouve-metadata:
      containerName: postgres-db
      purpose: Database
      protocol: TCP
      isPublic: false
      memoryBase: 2 # 2 GB
      cpuBase: 1

    x-clouve-healthcheck:
      enabled: true
      type: TCP
      path: ""
      port: 5432
      initialDelay: 30
      interval: 10
      timeout: 5
      failureThreshold: 3
      successThreshold: 1

    x-clouve-environment-types:
      POSTGRES_DB: static
      POSTGRES_USER: static
      POSTGRES_PASSWORD: secret

    x-clouve-volumes:
      - name: postgres-data
        size: 50Gi
        description: PostgreSQL database and WAL files

volumes:
  postgres-data:

WordPress with uploads and database

services:
  wordpress:
    image: wordpress:6.7.1
    volumes:
      - wp-uploads:/var/www/html/wp-content/uploads

    x-clouve-volumes:
      - name: wp-uploads
        size: 20Gi
        description: WordPress media uploads

  wordpress-mariadb:
    image: mariadb:10.11
    volumes:
      - db-data:/var/lib/mysql

    x-clouve-volumes:
      - name: db-data
        size: 20Gi
        description: MariaDB database files

volumes:
  wp-uploads:
  db-data:

Application with multiple volumes

services:
  my-app:
    image: myapp:2.0.0
    volumes:
      - app-data:/app/data
      - app-logs:/var/log/myapp

    x-clouve-volumes:
      - name: app-data
        size: 10Gi
        description: Application-generated data files
      - name: app-logs
        size: 8Gi
        description: Application logs

volumes:
  app-data:
  app-logs:

Volume naming rules

Volume names follow the same rules as container names:

  • Lowercase letters, numbers, hyphens
  • Start and end with an alphanumeric character
  • Maximum 63 characters
  • Must be unique within the service
✅ postgres-data
✅ wp-uploads
✅ app-data-v2

❌ PostgresData    (uppercase)
❌ postgres_data   (underscore)
❌ -postgres-data  (starts with hyphen)

Common mistakes

MistakeSymptomFix
Volume name in x-clouve-volumes doesn't match service volumeData not persisted, or errorEnsure exact match: postgres-data in both places
Volume declared in service but missing from top-level volumes:Docker Compose parse errorAdd postgres-data: (empty) to top-level volumes:
Volume too small (below 8 Gi)Validator rejects submissionUse minimum 8Gi; consult the sizing guide and add a 50% buffer
Wrong mount pathApplication fails to find dataUse the standard paths table; check image documentation
Missing Gi or Mi unitValidation errorAlways include the unit: 10Gi not 10

Next steps