Why I run a local S3 (MinIO) for fast payment/file testing — and the one bug it didn't catch

I run a local S3-compatible server (MinIO) for day-to-day uploads and presigned URL testing to save data and time. It sped iteration—but missed a presigned-policy mismatch that hit staging.

Written by: Arjun Malhotra

A developer working on a laptop with code visible on the screen
Photo by Brooke Cagle on Unsplash

The morning a QA testing an Android build in a Bangalore co‑working space complained that a file upload “succeeded” but the file never appeared in our S3 bucket, I learned two things: 1) waiting for CI to spin up and run full integration tests costs too much time, and 2) debugging uploads from a mobile on flaky office Wi‑Fi while burning through mobile data is miserable.

We were troubleshooting presigned POST uploads. Reproducing the issue on my laptop meant tethering my phone, waiting for a slow upload over my 4G (₹199/day kind of slow), and then waiting for cloud replication. Iteration took 20–30 minutes per change. I wanted the loop under a minute.

So I started running MinIO locally (sometimes on a small ₹300/month VPS) and pointing the AWS SDKs at it. It’s not glamorous, but it fixed the friction I actually had.

The setup that actually saves time

My goal was simple: make uploads and presigned URL flows testable from any device on my network without touching AWS every time.

What I do now:

Concretely, a docker-compose.yml with MinIO, a tiny nginx for TLS, and systemd unit to keep it running got me from 20-minute loops to sub‑1‑minute loops. Upload from the phone, check the object appears instantly, tweak the policy or metadata, repeat.

Why this matters in India: office networks die, personal mobile data is precious, and a ₹300 VPS plus Tailscale means I can test with a real device on a customer’s network without burning my monthly pack.

A few practical tips that saved me time:

Iteration speed improved dramatically. My patch–test–fix loop on presigned POSTs dropped to under two minutes. I stopped assuming network flakiness was the app; I could tell fast whether the client, the content policy, or the upload code was wrong.

The tradeoffs that almost cost us

A couple of things bit me hard before I learned the limits.

  1. Presigned POST policy differences MinIO is S3‑compatible, but its policy validation is more permissive in one important way: it accepted a presigned POST with a Content‑Type wildcard that AWS S3 rejects. I had a test suite that passed against MinIO and we shipped a change. In staging, uploads from some Android WebViews failed with 403s. Debugging that on staging cost us a customer demo. The root cause: MinIO accepted the lax policy; AWS did not.

Lesson: don’t assume compatibility equals equivalence. Add at least one quick integration test against real S3 for security‑sensitive flows like presigned policies, ACLs, or server‑side encryption.

  1. Server-side encryption / KMS We use SSE‑KMS in production. MinIO can do server-side encryption, but it isn’t KMS. A ledger system that relied on KMS‑generated encryption headers behaved differently in production (missing headers, different error codes). We had to run the final tests against real KMS-backed buckets.

  2. Multipart upload edge cases MinIO handled multipart uploads well until a particular client retried with the final complete call slightly out of order. AWS rejected that sequence while MinIO accepted it. This caused a subtle data corruption possibility that only showed up when large files were uploaded under poor network conditions.

  3. Deployment complacency I had a small failure in discipline: teams started treating MinIO tests as sufficient. That relaxed our “one real S3 check” rule until I saw the 403. The organizational cost was a noisy production incident and a back‑and‑forth apology with a customer.

Because of these tradeoffs I changed my routine:

Where it still shines, and what I now accept

Local S3 is still worth it. It reduced wasted mobile data, removed network flakiness from early debugging, and made device testing realistic (I can get a real Android WebView to upload to my MinIO endpoint in seconds). For day‑to‑day development it’s unbeatable.

But I no longer treat MinIO as a drop‑in replacement for S3. It is a development accelerator with known blind spots: presigned policy validation, KMS behavior, and some multipart edge cases. The one failure that cost us was entirely avoidable with a single, cheap integration test against the real service.

Takeaway: Use MinIO (or any local S3) to iterate fast—especially when testing real devices on flaky networks—but add one mandatory real‑S3 sanity check for crypto, presigned policies, and multipart flows. It keeps the fast loop, and prevents being surprised in staging.