# S3 Object Storage (/docs/databases-and-storage/s3-storage)



Rock8Cloud provides S3-compatible object storage that you can deploy alongside your services. Use it for file uploads, static assets, backups, or any S3-compatible workload.

## Create an S3 Service [#create-an-s3-service]

1. Click **Add Service** in your project
2. Select **S3 Storage**
3. Enter a name (e.g., `media-storage`, `uploads`)
4. Select a storage size
5. Click **Deploy S3 Storage**

Rock8Cloud provisions the storage and generates access credentials automatically.

***

## Credentials [#credentials]

After the S3 service is deployed, view your credentials from the service page:

### Credentials Table [#credentials-table]

| Variable        | Description                                                             |
| --------------- | ----------------------------------------------------------------------- |
| `S3_ENDPOINT`   | S3 API endpoint URL (use this for all uploads/downloads)                |
| `S3_BUCKET`     | Name of your storage bucket                                             |
| `S3_REGION`     | S3 region, auto-configured to `us-east-1`                               |
| `S3_ACCESS_KEY` | Authentication key for S3 API access                                    |
| `S3_SECRET_KEY` | Secret key paired with the access key                                   |
| `S3_PUBLIC_URL` | Public URL for reading objects (only resolves when public access is on) |

These variables are injected into your service automatically once the S3 service is linked - reference them by name as shown in the examples below. Credentials are encrypted at rest and only decrypted when you view them.

***

## Connecting Your Application [#connecting-your-application]

The S3 credentials are injected as environment variables when you link the service. Use any S3-compatible client library.

Use `forcePathStyle: true` (Node) / path-style addressing (other SDKs) - this is required for compatibility with the storage backend. The `S3_REGION` environment variable is set automatically, so never hardcode a region.

### Node.js (AWS SDK v3) [#nodejs-aws-sdk-v3]

Create one client and reuse it across your app:

```javascript
// s3.js
import { S3Client } from "@aws-sdk/client-s3";

export const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY,
  },
  forcePathStyle: true,
});

export const BUCKET = process.env.S3_BUCKET;
```

**Upload a buffer or string** - set `ContentType` so browsers render the file correctly:

```javascript
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "node:fs/promises";
import { s3, BUCKET } from "./s3.js";

const body = await readFile("photo.jpg");

await s3.send(new PutObjectCommand({
  Bucket: BUCKET,
  Key: "uploads/photo.jpg",
  Body: body,
  ContentType: "image/jpeg",
}));
```

**Upload large files or streams** - `Upload` from `@aws-sdk/lib-storage` handles multipart automatically, so you never load the whole file into memory:

```javascript
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";
import { s3, BUCKET } from "./s3.js";

const upload = new Upload({
  client: s3,
  params: {
    Bucket: BUCKET,
    Key: "videos/clip.mp4",
    Body: createReadStream("clip.mp4"),
    ContentType: "video/mp4",
  },
});

upload.on("httpUploadProgress", (p) => console.log(`${p.loaded}/${p.total}`));
await upload.done();
```

**Download an object**:

```javascript
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { s3, BUCKET } from "./s3.js";

const res = await s3.send(new GetObjectCommand({
  Bucket: BUCKET,
  Key: "uploads/photo.jpg",
}));
const bytes = await res.Body.transformToByteArray();
```

**List and delete objects**:

```javascript
import { ListObjectsV2Command, DeleteObjectCommand } from "@aws-sdk/client-s3";
import { s3, BUCKET } from "./s3.js";

const { Contents = [] } = await s3.send(new ListObjectsV2Command({
  Bucket: BUCKET,
  Prefix: "uploads/",
}));
for (const obj of Contents) console.log(obj.Key, obj.Size);

await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: "uploads/photo.jpg" }));
```

### Browser direct uploads (presigned URLs) [#browser-direct-uploads-presigned-urls]

Let users upload straight to S3 without proxying bytes through your server. Your backend signs a short-lived URL and the browser `PUT`s the file to it.

```javascript
// backend - issue a presigned PUT URL
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3, BUCKET } from "./s3.js";

const url = await getSignedUrl(
  s3,
  new PutObjectCommand({ Bucket: BUCKET, Key: "uploads/avatar.png", ContentType: "image/png" }),
  { expiresIn: 300 }, // 5 minutes
);
```

```javascript
// browser - upload directly, no credentials needed
await fetch(presignedUrl, {
  method: "PUT",
  headers: { "Content-Type": file.type },
  body: file,
});
```

The `Content-Type` sent by the browser must match the one used when signing.

### Python (boto3) [#python-boto3]

```python
import os
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_ENDPOINT"],
    region_name=os.environ["S3_REGION"],
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
    config=boto3.session.Config(s3={"addressing_style": "path"}),
)

bucket = os.environ["S3_BUCKET"]

# Upload from disk with an explicit content type (handles multipart for large files)
s3.upload_file(
    "photo.jpg", bucket, "uploads/photo.jpg",
    ExtraArgs={"ContentType": "image/jpeg"},
)

# Upload in-memory bytes
s3.put_object(Bucket=bucket, Key="data/report.json", Body=b"{}", ContentType="application/json")

# Download, list, delete
s3.download_file(bucket, "uploads/photo.jpg", "local-photo.jpg")
for obj in s3.list_objects_v2(Bucket=bucket, Prefix="uploads/").get("Contents", []):
    print(obj["Key"], obj["Size"])
s3.delete_object(Bucket=bucket, Key="uploads/photo.jpg")

# Presigned upload URL for browser direct uploads
url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": bucket, "Key": "uploads/avatar.png", "ContentType": "image/png"},
    ExpiresIn=300,
)
```

<Callout type="info">
  For files you want to serve publicly (images, static assets), see [Public Access](#public-access) below - the public URL serves objects by their key at the bucket root.
</Callout>

***

## Features [#features]

* **S3-compatible API** - works with any S3 client library (AWS SDK, boto3, MinIO, etc.)

* **Network-accessible** - accessible from services within your project and via the external URL

* **Persistent** - data survives restarts and redeployments

* **Encrypted credentials** - access keys are encrypted at rest

* **Public access** - optional read-only public access for serving files, images, and static assets

***

## Public Access [#public-access]

You can make your S3 bucket publicly readable to serve files like images, CSS, JavaScript, or any static assets without authentication.

### Enabling Public Access [#enabling-public-access]

**At creation**: Check the **Public Access** checkbox when creating a new S3 service.

**On an existing bucket**: Go to your S3 service's Overview tab and toggle the **Public Access** switch. Toggling public access triggers a redeployment - your credentials remain unchanged.

When public access is enabled, objects in your bucket are served at:

```
https://<bucket-name>.public.your-s3-domain.com/<object-key>
```

The bucket name is the **subdomain**, not a path prefix. The public URL is shown in the service's Overview tab and can be copied to your clipboard.

### Uploading Files with the AWS CLI [#uploading-files-with-the-aws-cli]

Uploads always require credentials, even when the bucket is publicly readable (public access only grants read). The AWS CLI reads its own variable names, so map the injected `S3_*` values to the `AWS_*` ones the CLI expects:

```bash
export AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY"
export AWS_DEFAULT_REGION="$S3_REGION"
```

The public endpoint matches the requested path against object keys **at the bucket root**. With the CLI's default path-style addressing your objects land under a `<bucket-name>/` key prefix and the public URL won't find them - so upload with virtual-hosted addressing:

```bash
# One-time AWS CLI config (recommended)
aws configure set default.s3.addressing_style virtual

# Upload a directory
aws --endpoint-url "$S3_ENDPOINT" s3 cp ./site/ "s3://$S3_BUCKET/" --recursive
```

Verify after upload - `aws --endpoint-url "$S3_ENDPOINT" s3 ls "s3://$S3_BUCKET/"` should show your files at the root, not under a folder matching the bucket name.

For AWS SDK clients, the existing `forcePathStyle: true` setting is fine - uploads through the SDK land at the root. The footgun is specifically the AWS CLI default.

### Serving a Website from the Bucket [#serving-a-website-from-the-bucket]

To make `https://<bucket-name>.public.your-s3-domain.com/` (the root URL) return content, upload an `index.html` at the bucket root with the right content type (assumes the credentials and addressing config above):

```bash
aws --endpoint-url "$S3_ENDPOINT" s3 cp index.html "s3://$S3_BUCKET/" \
  --content-type text/html
```

The AWS CLI guesses content types from file extensions in most cases, but explicit `--content-type` is safer when uploading via scripts.

### Important Notes [#important-notes]

* **Read-only** - public access only allows reading objects. Write operations always require authentication via S3 credentials.
* **All objects are public** - there is no per-object access control. Every object in the bucket becomes readable.
* **No directory listing** - the public endpoint serves individual objects by key. It does not list bucket contents.
* **Credentials unchanged** - toggling public access on or off does not change your S3 access keys.

### Disabling Public Access [#disabling-public-access]

Toggle the **Public Access** switch off from the Overview tab. Objects become inaccessible via the public URL. S3 API access (using credentials) is unaffected.

***

## Deleting an S3 Service [#deleting-an-s3-service]

1. Go to your project
2. Select the S3 service
3. Click **Delete**
4. Confirm the deletion

Deleting an S3 service removes the storage and all stored data permanently.

***

## Troubleshooting [#troubleshooting]

### Access denied [#access-denied]

* Verify `S3_ACCESS_KEY` and `S3_SECRET_KEY` are correctly set
* Ensure the bucket name matches `S3_BUCKET`

### Connection issues [#connection-issues]

* Use the **internal endpoint** for service-to-service access within your project
* Use the **external URL** for access from outside your project
* Ensure `forcePathStyle: true` is set in your client configuration

### Region errors [#region-errors]

* The `S3_REGION` environment variable is set automatically - don't hardcode a region value
* If you see region-related errors, make sure `S3_REGION` is included in your service's environment variables

### Public URL returns `NoSuchKey` / 404 [#public-url-returns-nosuchkey--404]

The bucket was matched but the requested key isn't there. Almost always caused by uploading with path-style addressing - files end up under a `<bucket-name>/` prefix instead of at the root.

Check with `aws s3 ls s3://$S3_BUCKET/` (use virtual-hosted addressing, see [Uploading Files for Public Access](#uploading-files-for-public-access)). If you see your files under a folder matching the bucket name, re-upload after configuring `addressing_style = virtual`.

***

## Related [#related]

* [PostgreSQL](/docs/databases-and-storage/postgresql) - Relational database
* [Dragonfly](/docs/databases-and-storage/dragonfly) - Redis-compatible in-memory datastore
* [Environment Variables](/docs/guides/environment-variables) - Configure secrets and settings
