The demo of self-hosting takes three commands and about two minutes. The real version takes an afternoon, because between “it runs on localhost” and “my team uses this daily” there are four things — a domain, TLS, backups and a plan for upgrades — that nobody puts in the quickstart.
This walks through both. Examples use Fluid Chat, but the shape applies to any Docker-based chat platform.
What to provision
Start smaller than you think. Team chat is a low-throughput workload — a busy 50-person team generates a few thousand messages a day, which is a rounding error for Postgres.
- Up to ~25 people: 1 vCPU, 2 GB RAM, 25 GB disk. A cheap VPS genuinely does this.
- 25–100 people: 2 vCPU, 4 GB RAM, and consider managed Postgres so backups are somebody else's routine.
- A domain name you control, pointed at the box.
- Object storage for uploads — any S3-compatible service, or MinIO on the same machine.
- Docker and Docker Compose installed. That's the only host software you need.
Step 1: Clone and start
git clone https://github.com/azianmike/fluid-chat.gitcd fluid-chatcp .env.example .envdocker compose up app ready on http://localhost:3000 realtime websockets on :3001 worker jobs running postgres migrations appliedFour things start: the app, a realtime server, a worker for scheduled jobs, and Postgres. Migrations run automatically. Open the app, create the first account — it becomes the workspace owner, with #general and #random already there.
At this point you have a working chat server. It's on localhost, it's HTTP, and it will lose everything if the box dies — so don't invite anyone yet.
Step 2: Configure the environment
Open .env. The defaults are set up for a local run; a few need real values before anyone else touches it.
- 1Secrets. Generate fresh values for anything the file marks as a secret. Do not ship the example values — they're examples.
- 2Database URL. Point at the bundled Postgres, or at your managed instance if you'd rather someone else handled backups.
- 3Public URL. The app needs to know its own address to build correct links and permalinks. Getting this wrong produces links that work for you and nobody else.
- 4Object storage. S3-compatible credentials and bucket for uploads and exports.
- 5SMTP, if you want invitation and notification email. Optional — you can invite people by link instead.
The repo's docs/env.md covers every variable. Read it once now rather than three times later.
Step 3: TLS and a domain
This is where most self-hosting afternoons go, and it's worth being deliberate. Put a reverse proxy in front — Caddy, nginx or Traefik — terminate TLS there, and route to the app. Two things trip people up:
- Websockets need explicit proxy configuration. Upgrade headers must be forwarded or realtime silently fails while everything else looks fine. This is the single most common self-hosting bug in chat software, and the symptom — “messages appear when I refresh” — is easy to misdiagnose as a performance problem.
- The public URL must match the domain, or permalinks and invitations will point somewhere useless.
Caddy is the fastest route if you don't already have opinions: it handles certificates automatically and its websocket defaults are sensible. The repo has an HTTPS and domains doc with the specifics.
Step 4: Backups, before you invite anyone
Backups are the difference between self-hosting and gambling. Fluid keeps everything in one Postgres plus your object store, so this stays simple:
# nightly, to somewhere that is not this serverdocker compose exec -T postgres pg_dump -U fluid fluid | gzip > backup.sql.gz# then sync your object storage bucket as well# then — and this is the step everyone skips —# restore it into a scratch container and log in- 1Dump Postgres on a schedule. Nightly is fine for chat.
- 2Ship the dump off the box. A backup on the same disk is not a backup.
- 3Sync the object store too, or your archive will have messages referencing files that don't exist.
- 4Restore once, on purpose. Spin up a scratch instance from the backup and log in. This hour is the highest-value hour in the whole deployment.
Step 5: Upgrades and maintenance
The ongoing cost is roughly an hour a month, and it's mostly this:
- Pull and restart to upgrade — migrations run on start. Take a backup first, every time, no exceptions.
- Patch the host. Unattended upgrades for security patches is the low-effort answer.
- Watch disk. Uploads accumulate quietly. Set an alert at 80%; disk-full is a particularly annoying way to discover a problem.
- Check the logs occasionally rather than only when someone complains.
When to add Redis
Redis is optional in Fluid until you run more than one app or realtime process. At that point, realtime events need to fan out across processes via pub/sub instead of a direct HTTP post — otherwise a message sent through process A never reaches someone connected to process B.
One process, no Redis. Scaling out, add Redis. That's the whole rule, and it's a good example of a dependency that earns its place rather than being there from day one.
The things that usually go wrong
| Symptom | Almost always |
|---|---|
| Messages only appear after refresh | Proxy not forwarding websocket upgrade headers |
| Invitation links point to localhost | Public URL not set to the real domain |
| Uploads fail silently | Object storage credentials, bucket name, or CORS |
| No email arrives | SMTP not configured — invite by link instead |
| Everything slow after a few months | Disk nearly full, or Postgres never vacuumed |
Start with the two-minute version.
Get it running on localhost first, then do the domain, TLS and backups properly. Or skip all of it — the hosted version is free too.