Self-Hosting

Object Storage with DigitalOcean Spaces for File-Based Tools (S3-Compatible)

July 26, 202615 min read

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:

code
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:

xml
<!-- pom.xml -->
<dependency>
  <groupId>software.amazon.awssdk</groupId>
  <artifactId>s3</artifactId>
  <version>2.28.0</version>
</dependency>

Configure the client against Spaces:

java
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:

bash
composer require league/flysystem-aws-s3-v3

In Laravel, add a disk in config/filesystems.php:

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',
],
env
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:

php
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:

json
{
  "Rules": [
    {
      "ID": "expire-results",
      "Status": "Enabled",
      "Filter": { "Prefix": "results/" },
      "Expiration": { "Days": 1 }
    }
  ]
}

Apply it with the S3-compatible AWS CLI:

bash
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


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/