S3 Object Storage
Deploy S3-compatible object storage for files, media, and assets
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
- Click Add Service in your project
- Select S3 Storage
- Enter a name (e.g.,
media-storage,uploads) - Select a storage size
- Click Deploy S3 Storage
Rock8Cloud provisions the storage and generates access credentials automatically.
Credentials
After the S3 service is deployed, view your credentials from the service page:
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
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)
Create one client and reuse it across your app:
// 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:
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:
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:
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:
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)
Let users upload straight to S3 without proxying bytes through your server. Your backend signs a short-lived URL; the browser PUTs the file to it.
// 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
);// 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)
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,
)For files you want to serve publicly (images, static assets), see Public Access below - the public URL serves objects by their key at the bucket root.
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
You can make your S3 bucket publicly readable to serve files like images, CSS, JavaScript, or any static assets without authentication.
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
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:
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:
# 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/" --recursiveVerify 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
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):
aws --endpoint-url "$S3_ENDPOINT" s3 cp index.html "s3://$S3_BUCKET/" \
--content-type text/htmlThe AWS CLI guesses content types from file extensions in most cases, but explicit --content-type is safer when uploading via scripts.
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
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
- Go to your project
- Select the S3 service
- Click Delete
- Confirm the deletion
Deleting an S3 service removes the storage and all stored data permanently.
Troubleshooting
Access denied
- Verify
S3_ACCESS_KEYandS3_SECRET_KEYare correctly set - Ensure the bucket name matches
S3_BUCKET
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: trueis set in your client configuration
Region errors
- The
S3_REGIONenvironment variable is set automatically — don't hardcode a region value - If you see region-related errors, make sure
S3_REGIONis included in your service's environment variables
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). If you see your files under a folder matching the bucket name, re-upload after configuring addressing_style = virtual.
Related
- PostgreSQL — Relational database
- Dragonfly — Redis-compatible in-memory datastore
- Environment Variables — Configure secrets and settings