Documentation
Running it in production
There are two profiles. Profile A is one process, SQLite, local media and a TLS reverse proxy. Profile B is several processes, PostgreSQL and S3. This page states what a deployment must be true of; the commands that get you there differ per operating system, and none of those systems is treated as the default.
Profile A: one process
One littleFedi process, SQLite in WAL mode, local owned media, one TLS reverse proxy, encrypted off-host backups. This is the profile almost everyone wants.
Start with at most 100 local accounts, 50 concurrent streams, 10 inbound activities per second sustained, 40 MiB uploads, and a queue that returns to its normal depth within an hour. Those are conservative operating limits, not benchmarked guarantees. Record a 24-hour staging soak before you raise them.
The default objective is a 24-hour recovery point and a four-hour recovery time. Backups run daily. Do not consider a release fit for your users until you have personally restored one inside four hours.
Host requirements
The binary is static and self-contained, so every supported system carries the same requirements. What differs is only how you express them.
- An unprivileged service account that owns nothing but its own data.
- A private data directory, mode
0700, owned by that account, holding the database,media/andbackups/. - A root-owned binary, mode
0755, that the service account cannot replace. - A configuration file, mode
0640, readable by the service group and writable only by root. - A secrets file beside it, mode
0640, holding the MFA encryption key, the media proxy secret and the SMTP password as plainKEY=valuelines. Never in the TOML, never in a command argument. - A descriptor limit of at least 65536, applied by whatever mechanism the platform provides.
- A service definition that starts the process as that account, stops it with
SIGTERM, allows at least 45 seconds for shutdown, and restarts it after a failure where the service manager supports that. - A reverse proxy terminating TLS in front of the loopback listener.
Platforms differ in how much they add on top. The systemd unit confines the filesystem, the temporary directory and kernel interfaces; SMF drops privileges and pairs with zones; the BSDs rely on the account, the directory modes and, on FreeBSD, jails. None of that is load bearing. The list above is what the deployment actually depends on, and each platform guide states plainly what its service manager does and does not add.
Identity bootstrap
- Keep
registrations = false,require_admin_mfa = falseandrequire_email = truefor the bootstrap. - Create the administrator with an explicitly verified email using the local CLI. Sign in, enroll TOTP, and store the recovery codes offline.
- Set
require_admin_mfa = true, restart, and confirm that web, admin, API, OAuth and streaming access all still work. - On an upgraded instance, run
admin email-verification-reportbefore turning verified email on. - Keep registration closed for launch. If you enable invitations later, keep approval and verified email required.
DNS, TLS and network
- Point A and AAAA at the proxy and test both families. If IPv6 is not routed and firewalled correctly, do not publish AAAA at all.
- Permit inbound TCP 80 and 443 only. Keep 8080, metrics, pprof, the database and SSH administration off the public network. Allow outbound 443, the SMTP submission port, DNS and NTP.
- Each platform guide ships a ruleset for its own firewall: pf, npf, ipfilter or nftables.
- Use an automated ACME client, alert at 21 days to expiry, and test renewal with the client's own dry run.
proxy_uriis the exact public HTTPS origin.trusted_proxiesis loopback only in this topology.
Start and smoke test
curl --fail http://127.0.0.1:8080/health
curl --fail http://127.0.0.1:8080/readyz
curl --fail https://social.example.com/.well-known/nodeinfo
Then confirm, by hand:
- HSTS and CSP headers are present.
- Login and MFA work.
- A post with media publishes and renders.
- WebSocket and SSE reconnect after an interruption.
- The administrator SMTP test arrives.
- A remote follow works in both directions.
- The queue drains.
Then reboot the machine once and repeat all of it. A deployment that has never survived a reboot is not deployed, it is running.
The process writes structured lines to stdout, and the service manager decides where they land: the journal, syslog, or an SMF service log. Configure that transport's retention and forward alerts without forwarding private request content.
Upgrade and rollback
Drain proxy traffic, stop littleFedi, take an off-host encrypted backup and copy
it away, install the new root-owned binary, start one process so migrations run
exactly once, then restore traffic and watch /readyz and the queue
age.
For any migration that changes schema or data, rollback means stopping the new binary and restoring the pre-upgrade backup. Never start an older binary against a database whose newer migrations have already executed. There is no supported path back from that.
Profile B: PostgreSQL and S3
Profile B targets 500 local accounts and 100 simultaneous streams. Those are certification inputs, not a promise that every host and provider meets a fixed latency.
background_maintenance = false
[db]
dsn = "postgres://littlefedi:secret@db/littlefedi?sslmode=require"
[streaming]
backend = "postgres"
[media]
backend = "s3"
cache_remote = "off"
[media.s3]
bucket = "littlefedi-media"
region = "eu-west-1"
[backup]
enabled = false
include_media = false
[blog]
enabled = false
Every process shares the same PostgreSQL DSN, the same streaming backend and the
same bucket. Exactly one process runs with
background_maintenance = true. If that process stops, request
handling, queues, actor refresh and streaming continue; housekeeping pauses until
it comes back. The role is explicit on purpose, so the cluster does not need a
distributed election to decide who runs the sweeps.
- The application rate limiter is per process, so the reverse proxy must apply a shared limit.
- Do not use the built-in media backup with S3. Use PostgreSQL-native or managed backups plus bucket versioning and provider replication, and restore both into an isolated environment before you rely on them.
- Static blogs are not part of Profile B. Their generated files and rebuild coalescing are process-local. Leave them off.
- Start one process first during an upgrade, wait for migrations and readiness, then roll the rest.
There is an opt-in contract test you can run against a disposable prefix on the real provider, because an emulator does not certify production behaviour:
LITTLEFEDI_TEST_S3_BUCKET=littlefedi-staging \
LITTLEFEDI_TEST_S3_REGION=eu-west-1 \
go test -tags s3 -run TestS3ProviderContract ./internal/media/objectstore
Profile B is documented, implemented and tested in the repository. The external checks that would let anyone call it certified, two processes against a real provider, a load scenario, restarts, a restore, and a 24-hour clustered soak, are still open.
Platform conventions
One static binary means the differences between operating systems are entirely in packaging: where files go, which service manager starts the process, which firewall admits the two public ports, and where the logs land.
| Alpine | FreeBSD | illumos | Linux | NetBSD | OpenBSD | |
|---|---|---|---|---|---|---|
| Packages | apk | pkg | IPS / pkgin | distribution | pkgin | pkg_add |
| Service manager | OpenRC | rc.d | SMF | systemd | rc.d | rc.d, rcctl |
| Service account | littlefedi | littlefedi | littlefedi | littlefedi | littlefedi | _littlefedi |
| Configuration | /etc/littlefedi | /usr/local/etc/littlefedi | /etc/littlefedi | /etc/littlefedi | /usr/local/etc/littlefedi | /etc/littlefedi |
| Data | /var/lib/littlefedi | /var/db/littlefedi | /var/littlefedi | /var/lib/littlefedi | /var/db/littlefedi | /var/littlefedi |
| Firewall | nftables | pf | ipfilter | nftables, ufw | npf | pf |
| ACME | certbot, Caddy | acme.sh, certbot, Caddy | acme.sh, certbot, Caddy | certbot, Caddy | acme-client, certbot, Caddy | acme-client(1) |
| Logs | supervise-daemon | syslog, newsyslog | SMF log, logadm | journald | syslog, newsyslog | syslog, newsyslog |
| Descriptor limit | rc_ulimit | login class | project | LimitNOFILE | login class | login class |
| Restart on failure | yes | daemon(8) -R | SMF | systemd | no | no |
Ports to other systems are welcome. macOS and Windows binaries are built and published, but there is no operations guide for them: they are development and evaluation targets, not deployment targets.
These pages are written from each system's documented conventions and from the release matrix. A platform counts as verified only once an operator has recorded a full install, smoke test and reboot against it. Do not read a page's existence as evidence that anyone has done that yet.
Which SQLite driver you get
The default build embeds SQLite. Two drivers cover the supported platforms, and which one you get is decided at build time by the target, not by configuration. It matters, because one of them is much more limited.
| Driver | Platforms | Behaviour |
|---|---|---|
modernc.org/sqlite |
FreeBSD, Linux, NetBSD amd64, OpenBSD amd64 and arm64, macOS, Windows | Native translation of SQLite. No restrictions. |
ncruces/go-sqlite3 |
illumos, NetBSD arm and arm64 | SQLite in a WASM interpreter. One database connection, the Go heap
capped at 128 MiB unless GOMEMLIMIT is set, and slower. |
On a fallback platform a personal or small community instance works as shipped. Anything larger should use PostgreSQL, whose driver is native everywhere.
The fallback driver is pure Go with no platform-specific code, and that is the reason the build matrix below reaches as far as it does: where the native driver has no port, the interpreter takes over and the binary still links.
Every target it compiles for
make allreleases builds 40 targets across 12 operating systems. The
list is derived from the Go toolchain rather than hand-typed, so a new port
appears here on its own. 32-bit ARM targets are built twice, for ARMv6 and ARMv7,
so the binary count is higher than the target count.
| Operating system | Published binariesmake release |
Full release matrixmake release-all |
Also compilesmake allreleases |
|---|---|---|---|
| FreeBSD | amd64, arm64 | 386, arm | none |
| Linux | amd64, arm64, arm | 386, loong64, ppc64le, riscv64, s390x | mips, mipsle, mips64, mips64le, ppc64 |
| NetBSD | amd64, arm | arm64 | 386 |
| OpenBSD | amd64, arm64 | none | 386, arm, ppc64, riscv64 |
| illumos | amd64 | none | none |
| macOS | amd64, arm64 | none | none |
| Windows | amd64 | 386, arm64 | none |
| Solaris | none | none | amd64 |
| DragonFly BSD | none | none | amd64 |
| AIX | none | none | ppc64 |
| Plan 9 | none | none | 386, amd64, arm |
| Android | none | none | arm64 |
The three columns are three different promises:
- Published binaries
- The common set built for distribution. FreeBSD, illumos, Linux, NetBSD and OpenBSD additionally have an operations guide, service definitions and a firewall example of their own. macOS and Windows are published for development and evaluation only.
- Full release matrix
- Every target whose SQLite driver support is known and deliberate, native or fallback. These are extra architectures rather than new operating systems, so the guide for that system still applies to them word for word.
- Also compiles
- The rest of what the Go toolchain can target. The binary links, the pure-Go SQLite fallback comes along, and that is the entire claim. Nobody has installed littleFedi on AIX, watched it federate for a month, and rebooted the machine.
A target in the third column means the code builds for it, and nothing more. Even in the first column, a platform counts as verified only once an operator has recorded a full install, smoke test and reboot against it. If you run one of the unusual ones and it works, that is worth telling me about. If it does not, that is worth telling me about twice.
Monitoring and alerts
Keep /metrics on the monitoring network. The application alone
cannot observe every production dependency, so Profile A combines littleFedi
metrics with reverse-proxy, service-manager, filesystem, TLS and backup-tool
data.
| Signal | Initial condition | Source |
|---|---|---|
| Readiness | /readyz fails twice over 2 minutes | black-box probe |
| HTTP failures | 5xx ratio above 2% for 5 minutes | reverse-proxy metrics |
| Slow requests | p95 above 2 seconds for 10 minutes | littlefedi_request_duration_seconds |
| Queue backlog | depth grows for 15 minutes, or oldest claimed job older than 15 minutes | littlefedi_queue_depth, littlefedi_queue_age_seconds |
| Failed jobs | littlefedi_queue_failed_total increases | littleFedi metrics |
| Federation | failed deliveries above 10% of attempts for 15 minutes | delivery counters |
| Workers | configured is zero, or active is saturated with growing depth | worker-pool gauges |
| Database | connection waits rise, or in-use reaches maximum | database gauges |
| Disk | free space below 20% warning, below 10% critical | system metrics |
| Backup | newest verified off-host backup older than the RPO | backup tool |
| TLS | certificate expires within 21 days | black-box or ACME monitor |
| Media | repeated upload or object errors | structured logs, store probe |
| Mesh directory | /healthz fails twice, or publication failures continue for half the record TTL | probe and logs |
Label alerts with the instance and a runbook URL. Do not put email addresses, tokens, private statuses, job payloads, signatures or signed object URLs into an alert.
Before you go live, break things deliberately: stop the service, pause the workers, exhaust a staging filesystem threshold, age a backup marker, use an expiring test certificate. Confirm each alert reaches a real person. An untested notification route is not evidence, it is an assumption.
Backup, restore, drill
The built-in backup includes a consistent database copy, a configuration copy, a manifest, and optionally owned media. The remote media cache is disposable and is not included.
littlefedi --config <config> backup now --media
littlefedi --config <config> backup restore /path/to/backup
Run both as the service account, never as root. Each platform guide gives the exact privilege-drop command for that system. Copy the finished backup off-host immediately with an established encrypted tool such as restic or borg, driven from a root-owned credential file rather than a password on the command line.
The drill, which is the only thing that turns a backup into evidence:
- Provision a clean isolated host with no route to production peers or SMTP.
- Fetch and decrypt one off-host backup. Verify its checksum and manifest.
- Install the exact release binary and the private production configuration.
- Restore while littleFedi is stopped.
- Start once so migrations apply. Check
/healthand/readyz. - Sign in with a test account and MFA. Fetch timelines, profile images, status media, OAuth application records and queued jobs.
- With controlled network access, federate one test activity to a designated staging peer and confirm a signed response. Then cut the network again.
- Record the elapsed time and the newest restored object timestamp.
The drill fails if you miss the recovery time or recovery point objective, or if actor keys, media, OAuth applications or durable queued work are missing. Repeat it for every release candidate and quarterly after launch.
Runbooks
In every incident: declare an owner, record UTC timestamps, preserve redacted logs, protect user privacy, and announce impact through your status channel. Closing registration first costs nothing and does not take existing accounts offline.
- Failed migration
- Stop every process. Preserve the failed database and the logs. Do not edit schema rows by hand. If the failure is understood and the migration is transactional, retry once with the same binary; otherwise restore the pre-upgrade backup and the previous binary.
- Full disk
- Close registration and uploads at the edge, stop littleFedi before SQLite runs out of space, and find the growth without deleting database or WAL files. Remove only disposable remote media cache, or expired verified backups.
- Stuck or failed queue
- Check readiness, depth and oldest age by kind, peer failure rate, DNS and disk. Do not delete payloads to make a graph green. Fix the dependency and let durable retry recover. Delete a terminal job only after recording why abandoning its effect is safe.
- Compromised token, signing key or MFA key
- Close registration, drain writes, revoke the affected sessions and tokens. A compromised actor private key needs incident-specific federation coordination; do not silently replace database values. Rotate service, SMTP, object store, media proxy and backup credentials.
- Abusive instance or spam burst
- Close registration, apply a domain suspension, preserve reports, notify the moderation contact. Tighten edge rules temporarily. Do not silently auto-ban individual users from an unreviewed heuristic.
- SMTP outage
- Keep registration closed if verified email is mandatory. Existing verified users carry on. Check that the port matches the TLS mode, 587 for STARTTLS and 465 for implicit TLS, and do not bypass certificate verification on a public path to make the symptom go away.
- Media or object store outage
- Disable uploads at the edge, leave existing posts readable, and avoid deleting media rows. Restore the store, check a known object, then let queued export, import and cache jobs resume.
- Database outage
- Remove the instance from traffic. For SQLite check disk, permissions, WAL companions and filesystem health, and never copy a live main file without its proper backup mechanism. For PostgreSQL check pool saturation, server health, credentials and TLS before failing over.
- Emergency read-only
- There is none. Set
auth.registrations = false, restart in a controlled way, and use the edge to reject write paths. Keep health and status pages available. Record every temporary edge rule and remove it deliberately afterwards.
The compatibility ledger
Compatibility is recorded, not asserted. Before a release candidate, the exact versions and UTC dates for current Mastodon, GoToSocial, Akkoma, snac, Tusky and the built-in interface go into a table, along with the result of the federation and OAuth scenarios for each.
| Peer or client | Core federation | OAuth and streaming |
|---|---|---|
| Mastodon | not certified | not certified |
| GoToSocial | not certified | n/a |
| Akkoma | not certified | n/a |
| snac | not certified | n/a |
| Tusky | n/a | not certified |
| Built-in interface | n/a | smoke tests pending |
A pending row is a release blocker, not an implied compatibility claim. The same applies to the littleMesh readable-handle acceptance checks, which must be run through the public gateway rather than a local mock.
Policy and retention
This is an operator checklist, not legal advice, and it needs jurisdiction-specific review before you accept users who are not yourself. Publish: who operates the server and under which jurisdiction; moderation, privacy, security and legal contact addresses; the rules; terms of service; and a privacy policy that is honest about federation.
That last one matters more here than on an ordinary web service. Profiles and posts are copied to independent servers you do not control, remote content is cached and proxied, deletions propagate as tombstones, and backups age out on their own schedule. Say so.
| Data | Starting retention |
|---|---|
| Access logs | 14 days |
| Failed or terminal jobs | 24 hours after the terminal state |
| Moderation and audit records | 2 years |
| Encrypted off-host backups | 7 daily and 4 monthly copies |
| User export archives | 24 hours, one successful download |
| Uploaded import archives | deleted after a successful import |
| Deleted-account tombstones | indefinite, minimal URI, type and date only |
| Deleted-status tombstones | 90 days |
| Remote statuses and media cache | 30 days unless still referenced |