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:
- A volume reference under the service's
volumes:key - A top-level
volumes:entry (Docker Compose convention) - An
x-clouve-volumesentry 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
| Field | Required | Rules | Examples |
|---|---|---|---|
name | Yes | Lowercase, alphanumeric + hyphens, start/end alphanumeric | postgres-data, wp-uploads |
size | Yes | \d+(Gi|Mi), minimum 8Gi | 8Gi, 10Gi, 100Gi |
description | No | Human-readable label | "PostgreSQL database files" |
Size format
- Use
Gifor gibibytes:8Gi,10Gi,50Gi,100Gi Miis 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
10is read as gibibytes (10Gi); values below8are 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/Service | Mount Path |
|---|---|
| PostgreSQL | /var/lib/postgresql/data |
| MySQL / MariaDB | /var/lib/mysql |
| MongoDB | /data/db |
| Redis (persistence) | /data |
| Elasticsearch | /usr/share/elasticsearch/data |
| Application | Mount 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 Size | Suggested Volume |
|---|---|
| Development / small org | 8-20 Gi |
| Small production (<10k records) | 20-50 Gi |
| Medium production | 50-100 Gi |
| Large production | 100-500 Gi |
Add a 50% buffer to your estimated current size to allow for growth.
Application data (file uploads)
| Usage Pattern | Suggested Volume |
|---|---|
| Internal tools, few users | 8 Gi (minimum) |
| Small org with media uploads | 10-50 Gi |
| Medium org with media uploads | 50-100 Gi |
Logs
| Retention Period | Volume Size |
|---|---|
| Short-term (days) | 8 Gi |
| Medium-term (weeks) | 10-20 Gi |
| Long-term | 50+ 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
| Mistake | Symptom | Fix |
|---|---|---|
Volume name in x-clouve-volumes doesn't match service volume | Data not persisted, or error | Ensure exact match: postgres-data in both places |
Volume declared in service but missing from top-level volumes: | Docker Compose parse error | Add postgres-data: (empty) to top-level volumes: |
| Volume too small (below 8 Gi) | Validator rejects submission | Use minimum 8Gi; consult the sizing guide and add a 50% buffer |
| Wrong mount path | Application fails to find data | Use the standard paths table; check image documentation |
Missing Gi or Mi unit | Validation error | Always include the unit: 10Gi not 10 |
Next steps
- Bundles →: multi-app marketplace listings with shared volumes
- Container configuration →: back to the full UI workflow
- Troubleshooting →: volume-related failure scenarios