Skip to main content

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

  1. Navigate to your project's environment.
  2. Click Create ServiceBucket (under the Storage category).
  3. Configure:
    • Name — a label for the bucket (e.g. media).
    • Quota (GiB) — the maximum the bucket may hold. 0 means unlimited.
  4. 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.
  5. 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)
WhereBucket page → Applications tab → Attach applicationStack canvas → draw a wire from the bucket to the app
Env var namesFixed: S3_ENDPOINT, S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGIONYou choose, one per field
Buckets per appOneAs many as you like (you name the variables)
Unprovisioned bucketRefused — provision firstAccepted; Deploy stack provisions it before the app
Shows up asAttached services on the environment page and a dashed node on any stack containing the appA 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 fieldWhat it isTypical env var
endpointS3 API endpoint (private to your cluster)S3_ENDPOINT / AWS_ENDPOINT_URL
bucketThe provisioned bucket nameS3_BUCKET
accessKeyAccess key IDAWS_ACCESS_KEY_ID
secretKeySecret access keyAWS_SECRET_ACCESS_KEY
regionRegion (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.

tip

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.

Choose the quota when you create the bucket

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

  1. Connect, don't paste — wire the bucket to your app so credentials inject automatically and rotate cleanly.
  2. One bucket per concern — separate, say, public media from private exports; each gets its own scoped key and quota.
  3. Set a quota — cap growth so one runaway feature can't fill the bucket unexpectedly.
  4. Keep keys server-side — never expose the injected AWS_* vars to browser code; serve or proxy files through your app.
  5. Store the path, not the file — keep object keys/paths in your database and stream from the bucket on demand.