GoToSocial had a bug serving 0-byte images. I could have patched Go source code. Instead, I bypassed the entire fileserver and offloaded 92,000 files to SeaweedFS. Sometimes the best fix is an architecture pivot, not a code change.


The Problem

GoToSocial v0.22.1 has a confirmed bug where it uses mmap to serve media. On certain configs, this produces EINVAL errors during close(), resulting in HTTP 200 responses with 0-byte bodies. Caddy logs unexpected EOF because the connection drops mid-stream. The bug doesn’t care whether the file exists — the codepath itself is broken.

The Solution

GoToSocial supports S3-compatible storage with redirect mode (storage-s3-proxy: false). Instead of streaming media through its fileserver, GtS issues 302 redirects to a CDN URL. The broken mmap codepath is bypassed entirely — GtS never touches file delivery.

Why SeaweedFS, Not MinIO

MinIO moved from Apache 2.0 to AGPLv3 in 2021, carrying network-use copyleft obligations. By late 2025, the Community Edition was pushed to maintenance mode with features stripped from the UI. A community fork emerged, but the direction is clear. SeaweedFS offered Apache 2.0, single-process deployment (weed server -s3), and 3–4× better upload throughput on small-file workloads. No contest.

The Migration

  • Deployed SeaweedFS in Docker with S3 IAM config (s3.json) for anonymous read and authenticated read/write, plus security.toml for STS signing keys
  • Created the gotosocial bucket via weed shell (S3 API has a known bucket-creation AccessDenied quirk the shell bypasses)
  • Migrated ~92,000 files (15.2 GiB) via rclone move with checksum disabled (--s3-disable-checksum --s3-no-head) to work around a SeaweedFS Content-MD5 validation bug causing 500 InternalError
  • Configured GtS with storage-backend: s3, storage-s3-proxy: false, storage-s3-redirect-url: https://cdn.geekyschmidt.com/gotosocial
  • Set up Caddy to reverse proxy cdn.geekyschmidt.com to SeaweedFS port 8333

Gotchas Along the Way

Volume exhaustion: Default volume count limit (-volume.max) and 1 GiB volume size caused free:0 after ~38,000 files, producing 500s on all subsequent uploads. Fixed with -volume.max=100 and -master.volumeSizeLimitMB=10240.

Bucket routing: The S3 redirect URL must include the bucket name in the path (https://cdn.geekyschmidt.com/gotosocial) — otherwise SeaweedFS interprets the first path segment as the bucket name and returns 404.

The Result

Media flows: GoToSocial → 302 → cdn.geekyschmidt.com → Caddy → SeaweedFS → full file delivered. The mmap codepath is completely bypassed. EOF errors gone. 15 GiB of local media storage reclaimed, served through the CDN, instance back to serving images cleanly.

SeaweedFS