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

Programming
Hacker NewsJun 2

Great Question (YC W21) Seeks Applied AI Interns: A Deep Dive

As fellow developers, we’re constantly scanning the landscape for companies pushing the boundaries, especially in the rapidly evolving AI space. Great Question, a Y Combinator W21 alumnus, has caught our eye with an

Navigating the Global AI Arena: Beyond Silicon Valley's Borders
Programming
Stack Overflow BlogJun 2

Navigating the Global AI Arena: Beyond Silicon Valley's Borders

The international AI landscape presents unique challenges and opportunities, requiring developers to think beyond traditional tech hubs. Key aspects include adapting AI models to local languages and cultures, navigating the complex global supply chain for critical hardware like semiconductors, and understanding how venture capital assesses these international ventures. Success hinges on deep local market understanding, robust technical solutions for localization, and resilience against logistical hurdles.

Programming
Hacker NewsJun 2

Engineering a Solution: Debugging Global Mosquito-Borne Diseases

As developers, we're constantly tasked with solving complex problems, whether it's optimizing a database query or architecting a distributed system. But what if the 'bug' we're trying to fix is biological, with global

Programming
Hacker NewsJun 1

Unleashing LLMs: A 10-Year-Old Xeon is All You Need

This article explores how a 10-year-old Intel Xeon E5-2620 v4 server with 128 GB DDR3 RAM and no GPU can run a modern LLM like Gemma 4 26B-A4B at reading speed. It highlights that LLM inference is often memory-bound and showcases deep optimization techniques using `ik_llama.cpp`, including speculative decoding, CPU-aware MoE routing, advanced memory management, and specialized attention kernels. The success demonstrates that granular software control can unlock significant performance on older, abundant-RAM hardware.

Start 5 Fun, Nerdy Hobbies for Cheap Right Now
How To
How-To GeekMay 31

Start 5 Fun, Nerdy Hobbies for Cheap Right Now

Discover 5 fun, nerdy hobbies you can start today for cheap, including 3D printing, electronics, smart home automation, and self-hosting, with step-by-step guidance and budget-friendly tips.

Secluso: Building Private Home Security on Raspberry Pi with E2EE
Programming
Hacker NewsMay 30

Secluso: Building Private Home Security on Raspberry Pi with E2EE

Reclaiming Privacy in Home Security with Secluso For many developers, the allure of smart home technology, including security cameras, is strong. Yet, the widespread reliance on cloud-based services for video storage

Back to Newsroom

Stay ahead of the curve

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