Object Storage
Kuploy provides managed object storage — S3-compatible buckets for the files your app produces: user uploads, images, video, generated assets, exports. Like a database, a bucket is a resource you create in your project and connect to your app; the platform provisions it and injects the credentials for you, so you never paste keys.
When to use it
Reach for a bucket whenever your app stores files that must outlive a redeploy and be shared across replicas — anything you'd otherwise put on local disk (which is ephemeral and per-pod). Because it speaks the S3 API, any S3 client works: boto3, the AWS SDKs, aws-cli, mc, rclone, and most frameworks' file-storage adapters.
Creating a bucket
- Navigate to your project's environment.
- Click Create Service → Bucket (under the Storage category).
- Configure:
- Name — a label for the bucket (e.g.
media). - Quota (GiB) — the maximum the bucket may hold.
0means unlimited.
- Name — a label for the bucket (e.g.
- Click Create. The bucket now exists as a service but has no storage and no keys yet — it shows as Stopped, and its page explains that it isn't provisioned.
- Click Provision. This creates the bucket on the shared storage and mints its scoped access key.
Provisioning takes a few seconds. When it's done, the bucket's page shows its endpoint, bucket name, region, access key, and secret key (the secret is hidden until you reveal it, and every value has a copy button).
Connecting a bucket to your app
Don't copy the credentials by hand — Kuploy injects them as environment variables. There are two ways to wire a bucket to an app, and which one you want depends on whether the app lives in a stack:
| Attach (no stack needed) | Connect (inside a stack) | |
|---|---|---|
| Where | Bucket page → Applications tab → Attach application | Stack canvas → draw a wire from the bucket to the app |
| Env var names | Fixed: S3_ENDPOINT, S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION | You choose, one per field |
| Buckets per app | One | As many as you like (you name the variables) |
| Unprovisioned bucket | Refused — provision first | Accepted; Deploy stack provisions it before the app |
| Shows up as | Attached services on the environment page and a dashed node on any stack containing the app | A stack component with a wire |
Both keep the app current: re-provisioning the bucket rotates its keys and rewrites every attached and connected app's variables, and detaching/disconnecting removes them. Both take effect on the app's next deploy.
Attaching (no stack)
Open the bucket, go to the Applications tab, click Attach application and pick an app in the same environment. Its Environment tab gains the five variables above inside a managed block (marked do not edit); they take effect on its next deploy. Detach from the same tab.
An attachment is refused, with the reason, when the bucket isn't provisioned yet, when the app already has a different bucket attached (the variable names are fixed, so a second would overwrite the first — use a stack connection if one app needs two buckets), or when the bucket already reaches that app through a stack connection (one writer per app; remove the connection first, or keep using it).
Connecting (in a stack)
In a stack, draw a connection from the bucket to your app and map each field to the env var your app expects:
| Bucket field | What it is | Typical env var |
|---|---|---|
endpoint | S3 API endpoint (private to your cluster) | S3_ENDPOINT / AWS_ENDPOINT_URL |
bucket | The provisioned bucket name | S3_BUCKET |
accessKey | Access key ID | AWS_ACCESS_KEY_ID |
secretKey | Secret access key | AWS_SECRET_ACCESS_KEY |
region | Region (arbitrary but required by most SDKs) | AWS_REGION |
Inside a stack a bucket is a component, not an attached service — it appears alongside your app and database, and that membership is what lets Deploy stack provision it before the app that reads its credentials. A bucket can be both: a member of one stack and attached to applications outside it. The only thing it cannot do is reach the same app both ways.
The resolved values land in your app's Environment tab inside a managed block (marked do not edit) and update automatically — if you rotate or re-provision the bucket, connected apps pick up the new values on their next deploy.
The connection is one-directional and server-side: credentials are resolved into the consumer's environment at deploy time and never exposed to the browser. Keep them server-side in your app too (don't ship them to client-side code).
Example (Python / boto3)
With the env vars above injected, no credentials appear in your code:
import boto3, os
s3 = boto3.client(
"s3",
endpoint_url=os.environ["S3_ENDPOINT"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
region_name=os.environ["AWS_REGION"],
)
s3.upload_file("thumbnail.png", os.environ["S3_BUCKET"], "courses/1/thumbnail.png")
Quotas
Each bucket has its own capacity limit. When a bucket reaches its quota, further uploads to that bucket fail with a quota error — other buckets and services are unaffected. Set 0 for unlimited.
The limit is applied to the storage backend at provisioning time, and the console has no control for changing it afterwards — the figure shown on the bucket page and in Organization storage is the one it was provisioned with. To change a bucket's quota you Re-provision it, which re-applies the limit but also rotates its keys (below).
Re-provisioning
Re-provision re-runs the provisioning against the storage backend. It is safe for your data — the bucket and everything in it are kept — but it mints a new access key and secret, and the quota is re-applied from the current setting.
Connected apps are updated for you: the new credentials are re-resolved into their environment, and they use them from their next deploy. Credentials you copied by hand are not updated and will start failing with an authentication error — one more reason to connect rather than paste.
Monitoring usage
Once a bucket is provisioned, its detail page shows a Storage usage panel with live figures read straight from the storage backend:
- Objects — how many objects the bucket holds.
- Total size — the bytes stored.
- Quota fill — a bar showing how full the bucket is against its quota (hidden when the quota is
0/ unlimited). - A Reachable / Unreachable badge for the shared storage backend — if it can't be reached, the badge carries the reason instead of silently showing zeros.
For the whole picture across every bucket in your organization, open Settings → Organization storage (organization admins). It aggregates total objects, total size, and summed quota, with a per-bucket breakdown and each bucket's reachability — a quick way to see where your storage is going without opening each bucket in turn.
Access & isolation
Every bucket gets its own dedicated access key, scoped to that bucket alone — a bucket's credentials cannot read or write any other bucket. The endpoint is reachable only from within your cluster (your other services), not the public internet, so uploaded objects are served through your app rather than exposed directly.
Deleting a bucket
Delete a bucket from its detail page (you'll be asked to type its name to confirm). This permanently removes the bucket, everything stored in it, and its access key. There is no undo — download anything you need first.
Best practices
- Connect, don't paste — wire the bucket to your app so credentials inject automatically and rotate cleanly.
- One bucket per concern — separate, say, public media from private exports; each gets its own scoped key and quota.
- Set a quota — cap growth so one runaway feature can't fill the bucket unexpectedly.
- Keep keys server-side — never expose the injected
AWS_*vars to browser code; serve or proxy files through your app. - Store the path, not the file — keep object keys/paths in your database and stream from the bucket on demand.