Skip to content
Go back

Stoat vs Fluxer: Self-Hosting Costs

By SumGuy 13 min read
Stoat vs Fluxer: Self-Hosting Costs
Contents

Fluxer ships 25 containers and Stoat ships 16, and the 25-container stack is the one that is actually easier to run.

If you pick a self-hosted Discord alternative by counting services in the Compose file, you will pick wrong. Every time someone asks “how hard is this to self-host,” the answer they reach for is a container count, because a container count is a number and numbers feel objective. They are not. A 16-container stack that pins nothing, healthchecks almost nothing, and wants a 101-port UDP range through your firewall is harder to run than a 25-container stack that pins its third-party images, healthchecks 22 of its services, and asks for exactly one UDP port.

This piece is about running the software, not choosing it. If you are still deciding whether to move your community off Discord at all, start with Discord Alternatives That Actually Work and come back here once you have narrowed it to these two.

That is the actual comparison here: Stoat, the self-hosted chat platform formerly named Revolt, against Fluxer, a newer entrant on the same AGPL-3.0 turf. Neither is a benchmark result. Nothing in this piece got deployed, measured, or load tested. I read both Compose files on 6 September 2026, straight from each project’s repository, and cross-checked the star and fork counts against the GitHub API the same day. Everything below is what the two projects declare about themselves in their own configuration. That declaration is not nothing: a Compose file with no memory limits and two healthchecks tells you the maintainers have not yet had to think hard about a bad restart at 3 AM. A Compose file with 22 healthchecks and a memory cap on every service tells you they already have.

What You Are Actually Standing Up

Stoat is 16 compose services (15 long-running, plus a one-shot createbuckets init job): a database, a cache, a message queue, object storage, a reverse proxy, an API layer, an events service, and a scattering of feature services (autumn for file uploads, january for link previews, gifbox, crond, pushd for push notifications) alongside the voice stack. Under the hood that is MongoDB, a cache, RabbitMQ, and MinIO, four datastores in total.

One entry in that list will trip you up. Stoat’s cache service is named redis in the Compose file, but the image behind it is valkey/valkey:9-alpine. Stoat runs Valkey, not Redis. Treat the service name as a leftover from an older stack and read the image line instead.

Fluxer is 25 compose services (24 long-running, plus a one-shot seaweedfs-init job): Postgres, Valkey, NATS, Meilisearch, and SeaweedFS behind a Caddy front door, then an API, a background worker, a gateway, a media proxy, a static proxy, and an app proxy. The remaining ten services are domain logic split into pairs, one worker and one shard companion each, for snowflakes, users, gifs, messages, and unfurls. Five datastores in total: Postgres, Valkey, NATS, Meilisearch, SeaweedFS.

Read those two lists side by side and the container-count gap makes sense on its own. Fluxer split its domain logic into worker/shard pairs instead of one API monolith handling everything. That is a real architectural choice with a real cost, more services to schedule, more images to pull, more log streams to tail. It is also the choice that makes the rest of this comparison go the way it does.

The Head-to-Head

StoatFluxer
Compose services1625
Datastores45
Healthchecks222
Memory limitsnoneall 25 services
Unpinned images40 third-party (own images on moving v1 tag)
LiveKit versionv1.9.13v1.12.0
Public voice ports101 UDP (50000-50100)1 UDP (7882)

Every column past the first two favors Fluxer, and Fluxer is the stack with nine more containers in it. That is the whole argument. Now the detail behind each row.

Image Pinning: One Floating Tag Is All It Takes

Stoat hard-pins its own application images to v0.15.1 and pins the web frontend to a specific commit, 0c31cf0. That part is done correctly. But four third-party images ride with no tag at all: docker.io/mongo, docker.io/minio/minio, docker.io/caddy, and docker.io/minio/mc. No tag means Docker resolves to latest, and latest means the next docker compose pull can hand you a new MongoDB major version on a Tuesday, with no changelog read and no rollback plan in place. A database major upgrade is not the kind of thing you want arriving as a side effect of an unrelated update.

Fluxer hard-pins every third-party image with an explicit version: postgres:16-alpine, valkey/valkey:8.1-alpine, nats:2.14-alpine, getmeili/meilisearch:v1.12, chrislusf/seaweedfs:4.34, livekit/livekit-server:v1.12.0, caddy:2.10-alpine. A docker compose pull on this stack changes nothing unless you go edit the Compose file first.

That said, do not walk away thinking Fluxer pins everything. Its own application images default to tag v1, and v1 is a moving major tag, not a digest or a fixed patch version. It is a smaller blast radius than an unversioned third-party image (a major-version bump inside your own app is at least something the Fluxer team controls and tests), but it is not the same guarantee as a pinned commit. Fluxer gets the third-party pinning right and leaves the same rough edge Stoat has, just on a shorter list.

Healthchecks: 2 vs 22

Stoat declares two healthcheck blocks and eight depends_on blocks. Fluxer declares 22 healthchecks and 18 depends_on blocks. On paper that gap looks huge, and on a cold docker compose up it is.

A healthcheck is what tells Docker (and depends_on: condition: service_healthy) that a dependency is actually ready, not just that its process started. Without a healthcheck, depends_on only waits for the container to exist. On Stoat, the API can start reaching for MongoDB, MinIO, or the cache before any of them have finished booting, and the failure mode is a container that starts, crashes, and restarts a few times before things settle. It usually works out on its own. It is also exactly the kind of flake that eats twenty minutes of your evening before you remember the ordering problem was baked into the Compose file all along.

Fluxer’s 22 healthchecks mean 22 of its 25 services can gate their own startup on a real readiness signal instead of a guess. That is more YAML to write and more YAML the Fluxer maintainers had to test. It is also the difference between a stack that boots in a predictable order and one that boots in whatever order the scheduler feels like that day.

Memory: Declared Caps, Not Measurements

Read carefully here: nothing below is a measurement. Nothing has been deployed. These are the deploy.resources limits and reservations written into Fluxer’s Compose file, the ceiling the project itself declares, not what it uses in practice.

Stoat’s Compose file has no deploy.resources blocks at all. No service declares a memory limit or a reservation. Every container can grow until the host itself falls over, and there is nothing in the Compose file stopping one runaway service from starving the rest.

Fluxer declares a memory limit on all 25 services. Summed, the limits total 17,152 MB (16.75 GB), a hard ceiling if you never touch a single value. Only four services declare a reservation at all: Postgres (3072 MB), the API (1024 MB), the worker (1024 MB), and the gateway (384 MB), summing to 5,504 MB (5.38 GB) as the floor Fluxer actually asks the host to hold aside.

The 25-container number is the misleading part. Postgres, the API, and the worker alone account for 10,240 MB of limit, about 60 percent of the entire 16.75 GB ceiling. Meanwhile all ten of the domain and shard containers, the ones actually driving the service count up to 25, cap out at a combined 1,920 MB. Container count and memory footprint are not the same axis, and Fluxer’s own Compose file is the proof.

fluxer docker-compose.yml
livekit:
image: livekit/livekit-server:v1.12.0
deploy:
resources:
limits:
memory: ${FLUXER_LIVEKIT_MEMORY_LIMIT:-512mb}

Every one of these caps is an env var away from being raised or lowered. If your host has 32 GB free and you want Postgres to breathe, override FLUXER_POSTGRES_MEMORY_LIMIT and move on. Stoat gives you nothing to override because it declares nothing to begin with, which is its own kind of flexibility and its own kind of risk.

Voice Chat: This Is Where The Story Actually Lives

Container count and memory caps are interesting. This is the section that decides whether your evening goes well.

Stoat’s LiveKit service publishes 7881:7881/tcp for the signaling connection, plus 50000-50100:50000-50100/udp. That is 101 individual UDP ports you need to forward on your router, and if you run a firewall in front of your host, 101 rules or one wide range rule, either way a meaningfully larger attack surface than a single port. Behind carrier-grade NAT, which is most mobile connections and a growing share of residential ones, a 101-port range is not something you can forward at all; there is no router UI on the other end for you to configure. Through a Cloudflare Tunnel, it is simply not possible: tunnels proxy TCP and a limited set of protocols, not a hundred-plus arbitrary UDP ports.

Fluxer’s LiveKit service publishes the same 7881:7881/tcp signaling port, plus a single 7882:7882/udp, LiveKit’s UDP mux, which multiplexes every voice stream over that one port instead of allocating a fresh port per call. Fluxer keeps port 7880, LiveKit’s other TCP port, internal, reachable only through Caddy, and separately publishes 443/udp for HTTP/3. One UDP port to forward is a five-minute router change instead of an evening lost to port-range syntax.

stoat compose.yml
livekit:
ports:
- "7881:7881"
- "50000-50100:50000-50100/udp"
fluxer docker-compose.yml
livekit:
ports:
- "7881:7881"
- "7882:7882/udp"

Neither stack escapes physics here. WebRTC media is peer-negotiated, and behind a restrictive NAT or a Cloudflare Tunnel, both Stoat and Fluxer still need a TURN server to relay voice traffic, LiveKit ships one, but you still have to run and expose it. Both projects will eventually make you do extra plumbing for the hard network cases. What separates them is the common case, where one asks for a single forwarded port and the other asks for a hundred and one. Stoat runs LiveKit v1.9.13, Fluxer runs the newer v1.12.0.

Revolt Scar Tissue

Stoat used to be named Revolt. The project renamed itself to Stoat in October 2025 after a cease-and-desist, and the GitHub organization moved from revoltchat to stoatchat (the old revoltchat/self-hosted URL still resolves to the same repository). That is a normal thing for an open-source project to do when a name collides with someone else’s trademark.

What is funnier, and entirely checkable in the Compose file, is what did not get renamed. Stoat is configured almost entirely through one file you bind-mount into the containers, and that file is still called Revolt.toml. Eight services mount it, each one as ./Revolt.toml on your host mapped to /Revolt.toml inside the container. For a project called Stoat, you will still be editing a file named after the old brand as of September 2026.

stoat compose.yml
volumes:
- type: bind
source: ./Revolt.toml
target: /Revolt.toml

That costs nothing to run, and it is worth knowing before you go hunting for a config file named after the project you actually installed. It is a good story about how thorough a rename turns out to be once you get past the parts people see.

Who Should Pick Which

Pick Stoat if you already have opinions about your firewall and you want a codebase with years of production history behind it. stoatchat/stoatchat has 3,281 stars and 385 forks, stoatchat/self-hosted has 2,624 stars and 295 forks, and the project traces back to 2021. That is a long runway of real-world edge cases already found and fixed, even if the Compose file itself hasn’t caught up on pinning and healthchecks.

Pick Fluxer if you’re behind NAT, behind a tunnel, or you just want a stack that tells you when something is actually broken instead of quietly restarting. Fluxer is younger (the repository was created 1 January 2026) but already sits at 10,196 stars and 741 forks as of September 2026, and its Compose file is the one that reads like someone got burned by a 3 AM page and fixed the underlying config instead of just writing a runbook about it.

Both are AGPL-3.0. Neither is going to feel like a weekend project either way, but the amount of Compose-file homework you’ll do after the initial docker compose up is not close.

The Fair Shake

Fluxer is not a finished product standing in for Discord. It has no native mobile apps and no federation, and both are on its own public roadmap, not promises I am inventing here. If a mobile app matters to you today, Fluxer does not have one to offer. Stoat, being older, has had more time to grow client coverage and community tooling around it. Newer and better-configured are two different axes, and Fluxer only wins on one of them. The other one, three more years of real users finding real edge cases, still belongs to Stoat.

Common Questions

How much RAM do I need to self-host Fluxer?

Fluxer’s Compose file reserves 5.38 GB (5,504 MB) across four services and caps every service’s combined limit at 16.75 GB (17,152 MB). Both numbers are declared ceilings and floors read from deploy.resources in the Compose file, not measured usage from a running deployment. Plan around the reservation floor and leave headroom under the limit ceiling; every cap is overridable by its own env var.

Do I need to open a port range for Stoat voice chat?

Yes. Stoat’s LiveKit service publishes 7881/tcp plus a 50000-50100/udp range, 101 separate UDP ports, for voice and video. That range needs forwarding on your router or firewall for calls to connect reliably. Fluxer’s equivalent setup needs only one UDP port, 7882, because it multiplexes every call over LiveKit’s UDP mux instead of allocating one port per connection.

Can I run Stoat or Fluxer behind a Cloudflare Tunnel?

Partially. A Cloudflare Tunnel handles the web UI and API traffic fine for both projects. Voice chat will not work over the tunnel itself: WebRTC media needs direct UDP reachability or a TURN relay, and tunnels proxy HTTP-style traffic, not arbitrary UDP. Behind NAT or a tunnel, both Stoat and Fluxer need a TURN server exposed separately for calls to connect.

Is Stoat the same thing as Revolt?

Yes. Stoat is Revolt renamed in October 2025 after a cease-and-desist, with the GitHub organization moved from revoltchat to stoatchat. The rename is incomplete under the hood: as of September 2026 the configuration file you bind-mount into the containers is still named Revolt.toml, and eight of Stoat’s services mount it under that name.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Neovim Setup Without the 6-Month Slog

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts