Blog/Deploy Your Own S3-Compatible Storage with Nest3 and Coolify

Deploy Your Own S3-Compatible Storage with Nest3 and Coolify

A.Yasar G.·Sat, Jan 24

TL;DR: Stop paying for S3 storage for your side projects. Deploy Nest3 on Coolify in 5 minutes and get free, self-hosted, S3-compatible object storage that works with any AWS SDK.


The Problem

You're building a Spring Boot application that needs file storage. Maybe it's user avatars, document uploads, or generated reports. The obvious choice is AWS S3, but:

  • Cost adds up for side projects and startups
  • Vendor lock-in ties you to AWS
  • Overkill for small to medium workloads
  • Data sovereignty concerns for some use cases

What if you could have S3-compatible storage running on your own server, completely free?

Enter Nest3

Nest3 is a lightweight, open-source, S3-compatible object storage server I built to solve exactly this problem. It features:

  • S3-Compatible API - Works with AWS CLI, SDKs, and any S3 client
  • Multi-Tenant - Isolated storage with quotas per tenant
  • Admin Console - Web UI to manage everything
  • Single Docker Image - No external dependencies
  • Public/Private Buckets - Serve static assets directly

What is Coolify?

Coolify is an open-source, self-hostable alternative to Heroku, Netlify, and Vercel. If you have a VPS (Hetzner, DigitalOcean, etc.), Coolify lets you deploy applications with a beautiful UI, automatic SSL, and zero DevOps headaches.

Architecture Overview

Here's what we're building:

┌─────────────────────────────────────────────────────────┐ │ Your VPS (Coolify) │ │ │ │ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ Spring Boot │ ────> │ Nest3 │ │ │ │ App │ S3 API │ (Object Storage) │ │ │ │ │ <──── │ │ │ │ └──────────────┘ └──────────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ PostgreSQL /data volume │ │ (your app DB) (files stored here) │ └─────────────────────────────────────────────────────────┘

Step 1: Deploy Nest3 on Coolify

1.1 Create New Resource

In your Coolify dashboard:

  1. Click "+ New Resource"
  2. Select "Docker Image"
  3. Choose your server and environment

1.2 Configure the Docker Image

Image: Build from Git repository

Repository URL:

https://github.com/ysrgozudeli/nest3

Branch: main

Port: 8080

1.3 Set Environment Variables

Add these environment variables in Coolify:

VariableValueDescription
NEST3_ADMIN_USERmyadminAdmin console username
NEST3_ADMIN_PASSsuper-secret-passwordAdmin console password
NEST3_STORAGE_PATH/data/storageWhere files are stored
NEST3_DB_PATH/data/db/nest3.dbSQLite database path

Important: Change the admin credentials to something secure!

1.4 Configure Persistent Storage

In Coolify's "Storages" section, add a volume:

SourceDestinationDescription
nest3-data/dataPersistent storage for files and database

This ensures your data survives container restarts and redeployments.

1.5 Configure Domain (Optional)

In "Settings", set your domain:

storage.yourdomain.com

Coolify will automatically provision SSL certificates via Let's Encrypt.

1.6 Deploy

Click "Deploy" and wait for the build to complete. Your Nest3 instance will be available at:

  • Admin Console: https://storage.yourdomain.com
  • S3 API: https://storage.yourdomain.com/s3

Step 2: Configure Nest3

2.1 Login to Admin Console

Open https://storage.yourdomain.com and login with your admin credentials.

2.2 Create a Tenant

  1. Click "Tenants" > "+ New Tenant"
  2. Name: my-app (or your application name)
  3. Click "Create"

2.3 Set Quota (Optional)

Edit the tenant and set a storage quota (e.g., 10 GB) to prevent runaway storage usage.

2.4 Create a User

  1. Click on your tenant
  2. Click "Manage users" > "+ New User"
  3. Email: [email protected]
  4. Role: USER
  5. Click "Create"

2.5 Generate Access Key

  1. Click "Manage access keys" > "+ New Access Key"
  2. Select your user
  3. Description: Production Spring Boot App
  4. Click "Create Access Key"

CRITICAL: Copy both the Access Key ID and Secret Access Key immediately. The secret is only shown once!

Example:

Access Key ID: NESTABCD1234567890XY Secret Access Key: aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5a

2.6 Create a Bucket

You can create buckets via the admin UI or via S3 API:

Via Admin UI:

  1. Go to tenant > "Manage buckets" > "+ New Bucket"
  2. Name: uploads
  3. Check "Public Read Access" if you want public file access
  4. Click "Create Bucket"

Via AWS CLI:

aws --endpoint-url https://storage.yourdomain.com/s3 \ s3 mb s3://uploads

Step 3: Integrate with Spring Boot

Now let's connect your Spring Boot application to Nest3.

3.1 Add AWS SDK Dependency

Maven (pom.xml):

<dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.21.0</version> </dependency>

Gradle (build.gradle):

implementation 'software.amazon.awssdk:s3:2.21.0'

3.2 Configure Application Properties

application.yml:

nest3: endpoint: https://storage.yourdomain.com/s3 access-key: ${NEST3_ACCESS_KEY} secret-key: ${NEST3_SECRET_KEY} bucket: uploads region: us-east-1 # Can be anything, Nest3 ignores it

Environment Variables (in Coolify for your Spring Boot app):

NEST3_ACCESS_KEY=NESTABCD1234567890XY NEST3_SECRET_KEY=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5a

3.3 Create S3 Configuration

package com.example.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import java.net.URI; @Configuration public class S3Config { @Value("${nest3.endpoint}") private String endpoint; @Value("${nest3.access-key}") private String accessKey; @Value("${nest3.secret-key}") private String secretKey; @Value("${nest3.region}") private String region; @Bean public S3Client s3Client() { return S3Client.builder() .endpointOverride(URI.create(endpoint)) .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(accessKey, secretKey))) .forcePathStyle(true) // Important for S3-compatible storage .build(); } }

3.4 Create Storage Service

package com.example.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; import java.io.IOException; import java.util.UUID; @Service public class StorageService { private final S3Client s3Client; private final String bucket; private final String endpoint; public StorageService(S3Client s3Client, @Value("${nest3.bucket}") String bucket, @Value("${nest3.endpoint}") String endpoint) { this.s3Client = s3Client; this.bucket = bucket; this.endpoint = endpoint; } /** * Upload a file and return its URL */ public String upload(MultipartFile file, String folder) throws IOException { String key = folder + "/" + UUID.randomUUID() + "-" + file.getOriginalFilename(); PutObjectRequest request = PutObjectRequest.builder() .bucket(bucket) .key(key) .contentType(file.getContentType()) .build(); s3Client.putObject(request, RequestBody.fromBytes(file.getBytes())); // Return public URL (if bucket is public) return endpoint + "/" + bucket + "/" + key; } /** * Upload with custom key */ public String upload(MultipartFile file, String folder, String filename) throws IOException { String key = folder + "/" + filename; PutObjectRequest request = PutObjectRequest.builder() .bucket(bucket) .key(key) .contentType(file.getContentType()) .build(); s3Client.putObject(request, RequestBody.fromBytes(file.getBytes())); return endpoint + "/" + bucket + "/" + key; } /** * Download a file */ public byte[] download(String key) { GetObjectRequest request = GetObjectRequest.builder() .bucket(bucket) .key(key) .build(); return s3Client.getObjectAsBytes(request).asByteArray(); } /** * Delete a file */ public void delete(String key) { DeleteObjectRequest request = DeleteObjectRequest.builder() .bucket(bucket) .key(key) .build(); s3Client.deleteObject(request); } /** * Check if file exists */ public boolean exists(String key) { try { HeadObjectRequest request = HeadObjectRequest.builder() .bucket(bucket) .key(key) .build(); s3Client.headObject(request); return true; } catch (NoSuchKeyException e) { return false; } } /** * Get public URL for a key */ public String getPublicUrl(String key) { return endpoint + "/" + bucket + "/" + key; } }

3.5 Create Upload Controller

package com.example.controller; import com.example.service.StorageService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map; @RestController @RequestMapping("/api/files") public class FileController { private final StorageService storageService; public FileController(StorageService storageService) { this.storageService = storageService; } @PostMapping("/upload") public ResponseEntity<Map<String, String>> upload( @RequestParam("file") MultipartFile file, @RequestParam(value = "folder", defaultValue = "uploads") String folder) { try { String url = storageService.upload(file, folder); return ResponseEntity.ok(Map.of( "url", url, "message", "File uploaded successfully" )); } catch (IOException e) { return ResponseEntity.badRequest().body(Map.of( "error", "Upload failed: " + e.getMessage() )); } } @DeleteMapping public ResponseEntity<Map<String, String>> delete(@RequestParam("key") String key) { storageService.delete(key); return ResponseEntity.ok(Map.of("message", "File deleted")); } }

3.6 Usage Example

Upload a file:

curl -X POST https://your-app.com/api/files/upload \ -F "[email protected]" \ -F "folder=avatars"

Response:

{ "url": "https://storage.yourdomain.com/s3/uploads/avatars/550e8400-e29b-41d4-a716-446655440000-photo.jpg", "message": "File uploaded successfully" }

Access the file (if bucket is public):

https://storage.yourdomain.com/s3/uploads/avatars/550e8400-e29b-41d4-a716-446655440000-photo.jpg

Step 4: Serving Public Assets

For user avatars, images, and other public files, configure your bucket as public-read:

  1. In Nest3 admin console, go to your bucket
  2. Click the "Private" badge to toggle to "Public"

Now files can be accessed directly without authentication:

<img src="https://storage.yourdomain.com/s3/uploads/avatars/user-123.jpg" />

This is perfect for:

  • User profile pictures
  • Product images
  • Public documents
  • Static assets

Step 5: Best Practices

5.1 Organize with Folders

Use a consistent folder structure:

uploads/ avatars/ user-{id}.jpg documents/ {year}/{month}/ document-{id}.pdf temp/ ...

5.2 Secure Private Files

For sensitive files, keep the bucket private and generate temporary access:

// In your controller, proxy the download @GetMapping("/documents/{id}") public ResponseEntity<byte[]> downloadDocument(@PathVariable Long id) { // Check user permissions first! Document doc = documentService.findById(id); byte[] data = storageService.download(doc.getStorageKey()); return ResponseEntity.ok() .header("Content-Disposition", "attachment; filename=" + doc.getFilename()) .contentType(MediaType.parseMediaType(doc.getContentType())) .body(data); }

5.3 Set Storage Quotas

In the Nest3 admin console, set quotas per tenant to prevent storage abuse:

  • Development: 1-5 GB
  • Production: Based on your expected usage

5.4 Backup Your Data

The Nest3 data volume contains:

  • storage/ - All uploaded files
  • db/nest3.db - Metadata database

Back up the entire /data volume regularly. In Coolify, you can:

  1. SSH into your server
  2. Copy the volume: docker cp nest3:/data ./backup
  3. Or use Coolify's backup features

5.5 Monitor Usage

Check the Nest3 dashboard for:

  • Total objects stored
  • Storage usage per tenant
  • Recent access key activity

Comparison: Nest3 vs AWS S3

FeatureAWS S3Nest3
CostPay per GB + requestsFree (self-hosted)
SetupMinutesMinutes
S3 CompatibilityNativeFull (AWS SDK works)
Data LocationAWS regionsYour server
ScalingUnlimitedServer disk
Admin UIAWS ConsoleBuilt-in
Multi-tenantIAM complexityBuilt-in

When to use Nest3:

  • Side projects and startups
  • Data sovereignty requirements
  • Cost-sensitive applications
  • Development and staging environments
  • Small to medium file storage needs

When to use AWS S3:

  • Massive scale (petabytes)
  • Global CDN requirements
  • Enterprise compliance needs
  • Serverless architectures

Conclusion

With Nest3 and Coolify, you can have production-ready, S3-compatible object storage running in minutes. No AWS bills, no vendor lock-in, and complete control over your data.

The best part? Your Spring Boot application doesn't know the difference - it's just talking to "S3".

Links:


Have questions or feedback? Open an issue on GitHub!