Source Container Decides WHAT
Share Creator Controls:
- Which directory to share (source_path)
- Access mode (readonly or readwrite)
- Who can access (1-to-1 or project-wide)
- When it expires (optional)
- Enable/disable anytime
Share directories from one container to others—automatically works across servers. Perfect for multi-service applications, team collaboration, and data exchange without duplicating files.
Complete Storage Shares API:
Creating & Managing Shares:
Receiving & Mounting Shares:
Storage shares use a two-party system:
Source Container Decides WHAT
Share Creator Controls:
Target Container Decides IF
Share Receiver Controls:
Key principle: Source controls WHAT. Target controls IF.
Share specific directory with ONE container:
# Create 1-to-1 container share (readonly)hoody storage create --container $SOURCE_ID \ --source-path "/hoody/storage/shared-assets" \ --target-container-id $TARGET_ID \ --mode readonly \ --description "Static assets for frontend container"const share = await client.api.storageShares.create(SOURCE_CONTAINER_ID, { source_path: '/hoody/storage/shared-assets', target_container_id: TARGET_CONTAINER_ID, mode: 'readonly', description: 'Static assets for frontend container'});console.log(share.data.id); // Share ID for mountingcurl -X POST "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source_path": "/hoody/storage/shared-assets", "target_container_id": "'$TARGET_ID'", "mode": "readonly", "description": "Static assets for frontend container" }' Use when:
Share directory with ALL containers in a project:
# Create project-wide share — all containers in project can accesshoody storage create --container $SOURCE_ID \ --source-path "/hoody/storage/config" \ --target-project-id $PROJECT_ID \ --mode readonly \ --description "Shared configuration for all services"const share = await client.api.storageShares.create(SOURCE_CONTAINER_ID, { source_path: '/hoody/storage/config', target_project_id: PROJECT_ID, mode: 'readonly', description: 'Shared configuration for all services'});// Every container in the project automatically mounts this sharecurl -X POST "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source_path": "/hoody/storage/config", "target_project_id": "'$PROJECT_ID'", "mode": "readonly", "description": "Shared configuration for all services" }' Every container in project automatically mounts this share (by default).
Use when:
{ "mode": "readonly"}Target containers can:
Perfect for:
{ "mode": "readwrite"}Target containers can:
Changes visible to:
Perfect for:
/hoody/databases/)Read-write can be paused without failing the share. A cross-server readwrite share remains
active when read-write publication is paused, but do not assume reads remain available: an
already-live share may remain readable, whereas a share degraded during initial publication is
not mounted for consumers. The share still returns 201. After creation, retrieve it with
GET /api/v1/containers/{id}/storage/shares/{shareId} and check status_message first; for a
newly paused publication it gives the required action: set a disk size limit when the owner
container has no enforced quota; remove a nested subvolume that escapes the quota; wait for the
owner host to regain enforced disk quotas when the host itself cannot enforce them. Read-write
publication resumes automatically after any of those conditions is fixed. During reconciliation of
an already-published share, an owner-host quota-enforce capability miss can instead leave
status_message null or unchanged even though the live export is rendered read-only.
Cross-server read-write behavior is conditional. When CROSS_HOST_NFS_ENABLED=true but
CROSS_HOST_NFS_ALLOW_RW is not exactly true, creating a readwrite share that would span
hosts is rejected with HTTP 422 before the share is created; same-host read-write shares are
unaffected. When cross-server read-write is enabled, quota enforcement can instead pause
publication without failing the share.
# In source containermkdir -p /hoody/storage/team-assets
# Add filescp logo.png /hoody/storage/team-assets/cp styles.css /hoody/storage/team-assets/ Response (201) returns the full share object, including id (the new share’s ID) needed for mounting.
Share now accessible in /hoody/shares/{share_alias}/.
# In target containerls /hoody/shares/shared-assets/# logo.png styles.css
# Files from source container, accessible in targetcat /hoody/shares/shared-assets/styles.cssWhen a target container mounts a share, files appear at:
/hoody/shares/{share-alias}/Example:
# Source creates share with alias "config"POST /storage/shares{ "source_path": "/hoody/storage/app-config", "alias": "config"}
# Target mounts sharePATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Files now accessible at:/hoody/shares/config/├── app.yaml├── database.json└── secrets.envThe alias is reflected in the mount point name. The exact mount path is determined by Hoody’s infrastructure and cannot be set directly by the caller.
Backend shares upload directory with multiple frontends:
# Backend Container (source)POST /containers/{backend_id}/storage/shares{ "source_path": "/hoody/storage/user-uploads", "target_project_id": "{project_id}", "mode": "readwrite", "alias": "uploads"}
# Frontend Container 1 (accepts)PATCH /containers/{frontend_1}/storage/incoming/{share_id}/mount{"mount": true}
# Frontend Container 2 (accepts)PATCH /containers/{frontend_2}/storage/incoming/{share_id}/mount{"mount": true}
# Now all three containers see same /hoody/shares/uploads/# Upload from any frontend → visible to backend and other frontendsConfig container shares settings with all services (readonly):
# Config ContainerPOST /containers/{config_id}/storage/shares{ "source_path": "/hoody/storage/production-config", "target_project_id": "{project_id}", "mode": "readonly", "alias": "config"}
# All service containers mount it# Services read from /hoody/shares/config/# Only config container can update (others readonly)Developers share workspace between containers:
# Developer A's containerPOST /containers/{dev_a}/storage/shares{ "source_path": "/home/user/project", "target_container_id": "{dev_b_container}", "mode": "readwrite", "alias": "shared-project"}
# Developer B mounts in their container# Both edit files in real-time# Changes sync instantlyServices share logs with monitoring container (readonly):
# Service 1POST /storage/shares{ "source_path": "/hoody/storage/service1/logs", "target_container_id": "{monitor_container}", "mode": "readonly"}
# Service 2POST /storage/shares{ "source_path": "/hoody/storage/service2/logs", "target_container_id": "{monitor_container}", "mode": "readonly"}
# Monitor container sees all logs/hoody/shares/service1-logs//hoody/shares/service2-logs/A share reports one of these statuses:
| Status | Description | Next Steps |
|---|---|---|
active | The share specification is retained — not proof that it is enabled, unexpired, mounted, or writable | Check enabled, expires_at, and status_message |
failed | Mount failed (see status_message) | Check errors, fix, retry |
active means “this share specification is retained”, not “everything landed”. A share stays
active while cross-server read-write publication is paused or provisioning is deferred to the
background reconciler; those conditions are explained in status_message. It can also stay active
while a consumer is unmounted, but status_message does not report per-consumer mount state.
Check status:
GET /api/v1/containers/{id}/storage/shares/{shareId}
# Response includes: "status": "active" AND "status_message"# status_message is null when nothing is degraded. Read it, not just status.Source container can disable without deleting:
# Disable share temporarilyPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "enabled": false, "description": "Temporarily disabled for maintenance"}
# Files unmount from target containers# Share configuration preserved
# Re-enable laterPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "enabled": true}Use for: Maintenance windows, testing, gradual rollouts.
Set automatic expiration:
POST /api/v1/containers/{id}/storage/shares{ "source_path": "/hoody/storage/temp-files", "target_container_id": "{target}", "mode": "readonly", "expires_at": 1735689600 # Unix timestamp: 2025-01-01}
# Share auto-unmounts and notifies before expiryPerfect for: Temporary access, demo environments, time-limited shares.
# Shares you created from specific containerhoody storage list --container $SOURCE_ID
# All shares you created (across all containers)hoody storage list-all// Shares from specific containerconst shares = await client.api.storageShares.list(SOURCE_CONTAINER_ID);console.log(shares.data); // Array of shares you created
// All shares across all containersconst allShares = await client.api.storageShares.listGlobalIterator();# Shares you created from specific containercurl "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN"
# All shares you created (across all containers)curl "https://api.hoody.icu/api/v1/storage/shares" \ -H "Authorization: Bearer $TOKEN"# Incoming shares for specific containerhoody storage incoming list --container $TARGET_ID
# All incoming shares (all your containers)hoody storage incoming list-all// Incoming shares for specific containerconst incoming = await client.api.storageShares.listIncoming(TARGET_CONTAINER_ID);console.log(incoming.data); // Shares offered to this container
// All incoming shares across all containersconst allIncoming = await client.api.storageShares.listIncomingGlobalIterator();# Incoming shares for specific containercurl "https://api.hoody.icu/api/v1/containers/$TARGET_ID/storage/incoming" \ -H "Authorization: Bearer $TOKEN"
# All incoming shares (all your containers)curl "https://api.hoody.icu/api/v1/storage/incoming" \ -H "Authorization: Bearer $TOKEN"# Upgrade share from readonly to readwritehoody storage update --container $SOURCE_ID --share-id $SHARE_ID \ --mode readwrite --description "Now allows writes"await client.api.storageShares.update(SOURCE_CONTAINER_ID, SHARE_ID, { mode: 'readwrite', description: 'Now allows writes' });curl -X PATCH "https://api.hoody.icu/api/v1/containers/$SOURCE_ID/storage/shares/$SHARE_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "readwrite", "description": "Now allows writes"}'Target containers must remount to get updated mode.
# Delete share — unmounts from all target containershoody storage delete $SHARE_ID --yesawait client.api.storageShares.delete(SHARE_ID);// Unmounts from all target containers, configuration deleted permanentlycurl -X DELETE "https://api.hoody.icu/api/v1/storage/shares/$SHARE_ID" \ -H "Authorization: Bearer $TOKEN"Storage shares automatically work across different physical servers with full POSIX compliance:
┌──────────────────────────────┐│ Server US-West-1 ││ ┌──────────────────────┐ ││ │ Source Container │ ││ │ /shared/data/ │ ││ └──────────────────────┘ ││ ↓ Hoody handles │└──────────────────────────────┘ ↓ cross-server┌──────────────────────────────┐│ Server EU-Central-1 ││ ┌──────────────────────┐ ││ │ Target Container │ ││ │ /hoody/shares/data/ │ ││ └──────────────────────┘ │└──────────────────────────────┘What you get automatically:
CROSS_HOST_NFS_ENABLED=true, cross-server read-only shares use the normal share workflow; cross-server readwrite also requires CROSS_HOST_NFS_ALLOW_RW=trueShare remains mounted but inaccessible.
/hoody/shares/{alias}/Best practice: Don’t rely on shares from containers that stop frequently.
Yes! Create multiple shares from same source_path:
# Share /hoody/storage/assets with 3 containersPOST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "A"}POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "B"}POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "C"}
# Or share once to entire project (all containers see it)POST /storage/shares {"source_path": "/hoody/storage/assets", "target_project_id": "{project}"}Not recommended for SQLite databases. While you CAN share /hoody/databases/ directories, it bypasses the concurrent-write safety:
# NOT Recommended: Sharing /hoody/databases via storage sharesPOST /storage/shares{ "source_path": "/hoody/databases", "mode": "readwrite"}
# Problem: Network-shared SQLite loses local-filesystem optimizations# Better: Each container uses /hoody/databases/ locally (same-server concurrent writes)# Better: Use hoody-sqlite HTTP API for cross-container database accessWhy not recommended:
/hoody/databases/ concurrent-write safety is optimized for local same-server accessBetter solutions:
/hoody/databases/ directly (concurrent-write-safe)Share remains available but unmounted. Target can accept later:
# Initially rejectPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
# Accept later when neededPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Files appear in /hoody/shares/{alias}/Yes. Share configuration and mount state survive:
Yes:
PATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "alias": "new-alias"}Target containers must unmount and remount to use new alias. Old mount point /hoody/shares/old-alias/ becomes invalid.
Source container: Files count toward source’s storage.
Target containers: Mounted shares do NOT count toward target’s storage quota (they’re references, not copies).
Problem: Share status is "failed" with error message
Common causes:
Source path doesn’t exist:
# In source container, verify path existsls -la /hoody/storage/shared-path
# Create if missingmkdir -p /hoody/storage/shared-path
# Update share to retryPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{"enabled": true}Permission issues:
# Fix permissions in source containerchown -R root:root /hoody/storage/shared-pathchmod -R 755 /hoody/storage/shared-pathSource container stopped:
Not failed, but degraded: a share whose read-write is paused stays active, not failed.
If writes are rejected while status reads active, check status_message first. When
populated, it names the required action: set a disk size limit when the owner container has no
enforced quota; remove a nested subvolume that escapes the quota; wait for the owner host to
regain enforced disk quotas when the host itself cannot enforce them. Read-write publication
resumes automatically after any of those conditions is fixed. For an already-published share,
however, an owner-host quota-enforce capability miss can render the export read-only while
deliberately leaving status_message null or unchanged; restore quota enforcement on the owner
host in that case.
Problem: Share mounted but /hoody/shares/{alias}/ is empty
Solutions:
Verify share is active — and read status_message:
GET /api/v1/containers/{target_id}/storage/incoming# Check: "status": "active", "enabled": true# `status_message` can explain degraded or deferred provisioning.# This response does not report whether this consumer is mounted.Check source container is running:
GET /api/v1/containers/{source_id}# Verify: "status": "running"Verify files exist in source:
# In source containerls /hoody/storage/shared-path# Should show filesUnmount and remount:
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}Problem: Cannot access files in /hoody/shares/{alias}/
Cause: Source container restarted while target was accessing files
Solution:
# Unmount and remountPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Or restart target container (auto-remounts)POST /api/v1/containers/{target_id}/restart# (Consolidated lifecycle route: POST /api/v1/containers/{id}/{operation}# where {operation} is one of: start | stop | force-stop | restart | pause | resume)Problem: PATCH /mount succeeds but files don’t appear
Check:
Share is enabled:
GET /api/v1/containers/{target_id}/storage/incoming# Find the share in the list and verify: "enabled": trueShare not expired:
# Check expires_at (Unix timestamp)# If expired, ask source to extend or remove expirationTarget container has permission:
# Good: Specific subdirectory{"source_path": "/hoody/storage/assets"}
# Risky: Entire Hoody Kit storage{"source_path": "/hoody/storage"}
# Sharing entire /hoody/storage exposes all service dataOne share for all containers:
# Instead of creating 10 identical 1-to-1 sharesPOST /storage/shares {"target_project_id": "{project}"}
# All containers in project can mount# Simpler management, one configurationMultiple writers can conflict:
# Container A writes /hoody/shares/data/file.txt# Container B writes /hoody/shares/data/file.txt (same file)
# Last write wins (potential data loss)Solution: Use application-level locking or coordinate writes (e.g., different directories per container).
Or use /hoody/databases/ for SQLite databases (automatic concurrent-write safety).
Share deletion unmounts from all targets:
# Snapshot source container firstPOST /api/v1/containers/{source_id}/snapshots{"alias": "before-share-deletion"}
# Then delete shareDELETE /api/v1/storage/shares/{share_id}# Clear documentation{ "description": "Read-only access to team logo, CSS, and JS assets for frontend containers. Source: /hoody/storage/static-web-assets"}
# Vague{ "description": "shared files"}Future you will thank present you when managing dozens of shares.
Storage ecosystem:
Related features:
Understanding gained:
/hoody/databases/ for shared SQLite databasesShare directories between containers.
Readonly for safety. Readwrite for collaboration.
One share, multiple consumers. Data exchange without duplication.