The short version
docker compose pull and docker compose up -d update the instance. Every restart forgets the
room codes people are waiting on, so pick a quiet moment. Point a monitor at /health, and expect
the server’s log to be nearly empty.
Common commands
Terminal
restart: unless-stopped. The server, the client, and
coturn come back by themselves after a crash, and when the host starts again after a reboot. They
stay down only after docker compose stop or docker compose down. Docker does not restart a
server that is only unhealthy.
What a restart does to each person using the instance:
A web client older than 1.11.0 is the exception to the third row: its transfer stops when the
server tells the page that the other side has left, as a shutdown can.
The client also waits on the server: Compose starts it only once the server reports healthy. If
the server never gets there, you do not get a web app that cannot connect. You get no web app at
all.
Update to the latest code
Terminal
latest unless FLOE_IMAGE_TAG in .env names another, and restarts on it. See Container images for the tags. Nothing is built locally, so there is no repository to keep in sync. If you pinned a version, change FLOE_IMAGE_TAG first, or the pull has nothing new to fetch. An update is a restart, with the cost described above.
Pulling never changes your docker-compose.yml, though, and a setting that a release adds reaches the containers only through that file. A copy from 1.11.0 or earlier passes neither TURN_TLS nor REQUEST_REPORT_EMAIL, both new in 1.11.1, and a copy from 1.11.1 or earlier does not pass MAX_SOCKETS_PER_IP, which is newer, so setting any of them in .env does nothing. To use one, download the current file from GitHub and copy your own changes across.
If you run the optional coturn relay, add its profile to both commands or Compose skips the service entirely, so a stopped relay is never restarted:
Terminal
COMPOSE_PROFILES=turn in your .env applies it to every later docker compose command. The compose file pins coturn, Floe’s optional TURN server, to coturn/coturn:4.15, so the pull refreshes that version only; see TURN relay.
Changing NEXT_PUBLIC_SOCKET_URL needs only docker compose up -d, because the client reads it at runtime. See How the client finds the server. NEXT_PUBLIC_SITE_URL is the exception: it is written into the page at build time, so the published image cannot carry it. That image leaves your domain out of share previews and the sitemap rather than pointing them at somebody else’s. See Container images for what the image emits and how to build your own.
Moving to 1.11.0
Version 1.11.0 changed what a self-hosted instance needs. Go through these five points once, when you move up from an older version:- Pass the
Hostheader through your proxy. This matters only when the CLI or Floe Desktop reaches the server through a reverse proxy on a hostname that is not exactly yourCLIENT_URL, as on the two-hostname layout. From 1.11.0 the server checks the origin of every WebSocket, and it lets those two clients in by comparing theirOriginwith the hostname inHost. A proxy that rewritesHostto its upstream address gets them refused with a403. Browsers on yourCLIENT_URL, a one-domain deployment, and a server reached without a proxy are not affected. - Take the current
docker-compose.yml. Pulling images never changes your copy. One from before v1.11.0 passes neitherPOLICY_FILEnorMAX_FAILED_CODE_RESOLVESto the server, so setting them in.envdoes nothing. Download the one on GitHub and copy your own changes across. You need it before you turn on request links, which stay off until you do. - Keep the client and the server on one version. The page a request-link sender opens is part
of the web client, so a 1.11.0 server behind an older client hands out links to a page that does
not exist. Compose picks both images with
FLOE_IMAGE_TAG; if you pinned1.10.11, move it to1.12.0or the newest tag in Container images. See Keep the client and server on one version. - Know the new failed-lookup setting. Each address gets 10 failed room-code lookups in 60
seconds, and the new optional
MAX_FAILED_CODE_RESOLVESchanges that number. A lookup that finds a code costs nothing, but once an address has spent its 10, every lookup from it answers429until the misses age out. Raise it only if many people behind one address mistype codes. See Configuration. - Expect IPv6 clients to share limits by /64. Every per-address limit now counts an IPv6 client
by its /64 instead of its single address, so everyone inside one /64 shares one budget, as
everyone behind one IPv4 address always has. If a whole office or campus sits in one /64, raise
MAX_CONNECTIONS_PER_IPand the other limits to fit.
What an older web app shows
The web app at floe.one always runs the newest code, and it is the one the rest of these docs describe. A web app you host changes only when you pull a newer image. Until then its pages show the wording of the version they run, and this table lists each place where that differs.
On version 1.11.0, where the “Too many refreshes. Reconnecting” banner stays after a retry has
worked, the badge at the top right turning green is the sign that the page is connected again, and
the X closes the banner. For a relayed transfer over 2 GB, the sending page’s own notice now
says to reload the page.
To see the newer wording, update the client image to the newest version: see
Update to the latest code.
The server logs very little
Worth knowing before you go looking. The signaling server has no access log, no request log, and no startup banner, not even a line saying which port it bound.docker compose logs server on a healthy instance is usually empty, or holds the one line
request links: on, and that is expected rather than a symptom. A server run with Node directly
and no TRUSTED_PROXY_COUNT writes one line about that at startup, and so does a server given a
word or a negative number for it; see the table. So does a server given half of a pair of variables,
or a TURN_DOMAIN that holds more than a hostname.
It never logs an accepted connection, a room, a code, a rate-limit refusal, or a request-link event,
and no line is built from a client’s address. These are all the lines it can write:
The
Refused a request with line carries the status and nothing else, never the origin, the path,
or any of the request’s text. Besides 403, it shows 400 for malformed JSON and 413 for a body
over the size limit. A server image of version 1.11.0 or earlier never writes the MAX_* and
TRUSTED_PROXY_COUNT lines, and one of version 1.11.3 or earlier never writes the
TRUSTED_PROXY_COUNT line about a value that is not a whole number. One of version 1.11.7 or earlier
never writes the Only one of, TURN_DOMAIN holds or Upstash counter lines, and one of version
1.10.10 to 1.11.6 given a PORT that Node refuses logs
Unhandled error (uncaughtException), server still serving: instead of HTTP server error:, then
exits with code 0.
To confirm the server is alive, or that a restart actually landed, use the health endpoint below
rather than the logs.
The client container writes Next.js’s own lines when it starts. Once it is listening it also warms
its picture cache, and writes one line only if that does not finish:
Floe: the image warm-up did not finish; pictures are resized on first request instead. The web
app works either way.
When Docker stops the server, the server closes every connection and exits within 10 seconds, and
logs nothing on the way out. In detail: on SIGTERM or SIGINT it closes every /ws connection
with code 1001 and the reason Server shutting down, closes its Socket.IO connections, and stops
accepting new ones. If something is still holding it open 10 seconds later, it exits anyway.
Health endpoints
The signaling server exposes two status endpoints:features is ["request-1"] while request links are on and [] while they are off. See
HTTP API.
Point a monitor at /health. It is the only one of the two that survives a
one-domain deployment, where / belongs to the
web client and returns a page rather than JSON. GET / on the signaling server
is reachable only when you address the server directly, on :3001 or on its own
subdomain.
Both endpoints pass the same origin check as every other HTTP route. A monitor that sends no
Origin header, as curl and most uptime services do, is answered. One that runs inside a web
page on another site sends that site’s origin, and gets 403 with {"error":"Bad request"} while
the server is healthy.
The server image runs the same check on itself every 30 seconds, which is the health state
docker compose ps shows. Each check gets 3 seconds to answer, and 3 failed checks in a row mark
the server unhealthy.
The client image checks itself the same way, every 30 seconds, by asking its own /api/config, so
docker compose ps shows a health state for the client too. A client image of version 1.11.4 or
earlier has no such check and shows none.