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/ and backups/.
  • 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 plain KEY=value lines. 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

  1. Keep registrations = false, require_admin_mfa = false and require_email = true for the bootstrap.
  2. Create the administrator with an explicitly verified email using the local CLI. Sign in, enroll TOTP, and store the recovery codes offline.
  3. Set require_admin_mfa = true, restart, and confirm that web, admin, API, OAuth and streaming access all still work.
  4. On an upgraded instance, run admin email-verification-report before turning verified email on.
  5. 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_uri is the exact public HTTPS origin. trusted_proxies is 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.

Rollback means restore, not downgrade

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
Not certified yet

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.

What each supported system uses
  Alpine FreeBSD illumos Linux NetBSD OpenBSD
PackagesapkpkgIPS / pkgindistributionpkginpkg_add
Service managerOpenRCrc.dSMFsystemdrc.drc.d, rcctl
Service accountlittlefedilittlefedilittlefedilittlefedilittlefedi_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
Firewallnftablespfipfilternftables, ufwnpfpf
ACMEcertbot, Caddyacme.sh, certbot, Caddyacme.sh, certbot, Caddycertbot, Caddyacme-client, certbot, Caddyacme-client(1)
Logssupervise-daemonsyslog, newsyslogSMF log, logadmjournaldsyslog, newsyslogsyslog, newsyslog
Descriptor limitrc_ulimitlogin classprojectLimitNOFILElogin classlogin class
Restart on failureyesdaemon(8) -RSMFsystemdnono

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.

A guide is not a drill

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.

SQLite drivers by platform
DriverPlatformsBehaviour
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.

Architectures by operating system, and how far each one has been taken
Operating system Published binaries
make release
Full release matrix
make release-all
Also compiles
make allreleases
FreeBSDamd64, arm64386, armnone
Linuxamd64, arm64, arm386, loong64, ppc64le, riscv64, s390xmips, mipsle, mips64, mips64le, ppc64
NetBSDamd64, armarm64386
OpenBSDamd64, arm64none386, arm, ppc64, riscv64
illumosamd64nonenone
macOSamd64, arm64nonenone
Windowsamd64386, arm64none
Solarisnonenoneamd64
DragonFly BSDnonenoneamd64
AIXnonenoneppc64
Plan 9nonenone386, amd64, arm
Androidnonenonearm64

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.
Compiling is not testing

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.

Start with these conditions and tune after the staging soak
SignalInitial conditionSource
Readiness/readyz fails twice over 2 minutesblack-box probe
HTTP failures5xx ratio above 2% for 5 minutesreverse-proxy metrics
Slow requestsp95 above 2 seconds for 10 minuteslittlefedi_request_duration_seconds
Queue backlogdepth grows for 15 minutes, or oldest claimed job older than 15 minuteslittlefedi_queue_depth, littlefedi_queue_age_seconds
Failed jobslittlefedi_queue_failed_total increaseslittleFedi metrics
Federationfailed deliveries above 10% of attempts for 15 minutesdelivery counters
Workersconfigured is zero, or active is saturated with growing depthworker-pool gauges
Databaseconnection waits rise, or in-use reaches maximumdatabase gauges
Diskfree space below 20% warning, below 10% criticalsystem metrics
Backupnewest verified off-host backup older than the RPObackup tool
TLScertificate expires within 21 daysblack-box or ACME monitor
Mediarepeated upload or object errorsstructured logs, store probe
Mesh directory/healthz fails twice, or publication failures continue for half the record TTLprobe 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:

  1. Provision a clean isolated host with no route to production peers or SMTP.
  2. Fetch and decrypt one off-host backup. Verify its checksum and manifest.
  3. Install the exact release binary and the private production configuration.
  4. Restore while littleFedi is stopped.
  5. Start once so migrations apply. Check /health and /readyz.
  6. Sign in with a test account and MFA. Fetch timelines, profile images, status media, OAuth application records and queued jobs.
  7. With controlled network access, federate one test activity to a designated staging peer and confirm a signed response. Then cut the network again.
  8. 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.

Current state of the ledger
Peer or clientCore federationOAuth and streaming
Mastodonnot certifiednot certified
GoToSocialnot certifiedn/a
Akkomanot certifiedn/a
snacnot certifiedn/a
Tuskyn/anot certified
Built-in interfacen/asmoke 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.

Suggested starting retention, to adjust only with a recorded reason
DataStarting retention
Access logs14 days
Failed or terminal jobs24 hours after the terminal state
Moderation and audit records2 years
Encrypted off-host backups7 daily and 4 monthly copies
User export archives24 hours, one successful download
Uploaded import archivesdeleted after a successful import
Deleted-account tombstonesindefinite, minimal URI, type and date only
Deleted-status tombstones90 days
Remote statuses and media cache30 days unless still referenced