Deploy Your Own S3-Compatible Storage with Nest3 and Coolify
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:
- Click "+ New Resource"
- Select "Docker Image"
- Choose your server and environment
1.2 Configure the Docker Image
Image: Build from Git repository
Repository URL:
https://github.com/ysrgozudeli/nest3Branch: main
Port: 8080
1.3 Set Environment Variables
Add these environment variables in Coolify:
| Variable | Value | Description |
|---|---|---|
NEST3_ADMIN_USER | myadmin | Admin console username |
NEST3_ADMIN_PASS | super-secret-password | Admin console password |
NEST3_STORAGE_PATH | /data/storage | Where files are stored |
NEST3_DB_PATH | /data/db/nest3.db | SQLite database path |
Important: Change the admin credentials to something secure!
1.4 Configure Persistent Storage
In Coolify's "Storages" section, add a volume:
| Source | Destination | Description |
|---|---|---|
nest3-data | /data | Persistent 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.comCoolify 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
- Click "Tenants" > "+ New Tenant"
- Name:
my-app(or your application name) - 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
- Click on your tenant
- Click "Manage users" > "+ New User"
- Email:
[email protected] - Role:
USER - Click "Create"
2.5 Generate Access Key
- Click "Manage access keys" > "+ New Access Key"
- Select your user
- Description:
Production Spring Boot App - 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: aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5a2.6 Create a Bucket
You can create buckets via the admin UI or via S3 API:
Via Admin UI:
- Go to tenant > "Manage buckets" > "+ New Bucket"
- Name:
uploads - Check "Public Read Access" if you want public file access
- Click "Create Bucket"
Via AWS CLI:
aws --endpoint-url https://storage.yourdomain.com/s3 \
s3 mb s3://uploadsStep 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 itEnvironment Variables (in Coolify for your Spring Boot app):
NEST3_ACCESS_KEY=NESTABCD1234567890XY
NEST3_SECRET_KEY=aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5a3.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.jpgStep 4: Serving Public Assets
For user avatars, images, and other public files, configure your bucket as public-read:
- In Nest3 admin console, go to your bucket
- 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 filesdb/nest3.db- Metadata database
Back up the entire /data volume regularly. In Coolify, you can:
- SSH into your server
- Copy the volume:
docker cp nest3:/data ./backup - 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
| Feature | AWS S3 | Nest3 |
|---|---|---|
| Cost | Pay per GB + requests | Free (self-hosted) |
| Setup | Minutes | Minutes |
| S3 Compatibility | Native | Full (AWS SDK works) |
| Data Location | AWS regions | Your server |
| Scaling | Unlimited | Server disk |
| Admin UI | AWS Console | Built-in |
| Multi-tenant | IAM complexity | Built-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!
