News Froggy
newsfroggy
HomeTechReviewProgrammingGamesHow ToAboutContacts
newsfroggy

Your daily source for the latest technology news, startup insights, and innovation trends.

More

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

Categories

  • Tech
  • Review
  • Programming
  • Games
  • How To

© 2026 News Froggy. All rights reserved.

TwitterFacebook
Programming

Self-Host S3-Compatible Object Storage with MinIO on Staging

This guide demonstrates how to self-host an S3-compatible object store using MinIO on your staging server. By leveraging Docker Compose and Traefik for HTTPS, you can significantly reduce cloud storage costs while maintaining a production-like environment for development and testing. It covers setup, application configuration, and secure file interactions.

PublishedJune 2, 2026
Reading Time11 min
Self-Host S3-Compatible Object Storage with MinIO on Staging

Slash Staging Costs: Self-Hosting S3 with MinIO and Docker Compose

As developers, we know the staging environment is a crucible for new features, automated tests, and experimental code. If your application handles file uploads—think user avatars, document PDFs, or media—each interaction in staging likely incurs a real cost on managed object storage services like AWS S3, Cloudflare R2, or Hetzner Object Storage. While these costs are justified in production for their robust features like replication and high availability, they quickly accumulate in staging environments due to frequent database resets, thousands of test uploads, and developer experimentation. This article will show you how to self-host an S3-compatible object store using MinIO on your staging server, leveraging Docker Compose and Traefik to save hundreds of dollars monthly, all while maintaining a production-like testing environment.

The Problem: Unnecessary Staging Storage Costs

Consider the typical staging workflow:

  • Automated end-to-end tests generating numerous dummy files.
  • Nightly database resets leaving orphaned objects.
  • Developers iterating on features, often re-uploading the same files.
  • Accumulation of test data that's rarely cleaned up.

These activities, while essential for development quality, translate directly into storage, request, and egress charges on cloud platforms. The goal is to eliminate this waste without compromising the fidelity of your staging environment.

The Solution: MinIO – An S3-Compatible Alternative

MinIO is a free, open-source object storage server that implements the Amazon S3 API. This S3 compatibility is key: your application code, using the standard AWS SDK, can interact with MinIO precisely as it would with AWS S3, Cloudflare R2, or other S3-compatible services. The only difference becomes an environment variable pointing to your self-hosted MinIO instance instead of a managed cloud endpoint. This setup provides identical code paths across environments, zero storage bills on staging, and a valuable fallback option.

Architecture: Production vs. Staging

A common and cost-effective approach is to segregate storage:

  • Production: Utilizes managed cloud object storage (AWS S3, Cloudflare R2, Hetzner Object Storage) for scalability, durability, and managed backups.
  • Staging / Development: Leverages a self-hosted MinIO instance, typically running in Docker on a VPS.

This architecture ensures your application code remains consistent, only varying the S3_ENDPOINT and credential environment variables.

Image 4: High-level architecture showing a Next.js application uploading files to Cloudflare R2 in production and MinIO in staging through the same S3-compatible API.

This setup offers a cheap staging environment, production-like testing, and reduces vendor lock-in by using the S3 protocol as a universal interface.

Example Environment Variables:

xml S3_ENDPOINT= S3_REGION= S3_ACCESS_KEY= S3_SECRET_KEY= S3_BUCKET=

By simply switching these values, your application seamlessly targets a different storage backend.

Prerequisites

Before you begin, ensure you have:

  • A Linux VPS (e.g., Hetzner, DigitalOcean) with a public IP.
  • Two A records pointing to your staging server's IP (e.g., minio-staging.domain.com, minio-console-staging.domain.com).
  • Docker and Docker Compose v2 installed.
  • Traefik v2 configured as a reverse proxy with Let's Encrypt.
  • Ports 80 and 443 open on your firewall.
  • Approximately 10 GB of free disk space for MinIO data.

If Docker isn't installed, you can use:

bash curl -fsSL https://get.docker.com | sh sudo apt-get install -y docker-compose-plugin docker --version && docker compose version

Step-by-Step Implementation

1. DNS Configuration

In your DNS provider, create two A records pointing to your staging server's public IP. For Cloudflare, ensure minio-staging.domain.com is set to DNS only (gray cloud) to avoid upload size limits and S3 header stripping. The console subdomain can remain proxied.

plaintext minio-staging.domain.com A 203.0.113.45 minio-console-staging.domain.com A 203.0.113.45

2. Running MinIO with Docker Compose

Add the following service to your docker-compose.staging.yml. Crucially, MINIO_SERVER_URL and MINIO_BROWSER_REDIRECT_URL must be set to the public HTTPS domains clients will use to ensure correctly signed URLs.

yaml

docker-compose.staging.yml

networks: proxy: external: true name: proxy internal: name: internal volumes: minio-data: services: minio: image: minio/minio:latest container_name: minio-staging restart: unless-stopped environment: - MINIO_ROOT_USER=${MINIO_ROOT_USER:-admin} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-change-me-please} - MINIO_SERVER_URL=https://minio-staging.domain.com - MINIO_BROWSER_REDIRECT_URL=https://minio-console-staging.domain.com command: server /data --console-address ":9001" volumes: - minio-data:/data networks: - proxy - internal ports: - "9000:9000" # S3 API - "9001:9001" # Web console healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s timeout: 5s retries: 3 start_period: 30s

Bring up the MinIO service:

bash docker compose -f docker-compose.staging.yml up -d minio docker compose -f docker-compose.staging.yml logs -f minio

3. Exposing MinIO with HTTPS via Traefik

Traefik will handle TLS termination and expose MinIO over HTTPS using Let's Encrypt. Add these labels to your minio service within the docker-compose.staging.yml:

yaml labels:

  • "traefik.enable=true"
  • "traefik.docker.network=proxy"

---- S3 API (port 9000) ----

  • "traefik.http.routers.minio-staging.rule=Host(minio-staging.domain.com)"
  • "traefik.http.routers.minio-staging.entrypoints=websecure"
  • "traefik.http.routers.minio-staging.tls.certresolver=letsencrypt"
  • "traefik.http.routers.minio-staging.service=minio-staging"
  • "traefik.http.services.minio-staging.loadbalancer.server.port=9000"

---- Web Console (port 9001) ----

  • "traefik.http.routers.minio-console-staging.rule=Host(minio-console-staging.domain.com)"
  • "traefik.http.routers.minio-console-staging.entrypoints=websecure"
  • "traefik.http.routers.minio-console-staging.tls.certresolver=letsencrypt"
  • "traefik.http.routers.minio-console-staging.service=minio-console-staging"
  • "traefik.http.services.minio-console-staging.loadbalancer.server.port=9001"

Ensure your Traefik configuration (traefik.staging.yml) includes web and websecure entry points and a letsencrypt certificate resolver:

yaml api: dashboard: true entryPoints: web: address: ":80" websecure: address: ":443" certificatesResolvers: letsencrypt: acme: httpChallenge: entryPoint: web email: admin@domain.com storage: /etc/traefik/acme.json providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false network: proxy

After restarting Traefik, verify HTTPS access to https://minio-staging.domain.com.

4. Bucket and Access Key Management

Use the mc (MinIO client) CLI, available inside the MinIO container, to manage buckets and users. First, connect mc:

bash docker exec -it minio-staging
mc alias set local http://localhost:9000 admin change-me-please

Create a bucket for your application:

bash docker exec -it minio-staging mc mb local/domain-files-staging

Set a bucket policy (private, download, or public). private is recommended for sensitive documents, requiring presigned URLs for access. download allows public reads but no listing.

bash

Example: Private policy (recommended)

docker exec -it minio-staging
mc anonymous set none local/domain-files-staging

Crucially, create a dedicated, least-privilege user for your application instead of using the root credentials. Attach a policy allowing s3:* actions only on your specific bucket.

bash docker exec -it minio-staging mc admin user add local
domain-app a-long-random-secret-key cat > /tmp/policy.json <<'EOF' { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:"], "Resource": [ "arn:aws:s3:::domain-files-staging", "arn:aws:s3:::domain-files-staging/" ] } ] } EOF docker cp /tmp/policy.json minio-staging:/tmp/policy.json docker exec -it minio-staging
mc admin policy create local domain-rw /tmp/policy.json docker exec -it minio-staging
mc admin policy attach local domain-rw --user domain-app

Save domain-app and a-long-random-secret-key as your S3_ACCESS_KEY and S3_SECRET_KEY.

5. Application Configuration

Configure your application to use MinIO's endpoint and credentials in staging, and your production service's in production. The S3_FORCE_PATH_STYLE=true setting is vital for both MinIO and other S3-compatible providers like R2/Hetzner, preventing virtual-host style requests that might not resolve correctly.

staging.env:

env

---- Staging: self-hosted MinIO ----

STORAGE_ENABLED=true S3_ENDPOINT=https://minio-staging.domain.com S3_PUBLIC_ENDPOINT=https://minio-staging.domain.com S3_BUCKET=domain-files-staging S3_ACCESS_KEY=domain-app S3_SECRET_KEY=a-long-random-secret-key S3_REGION=us-east-1 S3_FORCE_PATH_STYLE=true

production.env (example for Cloudflare R2):

env

---- Production: Cloudflare R2 ----

STORAGE_ENABLED=true S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com S3_PUBLIC_ENDPOINT=https://files.domain.com S3_BUCKET=domain-files S3_ACCESS_KEY=<r2-access-key> S3_SECRET_KEY=<r2-secret-key> S3_REGION=auto S3_FORCE_PATH_STYLE=true

Your S3 client in code remains identical:

javascript // src/lib/s3.js import { S3Client } from "@aws-sdk/client-s3";

export const s3 = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.S3_REGION, credentials: { accessKeyId: process.env.S3_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_KEY, }, forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true", });

export const BUCKET = process.env.S3_BUCKET; export const PUBLIC_ENDPOINT = process.env.S3_PUBLIC_ENDPOINT;

6. Uploading Files & Presigned URLs

MinIO supports standard S3 upload methods. For user-initiated uploads, presigned URLs are the recommended secure pattern. A presigned URL allows a client (e.g., a browser) to directly PUT or GET an object from MinIO for a limited time without exposing your S3_SECRET_KEY.

Presigned PUT (for uploads from browser): Your backend signs a URL, the browser then uploads the file directly to MinIO. This keeps file data off your API server.

javascript // src/lib/presign.js import { PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { s3, BUCKET } from "./s3.js"; import { randomUUID } from "node:crypto";

export async function presignUpload({ filename, contentType, userId }) { const key = users/${userId}/${randomUUID()}-${filename}; const cmd = new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentType: contentType, }); const uploadUrl = await getSignedUrl(s3, cmd, { expiresIn: 60 * 5 }); // 5 min return { uploadUrl, key }; }

Presigned GET (for downloads from browser): Similar to PUT, your backend signs a URL for a secure, temporary download link.

javascript export async function presignDownload(key, expiresIn = 60 * 10) { const cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key }); return getSignedUrl(s3, cmd, { expiresIn }); }

Why Presigned URLs? They eliminate the need to proxy large files through your application server, reducing CPU/RAM load and increasing throughput. Your application still performs authorization checks before signing the URL.

7. Public URLs (for specific use cases)

For truly public assets, where the bucket policy allows anonymous reads, the URL pattern is https://minio-staging.domain.com/<bucket>/<key>. For instance: https://minio-staging.domain.com/domain-files-staging/users/42/avatar.png.

javascript export function publicUrl(key) { return ${process.env.S3_PUBLIC_ENDPOINT}/${BUCKET}/${key}; }

Remember, for private documents, always use presignDownload(key) to enforce authorization and link expiry.

Security and Maintenance

MinIO offers robust features for security and lifecycle management:

  • CORS: Configure CORS rules on your bucket to allow uploads from your frontend origins. bash cat > /tmp/cors.json <<'EOF' { "CORSRules": [ { "AllowedOrigins": [ "https://crm-staging.domain.com", "http://localhost:3000" ], "AllowedMethods": ["GET", "PUT", "POST", "HEAD"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3000 } ] } EOF docker cp /tmp/cors.json minio-staging:/tmp/cors.json docker exec -it minio-staging
    mc cors set local/domain-files-staging /tmp/cors.json

  • Lifecycle Management: Automatically expire old test files (e.g., after 30 days) to prevent staging storage bloat. bash docker exec -it minio-staging
    mc ilm rule add --expire-days 30 local/domain-files-staging

  • Encryption at Rest: Enable server-side encryption for data at rest. bash docker exec -it minio-staging
    mc encrypt set sse-s3 local/domain-files-staging

  • Hard Rules: Never use default root credentials in production. Restrict console access. Rotate application access keys regularly.

Backups and Monitoring

While self-hosting reduces costs, it shifts responsibility for durability and backups. For staging, a simple mc mirror cron job can periodically sync your MinIO data to a cheap cold storage provider like Backblaze B2 or another S3 endpoint.

bash mc alias set b2 https://s3.us-east-005.backblazeb2.com <B2_KEY> <B2_SECRET> mc mirror --overwrite --remove
staging/domain-files-staging
b2/domain-staging-backup

MinIO also exposes Prometheus metrics at /minio/v2/metrics/cluster for integration with your monitoring stack.

FAQ

Q: Why is MINIO_SERVER_URL crucial for presigned URLs?

A: Without MINIO_SERVER_URL set to the public HTTPS domain, MinIO will sign presigned URLs using its internal Docker hostname (e.g., http://minio:9000), causing verification failures when clients attempt to access the public domain.

Q: What is S3_FORCE_PATH_STYLE=true and why is it important for MinIO and services like Cloudflare R2?

A: S3_FORCE_PATH_STYLE=true tells the S3 SDK to use path-style URLs (e.g., https://minio-staging.domain.com/bucket/key) instead of virtual-host style URLs (e.g., https://bucket.minio-staging.domain.com). MinIO and some S3-compatible services don't support virtual-host style access, so enabling path style ensures correct resolution.

Q: Why should I use presigned URLs for browser uploads instead of proxying through my API server?

A: Presigned URLs allow direct uploads from the browser to MinIO, bypassing your API server entirely. This significantly reduces your API's CPU and RAM load, improves throughput by leveraging MinIO's direct network interface, and avoids consuming your application's bandwidth, leading to a more scalable and cost-efficient architecture for file handling.

#minio#s3#docker-compose#devops#cloud-storage

Related articles

Build Your Own Local NMT App with React Native and QVAC
Programming
freeCodeCampJul 18

Build Your Own Local NMT App with React Native and QVAC

This article explores how Neural Machine Translation (NMT), powered by the Transformer architecture, revolutionized translation by understanding context. We then delve into QVAC, a local-first AI development platform, and its Bergamot engine, enabling private, on-device translation. Learn to set up a React Native app with QVAC and manage model lifecycles for efficient local translation.

Unpacking Roman Concrete's Durability: Carbonation and Self-Healing
Programming
Hacker NewsJul 17

Unpacking Roman Concrete's Durability: Carbonation and Self-Healing

The Enduring Legacy: Roman Concrete's Millennia-Long Stand As software developers, we're familiar with the ephemeral nature of technology; systems evolve, frameworks deprecate, and codebases undergo constant

PayPal in Microservices: NestJS, gRPC, and Docker Blueprint
Programming
freeCodeCampJul 17

PayPal in Microservices: NestJS, gRPC, and Docker Blueprint

Integrating payment logic directly into every microservice within a distributed system often leads to significant challenges. Scattering PayPal API calls across services like user-service, order-service, or

How to Reclaim 22GB on Your Samsung Phone Without Deleting Important
How To
MakeUseOfJul 16

How to Reclaim 22GB on Your Samsung Phone Without Deleting Important

Learn to effectively free up significant storage space on your Samsung phone by emptying trash, removing duplicates, archiving apps, clearing caches, and managing offline files in just a few steps, without sacrificing your essential data.

Demystifying Dijkstra's Algorithm: The Shortest Path Pioneer
Programming
freeCodeCampJul 16

Demystifying Dijkstra's Algorithm: The Shortest Path Pioneer

Explore Dijkstra's Algorithm, the foundational pathfinding technique conceived by Edsger W. Dijkstra. This guide explains how it solves shortest path problems using graphs, nodes, edges, and weights. Learn its greedy approach and the critical role of data structures like adjacency lists and priority queues in its efficient Python implementation.

AWS Leadership Shift: What It Means for Compute and AI/ML
Programming
GeekWireJul 16

AWS Leadership Shift: What It Means for Compute and AI/ML

Dave Brown, a key figure in AWS's EC2 and AI/ML growth, is departing. His successor, Dave Treadwell, brings extensive experience from Microsoft and Amazon's eCommerce Foundation, potentially signaling new directions for core cloud services and AI innovation.

Back to Newsroom

Stay ahead of the curve

Get the latest technology insights delivered to your inbox every morning.