Persistent storage in NEXUS AI
Most application deployment platforms make stateless services easy and stateful workloads awkward. That works until your app needs user uploads, generated reports, SQLite files, model artifacts, or a shared object store for background jobs.
By Platform Super Admin • • Storage · DevOps
# Persistent storage in NEXUS AI: volumes and S3-compatible buckets for deployed apps
**Published:** May 9, 2026
**Category:** Storage · DevOps
**Reading time:** 11 minutes
**Author:** NEXUS AI Team
---
Most application deployment platforms make stateless services easy and stateful workloads awkward. That works until your app needs user uploads, generated reports, SQLite files, model artifacts, or a shared object store for background jobs.
NEXUS AI now includes two storage primitives for deployed applications:
- **Volumes** for persistent filesystem mounts.
- **Buckets** for S3-compatible object storage.
Both are organization-scoped. Both can be attached to deployments. Both survive container restarts, redeploys, and deployment deletion until you explicitly delete the storage resource.
This post explains when to use each one, how to attach them, how they behave during scaling, and the exact CLI commands to run.
---
## Two storage primitives
Volumes and buckets solve different problems.
| Feature | Best for | Access pattern | Attachment model |
|---------|----------|----------------|------------------|
| Volumes | Filesystem state, SQLite, local uploads, persistent caches | App reads and writes a mounted path like `/data` | Single deployment |
| Buckets | User media, reports, generated assets, object storage | App uses S3-compatible SDK calls | Multiple deployments |
Use a **volume** when your app expects a local filesystem path.
Use a **bucket** when your app can store data as objects by key using an S3 SDK.
---
## Volumes: persistent filesystem mounts
A NEXUS AI volume is a persistent filesystem mount backed by a Docker named volume. Your app writes to a path, usually `/data`, and the data remains available after redeploys or container replacement.
Create a volume:
```bash
nexus volume create app-data --display-name "App data"
```
Deploy your app:
```bash
nexus deploy source \
--repo https://github.com/your-org/your-app \
--name myapp \
--provider docker \
--wait
```
Attach the volume:
```bash
nexus volume attach <volume-id> <deployment-id> --mount /data
```
Redeploy so the new container starts with the mount:
```bash
nexus deploy redeploy <deployment-id> --wait
```
That redeploy step matters. Docker mounts are applied when a container is created. You cannot add a volume mount to an already-running container.
Verify the mount:
```bash
docker exec <container-id> ls -la /data
```
---
## When to use volumes
Volumes are a good fit for:
- SQLite databases for small apps.
- User uploads written through the filesystem.
- Persistent cache directories.
- Generated files that your app expects to read from disk.
- Model files or local indexes that survive redeploys.
Volumes are not a good fit for every scaled workload. If multiple app replicas write the same file at the same time, your application needs to handle locking or coordination.
Think of a NEXUS AI volume like a shared network drive. It persists, but it does not magically make unsafe concurrent writes safe.
---
## How volumes behave when you scale
When you scale a deployment up, every replica mounts the same named volume at the same path.
```bash
nexus deploy scale <deployment-id> 3
```
All three replicas now see the same files under `/data`.
That is useful for read-mostly data, shared assets, or workloads with explicit locking. It is risky for apps that assume a single writer, such as a default SQLite setup with multiple write-heavy replicas.
When you scale down:
```bash
nexus deploy scale <deployment-id> 1
```
The removed containers detach. The volume remains. The remaining replica keeps using the same data.
When you delete the deployment, the volume still survives. You must explicitly detach and delete the volume if you want to destroy the data.
```bash
nexus volume detach <volume-id>
nexus volume delete <volume-id> --yes
```
---
## Buckets: S3-compatible object storage
A NEXUS AI bucket is S3-compatible object storage backed by MinIO. Your app interacts with it using standard AWS SDKs, boto3, or any S3-compatible client.
Create a bucket:
```bash
nexus bucket create user-uploads --display-name "User uploads"
```
Attach it to a deployment:
```bash
nexus bucket attach <bucket-id> <deployment-id>
nexus deploy redeploy <deployment-id> --wait
```
After redeploy, NEXUS AI injects S3 environment variables into the app container:
```bash
S3_ENDPOINT=http://host.docker.internal:9000
S3_REGION=us-east-1
S3_BUCKET=org-<orgIdShort>-<bucketName>
S3_ACCESS_KEY=<scoped access key>
S3_SECRET_KEY=<scoped secret key>
```
If multiple buckets are attached, NEXUS AI also injects per-bucket aliases:
```bash
S3_BUCKET_USER_UPLOADS=org-...-user-uploads
S3_BUCKET_USER_UPLOADS_ACCESS_KEY=...
S3_BUCKET_USER_UPLOADS_SECRET_KEY=...
```
---
## Example: write files to a bucket from Python
Your app can use boto3 with the injected environment variables:
```python
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url=os.environ["S3_ENDPOINT"],
aws_access_key_id=os.environ["S3_ACCESS_KEY"],
aws_secret_access_key=os.environ["S3_SECRET_KEY"],
region_name=os.environ["S3_REGION"],
)
s3.put_object(
Bucket=os.environ["S3_BUCKET"],
Key="uploads/hello.txt",
Body=b"hello from NEXUS AI",
)
```
That same pattern works with the AWS SDK for JavaScript, Go, Java, Ruby, PHP, and other S3-compatible clients.
---
## Bucket file operations from the CLI
List files:
```bash
nexus bucket files <bucket-id>
nexus bucket files <bucket-id> --prefix uploads/
```
Upload a file:
```bash
nexus bucket upload <bucket-id> ./report.pdf --key reports/report.pdf
```
Download a file:
```bash
nexus bucket download <bucket-id> reports/report.pdf --out ./report.pdf
```
Generate a short-lived signed download URL:
```bash
nexus bucket download <bucket-id> reports/report.pdf --share --ttl 900
```
Delete a file:
```bash
nexus bucket rm <bucket-id> reports/report.pdf --yes
```
The CLI upload path streams from disk, which is better for large files than browser uploads.
---
## Scoped credentials and rotation
Each bucket gets scoped S3 credentials. A deployment attached to one bucket does not automatically get access to every bucket in the organization.
Reveal credentials for external clients:
```bash
nexus bucket credentials <bucket-id>
```
Rotate credentials:
```bash
nexus bucket rotate-credentials <bucket-id> --yes
nexus deploy redeploy <deployment-id> --wait
```
Redeploy is required because the running container already has the old environment variables. The next container start receives the new `S3_ACCESS_KEY` and `S3_SECRET_KEY`.
---
## Buckets and scaling
Buckets are usually a better fit for scaled apps than shared filesystem volumes.
When you scale up, every replica receives the same S3 environment variables. Each replica makes independent S3 API calls. The object storage layer handles concurrent requests.
```bash
nexus deploy scale <deployment-id> 3
```
For object keys, the normal S3 rule applies: if two replicas write the same key, the last write wins. Use unique keys when each replica should produce independent output.
When you scale down, nothing needs to detach. The removed container stops making S3 calls. The bucket and objects remain.
---
## REST API reference
Volumes:
```text
GET /api/volumes
POST /api/volumes
POST /api/volumes/:id/attach
POST /api/volumes/:id/detach
POST /api/volumes/:id/refresh-usage
DELETE /api/volumes/:id
```
Buckets:
```text
GET /api/buckets
POST /api/buckets
POST /api/buckets/:id/attach
POST /api/buckets/:id/detach
POST /api/buckets/:id/refresh-usage
POST /api/buckets/:id/rotate-credentials
GET /api/buckets/:id/credentials
GET /api/buckets/:id/files
PUT /api/buckets/:id/files/:key
GET /api/buckets/:id/files/:key/download
POST /api/buckets/:id/files/:key/download-url
DELETE /api/buckets/:id/files/:key
GET /api/bucket-downloads/:token
```
Example volume attach request:
```bash
curl -s -X POST "$NEXUS_API_BASE/volumes/<volume-id>/attach" \
-H "Authorization: Bearer $NEXUS_JWT" \
-H "Content-Type: application/json" \
-d '{"deploymentId":"<deployment-id>","mountPath":"/data"}'
```
Example bucket attach request:
```bash
curl -s -X POST "$NEXUS_API_BASE/buckets/<bucket-id>/attach" \
-H "Authorization: Bearer $NEXUS_JWT" \
-H "Content-Type: application/json" \
-d '{"deploymentId":"<deployment-id>"}'
```
---
## MCP tools
AI clients can operate storage through MCP tools with scoped permissions.
Volume tools:
```text
nexusai_volume_list
nexusai_volume_create
nexusai_volume_attach
nexusai_volume_detach
nexusai_volume_delete
```
Bucket tools:
```text
nexusai_bucket_list
nexusai_bucket_create
nexusai_bucket_attach
nexusai_bucket_detach
nexusai_bucket_rotate_credentials
nexusai_bucket_files_list
nexusai_bucket_file_download
nexusai_bucket_file_delete
nexusai_bucket_delete
```
Use MCP for operational flows like “create a bucket for this deployment, attach it, then remind me to redeploy,” while keeping destructive operations behind confirmation.
---
## Which one should you choose?
Choose a **volume** when:
- Your app requires a filesystem path.
- You need persistent local files.
- You are running a single replica or have safe file-locking behavior.
- You want simple persistence for Docker-based deployments.
Choose a **bucket** when:
- Your app stores uploads or generated files.
- Multiple deployments need access to the same storage.
- The app may scale to multiple replicas.
- You want S3-compatible tooling and signed URLs.
For most user-upload and generated-asset workflows, buckets are the better long-term default. For apps that truly expect local disk, volumes are the practical answer.
---
## Final takeaway
NEXUS AI now gives deployed apps durable storage without forcing every app into the same model.
Volumes give you persistent filesystem mounts. Buckets give you S3-compatible object storage with scoped credentials, file operations, signed URLs, and multi-deployment attachment.
The rule is simple: use volumes for local filesystem state, and use buckets for object storage. Then redeploy after attaching so your containers receive the mount or environment variables they need.