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, plussecurity.tomlfor STS signing keys - Created the
gotosocialbucket viaweed shell(S3 API has a known bucket-creationAccessDeniedquirk the shell bypasses) - Migrated ~92,000 files (15.2 GiB) via
rclone movewith checksum disabled (--s3-disable-checksum --s3-no-head) to work around a SeaweedFS Content-MD5 validation bug causing 500InternalError - 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.comto 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.
