How to Build a Low‑Cost Private Object Storage Service with Nginx and MinIO

This article walks through creating a self‑hosted object storage solution by configuring Nginx for static file serving, implementing secure downloads with X‑Accel‑Redirect and signed URLs, and deploying MinIO as an S3‑compatible service, complete with Spring Boot integration and sample code.

Lin is Dream
Lin is Dream
Lin is Dream
How to Build a Low‑Cost Private Object Storage Service with Nginx and MinIO

Overview

Object storage services store both structured data (e.g., MySQL, Redis) and unstructured data such as documents, videos, and binaries. The article explains how to solve the engineering problem of saving and downloading files by either using a third‑party OSS or building a private object storage service with Nginx and MinIO.

1. Nginx static resource hosting

Nginx is a high‑performance HTTP gateway that supports range requests, making it ideal for serving static files. By letting Nginx read files directly from disk, the Java service avoids loading large byte streams into memory, which greatly improves performance in high‑concurrency scenarios.

Typical response headers control whether a file is displayed inline (e.g., images) or forced to download:

Content-Type: image/png
Content-Disposition: inline
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="a.png"

The following Nginx configuration demonstrates internal path protection, MIME type handling, forced download for non‑image files, and support for range requests:

server {
    listen 80;
    server_name domain.com;

    location /secure-files/ {
        internal;                     # Prevent direct access
        alias /data/files/;           # Physical storage directory
        default_type application/octet-stream;
        types {
            image/jpeg  .jpg .jpeg;
            image/png   .png;
            image/gif   .gif;
            image/webp  .webp;
            image/bmp   .bmp;
            image/tiff  .tiff .tif;
            image/svg+xml .svg;
        }
        if ($sent_http_content_type !~* ^image/) {
            add_header Content-Disposition 'attachment';
        }
        add_header Accept-Ranges bytes;   # Enable resumable download
    }
}

The internal directive marks the location as an internal path that can only be accessed via Nginx’s internal redirects (e.g., X‑Accel‑Redirect). Direct browser requests to the path return 404/403, forcing access through the backend logic where permission checks, logging, rate limiting, and signature verification can be performed.

Signed URL and X‑Accel‑Redirect workflow

To protect private files, the backend generates a signed URL containing the file path, expiration timestamp, and an HMAC signature. Example format:

fileurl=https://domain.com/api/download?file=xxx.pdf&expires=1691234567&sign=abcdef123456

The Spring Boot controller validates the expiration and signature, then returns the X-Accel-Redirect header pointing to the internal Nginx location. Nginx serves the file directly, so the Java process consumes no I/O, memory, or thread resources.

@RestController
@RequestMapping("/api/file")
public class FileController {
    private final String SECRET_KEY = "YourSecretKey";
    private final String NGINX_PREFIX = "/secure-download";
    private final String FILE_ROOT = "/data/files";

    @GetMapping("/download")
    public ResponseEntity<Void> download(@RequestParam String file,
                                          @RequestParam long ts,
                                          @RequestParam String sign) throws UnsupportedEncodingException {
        long now = System.currentTimeMillis() / 1000;
        if (ts < now) {
            return ResponseEntity.status(HttpStatus.GONE).build();
        }
        String expectedSign = DigestUtils.md5DigestAsHex((file + ts + SECRET_KEY).getBytes());
        if (!expectedSign.equals(sign)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        Path filePath = Paths.get(FILE_ROOT, URLDecoder.decode(file, StandardCharsets.UTF_8));
        if (!Files.exists(filePath) || Files.isDirectory(filePath)) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Accel-Redirect", NGINX_PREFIX + "/" + file);
        String mime = Files.probeContentType(filePath);
        if (mime != null && mime.startsWith("image/")) {
            headers.setContentType(MediaType.parseMediaType(mime));
            headers.add("Content-Disposition", "inline; filename=\"" + filePath.getFileName() + "\"");
        } else {
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.add("Content-Disposition", "attachment; filename=\"" + filePath.getFileName() + "\"");
        }
        return new ResponseEntity<>(headers, HttpStatus.OK);
    }
}

2. MinIO as an open‑source object storage solution

While the Nginx approach provides simple static file serving, it lacks lifecycle management, metadata, and advanced permissions. MinIO offers a high‑performance, Amazon S3‑compatible service that can be deployed on cloud, on‑premises, or containers, supporting API uploads, bucket management, and a web console.

Installation on macOS (ARM) example:

# Download binary
curl -O https://dl.min.io/server/minio/release/darwin-arm64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
# Create storage directory
mkdir -p ~/minio-data
# Start server
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=123456789 minio server ~/minio-data --console-address ":9002"

Installation on CentOS (amd64) example:

# Download binary
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
# Create data directory
sudo mkdir -p /data/minio
sudo chown $USER:$USER /data/minio
# Systemd service file (/etc/systemd/system/minio.service)
[Unit]
Description=MinIO
After=network.target

[Service]
User=root
Group=root
ExecStart=/usr/local/bin/minio server /data/minio --console-address ":9001"
Environment=MINIO_ROOT_USER=admin
Environment=MINIO_ROOT_PASSWORD=123456
Restart=always
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable minio
sudo systemctl start minio

After starting MinIO, the console is accessible at http://localhost:9002 (default credentials admin/123456789).

Java integration with MinIO

The following Spring component demonstrates uploading a file, ensuring the bucket exists, and generating a presigned URL for secure access:

public class MinioFileService {
    private final MinioClient minioClient;
    private final String bucketName = "files-local";

    public MinioFileService() {
        this.minioClient = MinioClient.builder()
            .endpoint("http://127.0.0.1:9000")
            .credentials("minioadmin", "minioadmin")
            .build();
    }

    public String upload(MultipartFile file) throws Exception {
        String originalName = file.getOriginalFilename();
        String extension = (originalName != null && originalName.contains("."))
            ? originalName.substring(originalName.lastIndexOf('.')) : "";
        String objectName = "image/" + TimeUtil.todayFolder() + "/" + System.currentTimeMillis() + "-" + UUID.randomUUID() + extension;
        ensureBucketExists();
        try (InputStream is = file.getInputStream()) {
            minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .stream(is, file.getSize(), -1)
                .contentType(file.getContentType())
                .build());
        }
        return "http://127.0.0.1:9000/" + bucketName + "/" + objectName;
    }

    public String getPresignedUrl(String objectName, int expirySeconds) throws Exception {
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
            .bucket(bucketName)
            .object(objectName)
            .method(Method.GET)
            .expiry(expirySeconds)
            .build());
    }

    private void ensureBucketExists() throws Exception {
        boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!exists) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }
}

class TimeUtil {
    public static String todayFolder() {
        return java.time.LocalDate.now().toString().replace("-", "");
    }
}

To make a bucket publicly readable (so that URLs can be accessed without authentication), use the MinIO client ( mc) to set an anonymous download policy:

# Install mc
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc && sudo mv mc /usr/local/bin/
# Configure alias
mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
# Set bucket policy
mc anonymous set download local/files-local

3. Summary

By combining Nginx’s efficient static file serving with X‑Accel‑Redirect and MinIO’s S3‑compatible object storage, developers can build a cost‑effective, self‑hosted file management platform that supports secure downloads, resumable transfers, and flexible bucket policies—ideal for internal tools, media assets, or any project requiring private, high‑performance storage.

Nginx and MinIO diagram
Nginx and MinIO diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootNginxMinioobject storagesigned URLX-Accel-Redirect
Lin is Dream
Written by

Lin is Dream

Sharing Java developer knowledge, practical articles, and continuous insights into computer engineering.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.