Object Storage with DigitalOcean Spaces for File-Based Tools (S3-Compatible)
Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.
Pricing note. Figures are as of July 2026: Spaces starts at $5/month for 250 GiB of storage + 1 TiB of outbound transfer (inbound is always free), with modest per-GB overage beyond that. Verify current pricing before relying on it.
Any tool that accepts a file — an image converter, a PDF processor, a background remover, a transcription service — needs somewhere to put the results. DigitalOcean Spaces is S3-compatible object storage with flat, predictable pricing, which makes it an ideal backing store: your existing S3 SDKs work unchanged, and you swap AWS's metered-everything billing for one simple monthly number.
Prerequisites:
- A DigitalOcean account
- A Java or PHP app (the SDK patterns transfer to any language)
1. Why Spaces for File Tools
| Amazon S3 | DigitalOcean Spaces | |
|---|---|---|
| API | S3 | S3-compatible (same SDKs) |
| Pricing model | Metered: storage + requests + egress | Flat: $5 = 250 GiB + 1 TiB transfer |
| Egress | $0.09/GB after 100 GB | Bundled 1 TiB, then low per-GB |
| Request charges | Per-1,000 GET/PUT | None |
| Built-in CDN | CloudFront (separate) | Included |
| Best for | Massive scale, deep AWS integration | Predictable bills, small–mid tools |
For a file-based tool with unpredictable traffic, the killer feature is no per-request and bundled egress — you don't get nickel-and-dimed on every GET, and a viral day doesn't produce a surprise bill.
2. Create a Bucket and Access Keys
In the DigitalOcean console: Spaces Object Storage → Create a Spaces Bucket. New signups get $200 in free credits for 60 days.
- Region: pick the one nearest your users (or your Droplet) — e.g.
nyc3,fra1,sgp1 - Name: globally unique, e.g.
mytool-uploads - File listing: Restrict for a private bucket (recommended — serve via signed URLs)
Then generate a key: API → Spaces Keys → Generate New Key. Save the access key and secret. Your endpoint is:
https://<region>.digitaloceanspaces.com
3. Uploads from Java (AWS SDK v2)
Spaces speaks S3, so the standard AWS SDK works — you just point it at the Spaces endpoint. Add the dependency:
<!-- pom.xml -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.28.0</version>
</dependency>
Configure the client against Spaces:
import software.amazon.awssdk.auth.credentials.*;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
public class SpacesStorage {
private static final String ENDPOINT = "https://nyc3.digitaloceanspaces.com";
private static final String BUCKET = "mytool-uploads";
private final S3Client s3 = S3Client.builder()
.endpointOverride(URI.create(ENDPOINT))
.region(Region.US_EAST_1) // any valid region id; Spaces ignores it
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(
System.getenv("SPACES_KEY"),
System.getenv("SPACES_SECRET"))))
.build();
/** Upload a file and return its object key. */
public String upload(Path file, String key, String contentType) {
s3.putObject(
PutObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.contentType(contentType)
.acl(ObjectCannedACL.PRIVATE)
.build(),
file);
return key;
}
/** Generate a signed URL that expires after the given minutes. */
public String signedUrl(String key, long minutes) {
try (S3Presigner presigner = S3Presigner.builder()
.endpointOverride(URI.create(ENDPOINT))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(
System.getenv("SPACES_KEY"),
System.getenv("SPACES_SECRET"))))
.build()) {
return presigner.presignGetObject(
GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(minutes))
.getObjectRequest(GetObjectRequest.builder()
.bucket(BUCKET).key(key).build())
.build()
).url().toString();
}
}
}
4. Uploads from PHP (Flysystem)
For Laravel or any PHP app, Flysystem with the S3 adapter treats Spaces like S3. Install:
composer require league/flysystem-aws-s3-v3
In Laravel, add a disk in config/filesystems.php:
'spaces' => [
'driver' => 's3',
'key' => env('SPACES_KEY'),
'secret' => env('SPACES_SECRET'),
'region' => 'us-east-1', // any valid id; Spaces ignores it
'bucket' => env('SPACES_BUCKET'),
'endpoint' => env('SPACES_ENDPOINT'), // https://nyc3.digitaloceanspaces.com
'use_path_style_endpoint' => false,
'visibility' => 'private',
],
SPACES_KEY=your_access_key
SPACES_SECRET=your_secret
SPACES_BUCKET=mytool-uploads
SPACES_ENDPOINT=https://nyc3.digitaloceanspaces.com
Then it's just the Storage facade:
use Illuminate\Support\Facades\Storage;
// Store an uploaded file
$key = Storage::disk('spaces')->putFile('results', $request->file('doc'));
// Temporary signed URL (expires in 15 minutes)
$url = Storage::disk('spaces')->temporaryUrl($key, now()->addMinutes(15));
Outside Laravel, the raw Flysystem + Aws\S3\S3Client setup uses the same endpoint and credentials.
5. Signed URLs and Temporary Access
For any private tool, never make objects public — hand out short-lived signed URLs instead. Both the Java signedUrl() and PHP temporaryUrl() above generate links that:
- Grant read access to one object for a fixed window (e.g. 15 minutes)
- Require no login — perfect for "here's your processed file" download links
- Expire on their own, so a leaked link is worthless after the window
Keep the TTL as short as the use case allows. For a single immediate download, 5 minutes is plenty.
6. Put a CDN in Front of Public Assets
If a tool serves genuinely public assets (thumbnails, shared exports), enable the built-in CDN on the bucket: Settings → CDN → Enable. You get a *.cdn.digitaloceanspaces.com hostname (or attach your own subdomain + cert). The CDN:
- Caches objects at edge locations for lower latency
- Serves cached hits without touching your origin transfer allowance
- Costs nothing extra to enable (it's part of Spaces)
Serve public assets via the CDN URL, and keep private/user files behind signed URLs — don't mix the two on one bucket.
7. Privacy-First Patterns: Auto-Expiry and No-Retention
For a privacy-conscious tool (the ToolsHubKit ethos), don't keep user files a second longer than needed. Set a lifecycle rule so objects self-delete. lifecycle.json:
{
"Rules": [
{
"ID": "expire-results",
"Status": "Enabled",
"Filter": { "Prefix": "results/" },
"Expiration": { "Days": 1 }
}
]
}
Apply it with the S3-compatible AWS CLI:
aws s3api put-bucket-lifecycle-configuration \
--bucket mytool-uploads \
--lifecycle-configuration file://lifecycle.json \
--endpoint-url https://nyc3.digitaloceanspaces.com
Now every processed file vanishes after 24 hours — no manual cleanup, no indefinite retention, and your storage footprint stays flat no matter how many files you process.
8. Cost vs S3 for a File-Heavy Tool
Consider a tool storing ~100 GB and serving ~800 GB/month of downloads:
| DigitalOcean Spaces | Amazon S3 (approx) | |
|---|---|---|
| Storage (100 GB) | Within the $5 base (250 GiB) | ~$2.30 |
| Egress (800 GB) | Within the 1 TiB bundle | 700 GB billable × $0.09 ≈ $63 |
| GET requests | Free | Per-1,000 fees add up |
| Ballpark total | ~$5 flat | ~$65+ and variable |
The gap is almost entirely egress. For anything that serves files to users, Spaces' bundled 1 TiB transfer is the difference between a flat $5 and a metered bill that scales with a good traffic day. S3 still wins at massive scale or when you need deep AWS-native integration — but for a small-to-mid file tool, Spaces is dramatically cheaper and far more predictable.
Re-verify current Spaces and S3 pricing before publishing your own figures — both drift.
9. Wiring It Into ToolsHubKit Tools
This is the storage layer under several ToolsHubKit-style tools: the background remover writes transparent PNGs here, an image converter drops its outputs here, a PDF tool stashes generated files here — each returns a signed URL and lets the lifecycle rule clean up. One bucket, one flat bill, S3-compatible so your existing SDK code just works.
Related Guides
- Self-hosted background-removal service with DigitalOcean Spaces — Spaces in action as a tool's result store
- Run a scheduled dependency-vulnerability scanner on a Droplet — archive scan results to Spaces
- Self-host your entire developer stack on one Droplet — where your file tools live
Resources
| Resource | Link |
|---|---|
| DigitalOcean Spaces (+ $200 free credit) | Sign up here |
| Spaces documentation | https://docs.digitalocean.com/products/spaces/ |
| AWS SDK for Java v2 | https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html |
| Flysystem | https://flysystem.thephpleague.com/ |