mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
docs: move the reference site to astro starlight (#2527)
This commit is contained in:
1 parent
7f8f09ee51
commit
5367c0ab42
198 files changed
+59927
-1216
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,281 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Get started
|
||||
description: Bring up a Fluxer instance with Docker Compose, from a fresh host to first login.
|
||||
---
|
||||
|
||||
import InstallerChecksum from '@/components/InstallerChecksum.astro';
|
||||
|
||||
:::tip[Plutonium and donations fund Fluxer]
|
||||
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). The Operator Pass is coming, and adds a direct line to the team for help and feedback.
|
||||
:::
|
||||
|
||||
By the end you have a Fluxer instance on a hostname you own, with the web app, HTTP API, Gateway, admin dashboard, uploads, search and voice signalling behind it. All of it runs in Docker containers on one machine. Allow about 30 minutes, most of it spent downloading images.
|
||||
|
||||
## What you need
|
||||
|
||||
| Requirement | Value |
|
||||
| --- | --- |
|
||||
| Host | Any machine that runs Docker with Linux containers. A Linux server, or a Mac or Windows PC with Docker Desktop |
|
||||
| Runtime | Docker Engine 24 or newer with the Compose plugin v2.20.2 or newer. Docker Desktop ships both |
|
||||
| Processor | Intel, AMD and Apple Silicon all work. Every image ships for `amd64` and `arm64`, and Docker pulls the matching one |
|
||||
| CPU | 2 vCPU minimum, 4 vCPU for a small active community. No service sets a CPU limit, so every container sees all of them |
|
||||
| RAM | 8 GB minimum with the shipped defaults, 16 GB for a small active community. 4 GB needs the [low-memory overrides](/operator/configuration/#resources) |
|
||||
| Disk | 20 GB minimum, more for uploads |
|
||||
| Inbound ports | 80/tcp, 443/tcp and 443/udp for the web tier, plus 7881/tcp and 7882/udp for voice media |
|
||||
| DNS | An `A` record for the hostname pointing at this machine, optionally an `AAAA` record |
|
||||
| Outbound | Working DNS resolution from inside the containers |
|
||||
| Host tools | `curl` and `openssl` on Linux. macOS ships both. The Windows installer needs neither |
|
||||
|
||||
The per-container memory limits are ceilings and reserve nothing, so the host does not need their total free. On a 4 GB host, lower the `api` and `worker` limits first, because those two size their JavaScript heap from the limit they are given.
|
||||
|
||||
Docker Desktop runs the containers inside a Linux virtual machine, so the RAM figure above is the RAM its settings give that machine, which starts well below what the hardware has.
|
||||
|
||||
The stack is one Compose project. The edge and LiveKit are the only services that publish ports.
|
||||
|
||||
The steps below use the bundled Caddy, which obtains and renews a TLS certificate for the hostname and needs no configuration.
|
||||
|
||||
To put your own proxy in front, add the `docker-compose.proxy.yml` overlay. The stack then leaves 80 and 443 alone and publishes one plain HTTP port, and your proxy forwards everything to that port. [Behind your own reverse proxy](/operator/reverse-proxy/) has the overlay, the requirements, and worked configuration for nginx, Caddy, Traefik, HAProxy, Apache, a Cloudflare Tunnel and Nginx Proxy Manager.
|
||||
|
||||
## Step 1: Install Docker
|
||||
|
||||
Check what you have:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
On Debian and Ubuntu, use Docker's apt repository from [Install Docker Engine](https://docs.docker.com/engine/install/). The distribution's `docker.io` package ships no Compose plugin. Add your user to the `docker` group with the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/), then log out and back in so the commands below run without `sudo`.
|
||||
|
||||
On a Mac or a Windows PC, install [Docker Desktop](https://docs.docker.com/desktop/) and leave it on Linux containers. It has both commands above.
|
||||
|
||||
On Linux, install the two host tools the steps below use. On Debian and Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y curl openssl
|
||||
```
|
||||
|
||||
Other distributions use `dnf`, `zypper`, `pacman` or `apk` with the same two package names. `curl --version` and `openssl version` both answer once the install finishes.
|
||||
|
||||
## Step 2: Point DNS at the host
|
||||
|
||||
Choose the hostname the instance answers on. Step 4 writes it into `.env` as `FLUXER_DOMAIN`, and every example below uses `chat.example.com` in its place.
|
||||
|
||||
Create an `A` record for that hostname pointing at the machine's public address, and an `AAAA` record when the machine has a public IPv6 address. A hosting provider shows that address in its console. On a home connection the address belongs to the router, and the router's status page shows it.
|
||||
|
||||
:::caution[Finish setup before anyone else reaches the hostname]
|
||||
An unconfigured instance takes registrations from anyone, and lets any account that registers read and change the instance configuration. Registration defaults to `open`. The window opens the moment DNS and the ports are live, so finish the wizard in [Step 6](#first-run) in the same sitting that brings the stack up.
|
||||
:::
|
||||
|
||||
## Step 3: Open the ports
|
||||
|
||||
Open six ports on the host:
|
||||
|
||||
| Port | Protocol | Used by |
|
||||
| --- | --- | --- |
|
||||
| 22 | tcp | SSH, on a host you reach remotely |
|
||||
| 80 | tcp | ACME HTTP validation and the redirect to HTTPS |
|
||||
| 443 | tcp | The web app, HTTP API, Gateway and admin dashboard |
|
||||
| 443 | udp | HTTP/3 for the same traffic |
|
||||
| 7881 | tcp | LiveKit media, published by the LiveKit container |
|
||||
| 7882 | udp | LiveKit media, published by the LiveKit container |
|
||||
|
||||
A machine behind a home router needs ports 80, 443 and the two LiveKit ports forwarded to it, because the certificate is issued from the public internet and voice media arrives on the LiveKit ports directly.
|
||||
|
||||
Behind your own reverse proxy the proxy holds 80 and 443 and the stack binds a plain HTTP port on the loopback. 7881 and 7882 still have to reach the host, because voice media never goes through a proxy.
|
||||
|
||||
On a cloud VM, open them in the provider's firewall or security group. On a Linux host running firewalld:
|
||||
|
||||
```bash
|
||||
sudo firewall-cmd --permanent --add-service=ssh
|
||||
sudo firewall-cmd --permanent --add-port=80/tcp --add-port=443/tcp --add-port=443/udp \
|
||||
--add-port=7881/tcp --add-port=7882/udp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Step 4: Bring up the instance
|
||||
|
||||
The installer downloads the stack files, writes `.env`, generates every secret, and starts the instance. Download it, check the published SHA-256, read it, then run it.
|
||||
|
||||
On Linux:
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://fluxer.dev/install.sh
|
||||
curl -fsSLO https://fluxer.dev/install.sh.sha256
|
||||
sha256sum -c install.sh.sha256
|
||||
```
|
||||
|
||||
On macOS, the same two downloads and:
|
||||
|
||||
```bash
|
||||
shasum -a 256 -c install.sh.sha256
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest https://fluxer.dev/install.ps1 -OutFile install.ps1
|
||||
Invoke-WebRequest https://fluxer.dev/install.ps1.sha256 -OutFile install.ps1.sha256
|
||||
(Get-FileHash .\install.ps1 -Algorithm SHA256).Hash -eq (Get-Content .\install.ps1.sha256).Split()[0]
|
||||
```
|
||||
|
||||
`install.sh: OK` on Linux and macOS, and `True` on Windows, mean the file arrived whole. The digest of the copy this site serves is <InstallerChecksum script="install.sh" /> for `install.sh` and <InstallerChecksum script="install.ps1" /> for `install.ps1`.
|
||||
|
||||
Read the script before you run it. It writes `.env`, which holds every secret the instance has.
|
||||
|
||||
The run below uses the bundled TLS. Add `--tls proxy` when something else terminates TLS, and read [Behind your own reverse proxy](/operator/reverse-proxy/) first.
|
||||
|
||||
Then run it. The hostname is the one from Step 2, and `--email` becomes the contact address on outgoing web push:
|
||||
|
||||
```bash
|
||||
sh install.sh --domain chat.example.com --email [email protected]
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Domain chat.example.com -Email [email protected]
|
||||
```
|
||||
|
||||
The run prints one line per phase and ends with the URL to open. It takes several minutes, most of it spent pulling eighteen images.
|
||||
|
||||
`~/fluxer` then holds `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml`, `Caddyfile` and `.env.example`, plus a `.env` of 20 lines readable only by you. Every value that ships as `CHANGE_ME` in `.env.example` is a fresh random value. Every command from here on runs in that directory.
|
||||
|
||||
### Installer flags
|
||||
|
||||
| Flag | Meaning |
|
||||
| --- | --- |
|
||||
| `--domain <host>` | Hostname the instance answers on. Prompted when absent |
|
||||
| `--email <address>` | Contact address written as `FLUXER_VAPID_EMAIL`. Prompted when absent |
|
||||
| `--dir <path>` | Working directory. Default `~/fluxer` |
|
||||
| `--ref <git ref>` | Ref the stack files come from. Defaults to the image tag, and to `main` when that tag is `v1` or `latest` |
|
||||
| `--image-tag <tag>` | Image tag the stack runs. Default `v1` |
|
||||
| `--tls <mode>` | `bundled` or `proxy`. Default `bundled` |
|
||||
| `--edge-bind <addr:port>` | Plain HTTP bind under `--tls proxy`. Default `127.0.0.1:8080` |
|
||||
| `--non-interactive` | Never prompt. A missing required value is an error |
|
||||
| `--dry-run` | Print the plan. Write nothing and start nothing |
|
||||
| `--no-start` | Write everything and skip `docker compose up -d` |
|
||||
| `--update` | Upgrade: record, back up, refresh, pull, recreate, verify |
|
||||
| `--rollback` | Put back the images and stack files of the newest record |
|
||||
| `--allow-root` | Permit running as root |
|
||||
|
||||
Run `--dry-run` first to see what a set of flags does. The PowerShell script takes the same flags as `-Domain`, `-Email` and so on, and has no `-AllowRoot`. [Upgrading](/operator/upgrading/#run-the-upgrade) has the flags that only `--update` and `--rollback` read, and the exit codes.
|
||||
|
||||
### The script is the reference
|
||||
|
||||
Each step in the script has a comment beside the code. The comment holds the command that does that step alone, and the reason the step exists: the download URL for every stack file, how `.env` is built from `.env.example`, what each `CHANGE_ME` takes, and how the VAPID pair is derived. Read it at [https://fluxer.dev/install.sh](https://fluxer.dev/install.sh), or the Windows script at [https://fluxer.dev/install.ps1](https://fluxer.dev/install.ps1).
|
||||
|
||||
## Step 5: Check that it works
|
||||
|
||||
Watch the stack settle:
|
||||
|
||||
```bash
|
||||
cd ~/fluxer
|
||||
docker compose ps
|
||||
docker compose logs -f api
|
||||
```
|
||||
|
||||
Every service reads `running` or `healthy` except `seaweedfs-init`, which creates the five upload buckets and then reads `exited (0)`.
|
||||
|
||||
Then the public probes. Replace the hostname with your own:
|
||||
|
||||
```bash
|
||||
base=https://chat.example.com
|
||||
for p in /_health /api/_health /gateway/_health /media/_health /.well-known/fluxer /; do
|
||||
printf '%s %s\n' "$(curl -sS -o /dev/null -w '%{http_code}' "$base$p")" "$p"
|
||||
done
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
$base = 'https://chat.example.com'
|
||||
foreach ($p in '/_health', '/api/_health', '/gateway/_health', '/media/_health', '/.well-known/fluxer', '/') {
|
||||
'{0} {1}' -f (Invoke-WebRequest "$base$p" -UseBasicParsing).StatusCode, $p
|
||||
}
|
||||
```
|
||||
|
||||
All six return 200. `/.well-known/fluxer` lists the endpoints clients use, with your own hostname in them.
|
||||
|
||||
<a id="first-run"></a>
|
||||
|
||||
## Step 6: Create the first admin
|
||||
|
||||
Open the hostname in a browser. A fresh instance is unconfigured, so it serves the setup wizard.
|
||||
|
||||
The wizard runs in two halves. Before you are signed in it walks a welcome step, a theme choice, an admin introduction, and an account form. After that account exists it walks branding, registration mode, community policy and media expiry, then the GIF, YouTube, captcha, email and Bluesky integrations, then the service toggles, the premium model and a finish step. Finishing marks the instance configured and takes the wizard down.
|
||||
|
||||
Create the owner account with an email address at a domain you control. On a self-hosted instance the first registration that supplies an email address receives the wildcard admin ACL, which is full access to the admin dashboard, unless registration is set to hold new accounts for approval. Finishing the wizard also grants the wildcard ACL to the account that completes it when that account holds no admin ACL yet.
|
||||
|
||||
`.env.example` ships `FLUXER_EMAIL_ENABLED=false`, which marks every address verified at creation and sends no mail at all. Nothing goes out by mail, so a forgotten owner password has no email reset. Record it, and register a passkey or a second admin account before you open registration.
|
||||
|
||||
Then sign in to the admin dashboard at `https://chat.example.com/admin` with the account holding the wildcard ACL. The **Instance Config** page has everything the wizard asked, plus registration mode, approvals and integration keys. **Limit Config** holds the instance limits published to clients. **Voice Regions** and **Voice Servers** come seeded, so voice needs no setup there.
|
||||
|
||||
The desktop client opens the hosted web app for its release channel, so reach your instance through the web app. [Issue #1088](https://github.com/fluxerapp/fluxer/issues/1088) tracks progress.
|
||||
|
||||
[Configuration](/operator/configuration/) lists every runtime setting and the environment variable each one overrides. [Deployment availability](/http-api/deployment-availability/) lists the routes that exist only on the hosted deployment.
|
||||
|
||||
## Backups
|
||||
|
||||
An instance comes back from two artifacts and its `.env`: a dump of the database, which covers `postgres-data`, and a tarball of `seaweedfs-data`, which holds every upload, avatar, report and harvest. [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists all seven volumes with what each one holds.
|
||||
|
||||
The dump costs no downtime, so run it on a schedule while the stack serves:
|
||||
|
||||
```bash
|
||||
cd ~/fluxer
|
||||
mkdir -p backups
|
||||
docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom \
|
||||
> "backups/fluxer-$(date -u +%Y%m%dT%H%M%SZ).dump"
|
||||
```
|
||||
|
||||
PowerShell writes UTF-16 through `>`, which corrupts a binary dump. On Windows, write the dump inside the container and copy it out:
|
||||
|
||||
```powershell
|
||||
mkdir -Force backups
|
||||
docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom -f /tmp/fluxer.dump
|
||||
docker compose cp postgres:/tmp/fluxer.dump backups/fluxer.dump
|
||||
docker compose exec -T postgres rm /tmp/fluxer.dump
|
||||
```
|
||||
|
||||
The uploads copy stops the stack, because a live copy can catch a file mid-write. Compose prefixes volume names with the project name, which the stack sets to `fluxer`:
|
||||
|
||||
```bash
|
||||
docker compose stop
|
||||
docker run --rm -v fluxer_seaweedfs-data:/data -v "$PWD/backups:/backup" alpine:3.22 \
|
||||
tar czf /backup/seaweedfs-data.tgz -C /data .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
In PowerShell write `${PWD}` in place of `$PWD`.
|
||||
|
||||
A dump and a tarball in `backups`, and every service back to `running`, is the success signal.
|
||||
|
||||
`sh install.sh --update` takes both before every upgrade. [What the backup covers](/operator/upgrading/#what-the-backup-covers) puts the dump on a nightly timer, and [Restore a backup](/operator/upgrading/#restore-a-backup) puts either artifact back.
|
||||
|
||||
### Remove the instance
|
||||
|
||||
Take the instance down and delete its data:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
`-v` deletes all seven named volumes, so every account, message, upload, search index and certificate the instance holds is gone, and no `docker compose up -d` brings them back. Take the copies above first.
|
||||
|
||||
## Upgrading
|
||||
|
||||
The default tag `v1` tracks the latest compatible release. The same script upgrades the instance:
|
||||
|
||||
```bash
|
||||
cd ~/fluxer
|
||||
sh install.sh --update
|
||||
```
|
||||
|
||||
`--update` records the running images, backs up the database and the uploads, refreshes the four stack files, pulls, recreates, and verifies. It leaves every secret in `.env` untouched. On Windows it is `.\install.ps1 -Update`. Run `sh install.sh --update --dry-run` first to see the plan.
|
||||
|
||||
[Upgrading](/operator/upgrading/) covers what the backup holds, rolling back, pinning a release and reclaiming disk.
|
||||
|
||||
## Getting help
|
||||
|
||||
File issues and follow development on [GitHub](https://github.com/fluxerapp/fluxer).
|
||||
@@ -0,0 +1,490 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Behind your own reverse proxy
|
||||
description: Serving a Fluxer instance from nginx, Caddy, Traefik, HAProxy, Apache, or a tunnel.
|
||||
---
|
||||
|
||||
:::tip[Plutonium and donations fund Fluxer]
|
||||
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). The Operator Pass is coming, and adds a direct line to the team for help and feedback.
|
||||
:::
|
||||
|
||||
A reverse proxy takes the HTTPS connection from a browser and passes each request on to Fluxer. Fluxer runs behind any proxy that terminates TLS and forwards WebSocket upgrades.
|
||||
|
||||
The stack ships Caddy and runs it by default on `80` and `443`. One extra Compose file stops that and publishes a single plain HTTP port instead. That port already routes every path to the right internal service, so the proxy in front needs one routing rule: send everything to it.
|
||||
|
||||
## Enable the overlay
|
||||
|
||||
`docker-compose.proxy.yml` ships next to `docker-compose.yml` and layers on top of it. Pass both files on every command:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
|
||||
```
|
||||
|
||||
Or set `COMPOSE_FILE` in `.env` once and keep typing plain `docker compose` commands:
|
||||
|
||||
```ini
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml
|
||||
```
|
||||
|
||||
On a first install, `sh install.sh --tls proxy` writes that line and `FLUXER_EDGE_BIND` into `.env` for you, so there is nothing to add by hand. [Installer flags](/operator/get-started/#installer-flags) lists it with the rest.
|
||||
|
||||
The port binds `127.0.0.1:8080` by default. `FLUXER_EDGE_BIND` sets both the interface and the host port. Move the port when something else on the host already holds `8080`:
|
||||
|
||||
```ini
|
||||
FLUXER_EDGE_BIND=127.0.0.1:8081
|
||||
```
|
||||
|
||||
The container side stays `8080` whatever you set. A proxy on the host then points at `127.0.0.1:8081`, and a proxy on the stack's own Docker network points at `edge:8080`. Bind a routable address only when the proxy runs on another machine, and firewall the port to that machine:
|
||||
|
||||
```ini
|
||||
FLUXER_EDGE_BIND=0.0.0.0:8080
|
||||
```
|
||||
|
||||
Confirm the port answers before you configure anything in front of it:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1:8080/_health
|
||||
```
|
||||
|
||||
`/_health` returns `200 OK` from the edge itself without reaching an upstream, so it is the health check to give your proxy.
|
||||
|
||||
:::note[There is nothing to configure inside the instance]
|
||||
The routing lives in a config file inside the stack. Point your proxy at the port and leave that file alone.
|
||||
:::
|
||||
|
||||
## What every proxy must do
|
||||
|
||||
#### Terminate TLS and serve the hostname over HTTPS
|
||||
|
||||
With `FLUXER_PUBLIC_SCHEME=https` the admin CSRF cookie is `__Host-csrf_token`, which browsers accept only over HTTPS.
|
||||
|
||||
#### Forward WebSocket upgrades
|
||||
|
||||
The client connection is on `/gateway` and voice signalling is on `/livekit/*`. Both open as WebSocket upgrades. Check that the handshake completes through whatever is in front of the instance, with your own hostname in place of the one below:
|
||||
|
||||
```bash
|
||||
curl -sS -i --http1.1 --max-time 5 \
|
||||
-H 'Connection: Upgrade' \
|
||||
-H 'Upgrade: websocket' \
|
||||
-H 'Sec-WebSocket-Version: 13' \
|
||||
-H 'Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==' \
|
||||
'https://chat.example.com/gateway?v=1&encoding=json' | head -1
|
||||
```
|
||||
|
||||
The answer is `HTTP/1.1 101 Switching Protocols`. Confirm a real client connects as well, which covers the whole path including the query string.
|
||||
|
||||
#### Replace `X-Forwarded-For` with the real client address
|
||||
|
||||
Set the header from the connection address and discard whatever the visitor sent. Rate limits, IP bans and abuse detection all apply to the address that arrives in it.
|
||||
|
||||
#### Accept a request body above the attachment limit
|
||||
|
||||
Uploads pass through the proxy. Raise any default body limit, such as nginx's 1 MB, above the instance attachment limit.
|
||||
|
||||
#### Hold a connection open for an hour
|
||||
|
||||
The Gateway socket stays open for the life of a client session, so give idle and read timeouts an hour.
|
||||
|
||||
#### Pass `Sec-Fetch-Site` through untouched
|
||||
|
||||
The browser sends it, and the admin dashboard refuses a mutating request with a cross-site value.
|
||||
|
||||
#### Send no `Content-Security-Policy` of its own
|
||||
|
||||
The instance sends its own policy, and its nonce is what lets the web app boot. Browsers enforce every policy they receive.
|
||||
|
||||
Every worked configuration below answers all seven.
|
||||
|
||||
## The public address
|
||||
|
||||
Three variables tell the instance the address browsers use. Fluxer reads neither `X-Forwarded-Proto` nor `X-Forwarded-Host`, so you keep these correct by hand:
|
||||
|
||||
```ini
|
||||
FLUXER_DOMAIN=chat.example.com
|
||||
FLUXER_PUBLIC_SCHEME=https
|
||||
FLUXER_PUBLIC_PORT=443
|
||||
```
|
||||
|
||||
They stay `https` on `443` even though the instance itself speaks plain HTTP on `8080`. Clients read every base URL from the discovery document the API builds out of these values.
|
||||
|
||||
Serving on a port other than `443` also needs `FLUXER_PUBLIC_ORIGIN`. Several endpoints are built from the scheme and the domain alone, and the port is lost without it:
|
||||
|
||||
```ini
|
||||
FLUXER_PUBLIC_ORIGIN=https://chat.example.com:8443
|
||||
```
|
||||
|
||||
## nginx
|
||||
|
||||
The `map` block makes `Connection` follow `Upgrade`. The snippet raises the body limit and the read timeout above nginx's own 1 MB and 60 seconds.
|
||||
|
||||
```nginx
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name chat.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name chat.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 512m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `X-Forwarded-For` from `$remote_addr`. `$proxy_add_x_forwarded_for` appends whatever the client sent, which puts an attacker-chosen address in front of the real one.
|
||||
|
||||
## Caddy
|
||||
|
||||
An external Caddy needs one site block. It forwards WebSocket upgrades natively, requests the certificate itself, and applies no request body limit and no read timeout of its own.
|
||||
|
||||
```caddy
|
||||
chat.example.com {
|
||||
reverse_proxy 127.0.0.1:8080 {
|
||||
header_up X-Forwarded-For {client_ip}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`{client_ip}` resolves to the peer address unless the peer is in this Caddy's own `trusted_proxies`. The `header_up` line replaces `X-Forwarded-For` with that one address.
|
||||
|
||||
## Traefik
|
||||
|
||||
Traefik forwards WebSocket upgrades natively, and by default it strips inbound `X-Forwarded-*` headers from untrusted clients and writes its own. Leave `forwardedHeaders` alone unless another proxy sits in front of Traefik.
|
||||
|
||||
### File provider
|
||||
|
||||
Static configuration:
|
||||
|
||||
```yaml
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: ":443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: 0s
|
||||
idleTimeout: 3600s
|
||||
|
||||
providers:
|
||||
file:
|
||||
filename: /etc/traefik/dynamic.yml
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: [email protected]
|
||||
storage: /etc/traefik/acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
```
|
||||
|
||||
`readTimeout` defaults to 60 seconds and covers reading the whole request including the body, so `0s` removes the bound and lets a large upload finish. `idleTimeout` defaults to 180 seconds, so raise it to an hour to hold the Gateway socket open.
|
||||
|
||||
Dynamic configuration in `/etc/traefik/dynamic.yml`:
|
||||
|
||||
```yaml
|
||||
http:
|
||||
routers:
|
||||
fluxer:
|
||||
rule: Host(`chat.example.com`)
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: fluxer
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
fluxer:
|
||||
loadBalancer:
|
||||
passHostHeader: true
|
||||
servers:
|
||||
- url: http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
### Docker labels
|
||||
|
||||
Traefik reaches the container over a shared Docker network, so the published port is not used. Keep the overlay applied anyway, or the instance takes `80` and `443` away from Traefik.
|
||||
|
||||
Put this in `traefik.compose.yml` next to the stack:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
edge:
|
||||
networks:
|
||||
- fluxer
|
||||
- traefik
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: traefik
|
||||
traefik.http.routers.fluxer.rule: Host(`chat.example.com`)
|
||||
traefik.http.routers.fluxer.entrypoints: websecure
|
||||
traefik.http.routers.fluxer.tls.certresolver: letsencrypt
|
||||
traefik.http.services.fluxer.loadbalancer.server.port: "8080"
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
Then list all three files so every command picks them up:
|
||||
|
||||
```ini
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:traefik.compose.yml
|
||||
```
|
||||
|
||||
## HAProxy
|
||||
|
||||
One frontend, one backend, and four timeouts:
|
||||
|
||||
```haproxy
|
||||
defaults
|
||||
mode http
|
||||
log stdout format raw local0
|
||||
option httplog
|
||||
timeout connect 5s
|
||||
timeout client 1h
|
||||
timeout server 1h
|
||||
timeout tunnel 1h
|
||||
|
||||
frontend fluxer
|
||||
bind :80
|
||||
bind :443 ssl crt /etc/haproxy/certs/chat.example.com.pem alpn h2,http/1.1
|
||||
http-request redirect scheme https unless { ssl_fc }
|
||||
http-request set-header X-Forwarded-For %[src]
|
||||
default_backend fluxer_edge
|
||||
|
||||
backend fluxer_edge
|
||||
server edge 127.0.0.1:8080
|
||||
```
|
||||
|
||||
`timeout client` and `timeout server` stop applying once a connection is upgraded, so `timeout tunnel` is what keeps the Gateway socket and LiveKit signalling alive. `set-header` replaces any header the client sent. `option forwardfor` appends to it. Give the frontend and the backend different names. HAProxy warns on a shared name and drops support for it in 3.3.
|
||||
|
||||
## Apache httpd
|
||||
|
||||
This configuration needs three modules: `mod_proxy`, `mod_proxy_http`, and `mod_headers`. Since 2.4.47 `mod_proxy_http` handles the WebSocket upgrade itself, so `mod_proxy_wstunnel` is not needed.
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName chat.example.com
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/letsencrypt/live/chat.example.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/chat.example.com/privkey.pem
|
||||
|
||||
LimitRequestBody 536870912
|
||||
ProxyPreserveHost On
|
||||
ProxyTimeout 3600
|
||||
|
||||
RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}"
|
||||
|
||||
ProxyPass / http://127.0.0.1:8080/ upgrade=websocket timeout=3600
|
||||
ProxyPassReverse / http://127.0.0.1:8080/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
`upgrade=websocket` is the whole WebSocket configuration on 2.4.47 and later.
|
||||
|
||||
`RequestHeader set` replaces whatever the client sent, and the quotes around `"expr=%{REMOTE_ADDR}"` are required by the expression parser. `ProxyAddHeaders` is on by default and appends the connection address after that value. The edge reads the real address either way.
|
||||
|
||||
`LimitRequestBody` is 0 by default, which means no limit, but distribution packages and `<Directory>` blocks often set it lower. The 536870912 above is 512 MiB. `ProxyTimeout` and the `timeout=3600` parameter both bound the upstream connection, and an hour covers the Gateway socket.
|
||||
|
||||
## Cloudflare Tunnel
|
||||
|
||||
One ingress rule for the hostname, with the catch-all `cloudflared` requires under it:
|
||||
|
||||
```yaml
|
||||
tunnel: fluxer
|
||||
credentials-file: /etc/cloudflared/fluxer.json
|
||||
|
||||
ingress:
|
||||
- hostname: chat.example.com
|
||||
service: http://127.0.0.1:8080
|
||||
originRequest:
|
||||
connectTimeout: 30s
|
||||
- service: http_status:404
|
||||
```
|
||||
|
||||
When `cloudflared` runs as a container on the stack's Docker network, point it at `http://edge:8080`. The published port is then never used. Compose prefixes the network name with the project name, which the stack sets to `fluxer`, so the network is `fluxer_fluxer` and an external declaration names it in full:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
fluxer:
|
||||
external: true
|
||||
name: fluxer_fluxer
|
||||
```
|
||||
|
||||
Cloudflare appends the visitor address to any `X-Forwarded-For` the visitor sent, so the header can arrive with a forged value in front of the real one. The edge reads the rightmost address that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Cloudflare appended. A Transform Rule that sets `X-Forwarded-For` to `cf.connecting_ip` removes the ambiguity outright.
|
||||
|
||||
Cloudflare caps a request body at 100 MB on Free and Pro, 200 MB on Business, and 500 MB on Enterprise, and answers 413 above the cap. The cap applies to tunnel traffic, and no `cloudflared` setting raises it. Keep the [`max_attachment_file_size`](/http-api/instance/#limit-keys) limit below the cap for your plan, in the admin dashboard under limit configuration. It defaults to 26214400 bytes for a non-premium account and 524288000 bytes for a premium one, which is above every cap below Enterprise.
|
||||
|
||||
A tunnel has the web app, API, Gateway, admin dashboard, media routes, and LiveKit signalling. It has no LiveKit media.
|
||||
|
||||
## Nginx Proxy Manager
|
||||
|
||||
Add a proxy host with these values:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Domain Names | The hostname `chat.example.com` |
|
||||
| Scheme | Plain `http` |
|
||||
| Forward Hostname / IP | `127.0.0.1`, or the `edge` container when both run in Docker |
|
||||
| Forward Port | Port `8080` |
|
||||
| Websockets Support | On, and required |
|
||||
| Block Common Exploits | Off, because its request filtering rejects legitimate API traffic |
|
||||
| SSL | Request a certificate, then turn on Force SSL and HTTP/2 Support |
|
||||
|
||||
In the Advanced tab:
|
||||
|
||||
```nginx
|
||||
client_max_body_size 512m;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
```
|
||||
|
||||
Nginx Proxy Manager appends to `X-Forwarded-For`, and the visitor's own value arrives in front of the real address. The edge takes the rightmost entry that is not in `FLUXER_EDGE_TRUSTED_PROXIES`, which is the one Nginx Proxy Manager appended. Users on a LAN or a VPN inside the default `private_ranges` are the exception and resolve to the forged entry, so narrow `FLUXER_EDGE_TRUSTED_PROXIES` to the proxy's address in that layout.
|
||||
|
||||
## What the single port routes
|
||||
|
||||
The instance rewrites paths before handing them to an upstream. Your proxy must not rewrite anything itself.
|
||||
|
||||
#### `/_health`
|
||||
|
||||
No upstream. Answered by the edge itself.
|
||||
|
||||
#### `/gateway?v=1&encoding=json&compress=zstd-stream&stream=1`
|
||||
|
||||
Goes to `gateway:8080` and reaches the upstream as `/?v=1&encoding=json&compress=zstd-stream&stream=1`.
|
||||
|
||||
#### `/gateway/foo`
|
||||
|
||||
Goes to `gateway:8080` and reaches the upstream as `/foo`.
|
||||
|
||||
#### `/api/v1/users/@me`
|
||||
|
||||
Goes to `api:8080` and reaches the upstream as `/v1/users/@me`.
|
||||
|
||||
#### `/media/attachments/1/2/a.png`
|
||||
|
||||
Goes to `media-proxy:8080` and reaches the upstream as `/attachments/1/2/a.png`.
|
||||
|
||||
#### `/livekit/rtc/v1`
|
||||
|
||||
Goes to `livekit:7880` and reaches the upstream as `/rtc/v1`.
|
||||
|
||||
#### `/admin`
|
||||
|
||||
Goes to `admin:8080` and reaches the upstream as `/`.
|
||||
|
||||
#### `/admin/users`
|
||||
|
||||
Goes to `admin:8080` and reaches the upstream as `/users`.
|
||||
|
||||
#### Static asset paths
|
||||
|
||||
`/web/*`, `/emoji/*`, `/libs/*`, `/avatars/*`, `/badges/*`, `/desktop/*` and `/embeds/*` go to `static-proxy:8080` unchanged.
|
||||
|
||||
#### `/.well-known/fluxer`
|
||||
|
||||
Goes to `api:8080` unchanged.
|
||||
|
||||
#### Apple app site association
|
||||
|
||||
`/.well-known/apple-app-site-association`, and `/apple-app-site-association` for older iOS, go to `app-proxy:8080` unchanged.
|
||||
|
||||
#### `/.well-known/assetlinks.json`
|
||||
|
||||
Goes to `app-proxy:8080` unchanged.
|
||||
|
||||
#### `/version.json`
|
||||
|
||||
Goes to `app-proxy:8080` unchanged.
|
||||
|
||||
#### Anything else
|
||||
|
||||
Goes to `app-proxy:8080` unchanged.
|
||||
|
||||
Pass the query string on `/gateway` through untouched. Clients always send `?v=`, `?encoding=`, `?compress=` and `?stream=`, and `1` is the only version the Gateway accepts.
|
||||
|
||||
Apple and Google fetch the two association files themselves at those fixed paths, for saved-password autofill in the iOS apps and link handling in the Android ones. The last entry already has all four, so a proxy that forwards `/` needs no extra rule. A proxy that forwards a named path allowlist has to list these four paths along with everything else that entry covers.
|
||||
|
||||
`/_metrics` on the API, Media Proxy, and Gateway, plus `/_health/ready`, `/_health/drain`, and `/_health/undrain` on the Gateway, are gated to loopback and are unreachable through any proxy. The probes that work through a proxy are `/_health`, `/api/_health`, `/gateway/_health`, and `/media/_health`.
|
||||
|
||||
## Trusted proxies
|
||||
|
||||
`FLUXER_EDGE_TRUSTED_PROXIES` names the peer addresses whose `X-Forwarded-For` the edge believes. It defaults to `private_ranges`, which Caddy expands to `192.168.0.0/16`, `172.16.0.0/12`, `10.0.0.0/8`, `127.0.0.1/8`, `fd00::/8`, and `::1`, so a proxy on the same host or the same Docker network needs no change.
|
||||
|
||||
The edge resolves one client address per request and rewrites `X-Forwarded-For` to it on every upstream hop, so no service ever reads what a visitor sent. A peer outside the list becomes the client address, and the edge discards the header it sent. A peer inside the list contributes the rightmost header entry that is not itself trusted, and a proxy that appends still delivers the real caller.
|
||||
|
||||
A proxy that reaches the instance from a public address is not trusted, so the edge discards its header and attributes every request to the proxy itself. That puts all your users in one rate limit bucket and one geolocation. Add the address, and list `private_ranges` too if you still need it:
|
||||
|
||||
```ini
|
||||
FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 203.0.113.10/32
|
||||
```
|
||||
|
||||
`private_ranges` does not cover `100.64.0.0/10`, so a proxy reaching the instance over a Tailscale or other CGNAT tailnet address is untrusted and collapses every visitor onto that one address. Name the tailnet address of the proxy:
|
||||
|
||||
```ini
|
||||
FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 100.101.102.103/32
|
||||
```
|
||||
|
||||
The edge believes a trusted peer without question. Anyone who can open a TCP connection from a trusted address can set `X-Forwarded-For` to any value and be recorded as that address, which defeats bans and rate limits and pollutes abuse detection. Keep the list down to the one address your proxy arrives from. That is also the only correct list on a LAN or VPN deployment, where a visitor whose own address falls inside a wider list is recorded under the proxy's address.
|
||||
|
||||
Find the address the edge sees a host-side proxy arrive from:
|
||||
|
||||
```bash
|
||||
docker network inspect fluxer_fluxer -f '{{(index .IPAM.Config 0).Gateway}}'
|
||||
```
|
||||
|
||||
The edge reads the variable at container start, so apply a change with `docker compose up -d`.
|
||||
|
||||
:::caution[Keep the plain HTTP port off the LAN]
|
||||
`private_ranges` trusts the whole LAN. With `FLUXER_EDGE_BIND=0.0.0.0:8080`, any host on that network can impersonate any client address. Firewall the port to the proxy host.
|
||||
:::
|
||||
|
||||
## Voice media does not use the proxy
|
||||
|
||||
LiveKit signalling goes through `/livekit/*` like everything else. WebRTC media does not touch the proxy at all:
|
||||
|
||||
- `7882/udp` is media.
|
||||
- `7881/tcp` is the fallback when UDP is blocked.
|
||||
|
||||
Both ports are published directly by the stack and must reach the host. A proxy or tunnel in front of `443` does nothing for them.
|
||||
|
||||
Hosting LiveKit on a hostname other than `FLUXER_DOMAIN` means widening the Content-Security-Policy the web app runs under:
|
||||
|
||||
```ini
|
||||
FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
|
||||
```
|
||||
@@ -0,0 +1,237 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Upgrading
|
||||
description: Upgrading and rolling back an instance with the installer, what the backup covers, and how to restore it.
|
||||
---
|
||||
|
||||
:::tip[Plutonium and donations fund Fluxer]
|
||||
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer). The Operator Pass is coming, and adds a direct line to the team for help and feedback.
|
||||
:::
|
||||
|
||||
An upgrade moves an instance to a newer release. `install.sh --update` does all of it: back up, download the new stack files, pull the new images, recreate the containers that changed. The stack files are the three compose files, the `Caddyfile` and `.env.example`. Allow about twenty minutes, most of it the backup and the pull.
|
||||
|
||||
| Requirement | Value |
|
||||
| --- | --- |
|
||||
| Working directory | The one holding `.env`, `docker-compose.yml`, `docker-compose.proxy.yml`, `tunnel.compose.yml` and `Caddyfile` |
|
||||
| Compose commands | Behind your own reverse proxy, set `COMPOSE_FILE` in `.env` once and every command picks up the overlay |
|
||||
| Free disk | Room for one database dump, one copy of the uploads, and the new images beside the old ones |
|
||||
| Downtime | A minute or two for the recreate, plus however long the uploads copy takes with the stack stopped |
|
||||
|
||||
Read the release notes between the version you run and the one you are moving to, at [Releases](https://github.com/fluxerapp/fluxer/releases).
|
||||
|
||||
## Match the images to the stack files
|
||||
|
||||
The images come from `FLUXER_IMAGE_TAG` in `.env`. The stack files come from a git ref, which is a branch or a tag in the Fluxer repository. The installer derives that ref from the image tag: `main` when the tag is `v1` or `latest`, and the tag string itself for anything else. `--ref` overrides the derivation. `docker compose pull` never updates a stack file, and refreshing a stack file never moves an image.
|
||||
|
||||
The repository holds no ref by the name of a pinned image tag. A release tags each image on its own, as `[email protected]`, so pass `--ref` with that tag or with the commit it points at. Without that override the download fails with exit 4.
|
||||
|
||||
## What the script does
|
||||
|
||||
`--update` runs these steps in this order, and stops on the first one that fails:
|
||||
|
||||
| Step | What it does |
|
||||
| --- | --- |
|
||||
| Record | Writes the image references, the image ID each container has, and the tag `.env` names, into a new record directory |
|
||||
| Save | Copies `.env` and the five stack files into that record |
|
||||
| Dump | Dumps the database with the stack still serving, and checks the custom-format header on the result |
|
||||
| Copy | Stops the stack, copies the uploads volume, and starts it again on the images it was already running |
|
||||
| Fetch | Downloads the stack files at the ref into a staging directory inside the working directory |
|
||||
| Guard | Refuses when the refreshed `docker-compose.yml` moves Postgres to a new major version |
|
||||
| Place | Moves every downloaded file into the working directory at once |
|
||||
| Pull | Pulls the images while the old containers still serve |
|
||||
| Recreate | Runs `docker compose up -d --remove-orphans`, then restarts `edge` when the `Caddyfile` changed |
|
||||
| Verify | Polls Compose state until every service is ready, then probes `/_health` on the hostname |
|
||||
|
||||
A failed run leaves the instance running on the images it already had.
|
||||
|
||||
`docker compose up -d` does not notice a changed `Caddyfile`. The script restarts `edge` by name to pick it up.
|
||||
|
||||
The refreshed `docker-compose.yml` renames the `caddy` service to `edge`, and the two publish the same host ports. `--remove-orphans` takes the old container down in the same call, so the new one can bind them. Without it the step stops with `Bind for 0.0.0.0:443 failed`. It also removes any container in the project whose service the loaded Compose files no longer define, so keep `COMPOSE_FILE` the same across an upgrade.
|
||||
|
||||
The rename also moves the `caddy-data` and `caddy-config` volumes to `edge-data` and `edge-config`, so the old two are left unused and the edge requests its certificate again on the first start.
|
||||
|
||||
`FLUXER_IMAGE_TAG` is the only line in `.env` that either upgrade mode writes. Every secret is left as it is.
|
||||
|
||||
The refreshed `docker-compose.yml` requires `FLUXER_ERLANG_COOKIE`. An `.env` written by an earlier installer has no such line, so add `FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32)` to it before you upgrade. The value is 64 hex characters, which is what a fresh install writes. Until `.env` sets it, every Compose command against the stack stops with `set FLUXER_ERLANG_COOKIE in .env`.
|
||||
|
||||
## The script is the reference
|
||||
|
||||
Every step above sits in the script beside a comment holding the command that does that step alone and the reason the step exists. Read [https://fluxer.dev/install.sh](https://fluxer.dev/install.sh), or [https://fluxer.dev/install.ps1](https://fluxer.dev/install.ps1) for Windows.
|
||||
|
||||
## Run the upgrade
|
||||
|
||||
Keep the installer beside the stack files. [Get started](/operator/get-started/#step-4-bring-up-the-instance) has the download and the checksum check for a fresh copy.
|
||||
|
||||
See the plan first:
|
||||
|
||||
```bash
|
||||
cd ~/fluxer
|
||||
sh install.sh --update --dry-run
|
||||
```
|
||||
|
||||
It prints the images the stack runs now, the backup it intends to take, which stack files the ref changes, which services that makes it restart, and any refusal that would stop the run. It writes nothing outside a temporary directory it removes on exit.
|
||||
|
||||
Then run it:
|
||||
|
||||
```bash
|
||||
sh install.sh --update
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
.\install.ps1 -Update
|
||||
```
|
||||
|
||||
The run ends by naming the record it wrote and the command that rolls back to it. It checks its own work with the readiness poll and one probe of `/_health`. Run the public probes in [Check that it works](/operator/get-started/#step-5-check-that-it-works) afterwards.
|
||||
|
||||
| Flag | Windows | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--update` | `-Update` | Record, back up, refresh, pull, recreate, verify |
|
||||
| `--rollback` | `-Rollback` | Put back the images and stack files of the newest record |
|
||||
| `--dry-run` | `-DryRun` | Print the plan. Change nothing |
|
||||
| `--backup-dir <path>` | `-BackupDir` | Where records go. Default `<dir>/backups` |
|
||||
| `--no-volume-backup` | `-NoVolumeBackup` | Take the database dump and skip the uploads copy |
|
||||
| `--skip-backup-accept-data-loss` | `-SkipBackupAcceptDataLoss` | Upgrade with no backup at all |
|
||||
|
||||
`--dir` and `--ref` mean what they mean on an install, and the installer derives an omitted `--ref` from the `FLUXER_IMAGE_TAG` line in `.env`. Every failure prints a sentence on standard error and exits non-zero, and the script header lists what each code means. A Postgres major version change is exit 3.
|
||||
|
||||
## What the backup covers
|
||||
|
||||
Each upgrade writes one record directory, named `record-` and a UTC stamp, under `backups` or wherever `--backup-dir` points. It holds:
|
||||
|
||||
| Artifact | What it covers |
|
||||
| --- | --- |
|
||||
| `fluxer.dump` | Every account, message, guild and configuration row, in the Postgres custom format |
|
||||
| `seaweedfs-data.tgz` | Every upload, avatar, report and harvest |
|
||||
| `.env` | Every secret the instance was built with |
|
||||
|
||||
The five stack files sit in the record beside those three, so a rollback puts back the exact files the instance was running. The record directory is created `0700` and the `.env` copy inside it is `0600`. Keep records wherever you already keep secrets.
|
||||
|
||||
The dump costs no downtime. The uploads copy stops the stack, and the script starts the stack again on the old images before it goes any further.
|
||||
|
||||
Nothing else is copied. The dump covers `postgres-data`. The other five volumes either rebuild themselves or hold queued work, and [Volumes and buckets](/operator/configuration/#volumes-and-buckets) lists all seven with what each one holds.
|
||||
|
||||
Put a dump on a timer as well. On Linux and macOS, this crontab line writes one a night and keeps two weeks of them:
|
||||
|
||||
```cron
|
||||
15 3 * * * cd /srv/fluxer && docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom > "backups/fluxer-$(date -u +\%Y\%m\%dT\%H\%M\%SZ).dump" && find backups -name 'fluxer-*.dump' -mtime +14 -delete
|
||||
```
|
||||
|
||||
`/srv/fluxer` stands for the directory holding `.env`. Write it as an absolute path, because cron does not expand `~`. An unescaped `%` in a crontab is a newline, which is why every one of them has a backslash.
|
||||
|
||||
## Roll back
|
||||
|
||||
A rollback puts the previous release back:
|
||||
|
||||
```bash
|
||||
cd ~/fluxer
|
||||
sh install.sh --rollback
|
||||
```
|
||||
|
||||
On Windows it is `.\install.ps1 -Rollback`. It takes the newest record, puts back the images and the stack files it holds, recreates, restarts `edge`, and verifies. `--dry-run` prints that plan too.
|
||||
|
||||
A rollback never pulls, so it needs the old images still on the host. Run [Reclaim disk](#reclaim-disk) once an upgrade satisfies you and not before.
|
||||
|
||||
The database does not move. `api`, `worker`, `users-shard` and `messages-shard` apply schema work in place while they start, and an older image does not undo it. Across a release that changed the schema, the dump in the record is the only way back, and putting it back is a decision you make.
|
||||
|
||||
## Restore a backup
|
||||
|
||||
Both artifacts go back with the stack stopped, so nothing writes while they are replaced. Name your own record directory in place of the one below.
|
||||
|
||||
The database restores from the custom-format dump:
|
||||
|
||||
```bash
|
||||
docker compose stop
|
||||
docker compose up -d --wait postgres
|
||||
docker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists \
|
||||
< backups/record-20260831T120000Z/fluxer.dump
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
`--clean` prints notices about objects that do not exist yet, which is expected against a fresh directory.
|
||||
|
||||
PowerShell has no `<` redirection. On Windows, copy the dump into the container and name it as a file:
|
||||
|
||||
```powershell
|
||||
docker compose stop
|
||||
docker compose up -d --wait postgres
|
||||
docker compose cp backups\record-20260831T120000Z\fluxer.dump postgres:/tmp/fluxer.dump
|
||||
docker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists /tmp/fluxer.dump
|
||||
docker compose exec -T postgres rm /tmp/fluxer.dump
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The uploads restore through the same helper container the backup used, in reverse:
|
||||
|
||||
```bash
|
||||
docker compose stop
|
||||
docker run --rm -v fluxer_seaweedfs-data:/data \
|
||||
-v "$PWD/backups/record-20260831T120000Z:/backup" alpine:3.22 \
|
||||
sh -c 'find /data -mindepth 1 -delete && tar xzf /backup/seaweedfs-data.tgz -C /data'
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
`tar` extracts over whatever is already on the volume, so the `find` empties it first. In PowerShell write `${PWD}` in place of `$PWD`. Success is an existing attachment URL answering 200 again. The `fluxer_` prefix is the Compose project name, which `docker-compose.yml` sets to `fluxer`.
|
||||
|
||||
## Move to a new Postgres major version
|
||||
|
||||
`docker-compose.yml` pins `postgres:16-alpine`. A newer major does not read the data directory an older major wrote, so the data moves across through a dump. The upgrade refuses when the refreshed compose file changes that pin, because the move destroys the volume holding the database.
|
||||
|
||||
Take the dump while the old major is still serving, then stop everything and drop the volume:
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom \
|
||||
> backups/pre-major.dump
|
||||
docker compose down
|
||||
docker volume rm fluxer_postgres-data
|
||||
```
|
||||
|
||||
On Windows, take that dump the way [Restore a backup](#restore-a-backup) moves one, with `-f /tmp/pre-major.dump` and `docker compose cp`, and restore it the same way.
|
||||
|
||||
Once that volume is removed, the dump is the only copy of the database.
|
||||
|
||||
Put the new `postgres` tag in `docker-compose.yml`, start the database on its own, and restore into the empty directory:
|
||||
|
||||
```bash
|
||||
docker compose up -d --wait postgres
|
||||
docker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists \
|
||||
< backups/pre-major.dump
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Run `sh install.sh --update` afterwards to finish the move on the refreshed files.
|
||||
|
||||
## Pin a release
|
||||
|
||||
`FLUXER_IMAGE_TAG=v1` tracks the latest compatible release, so every upgrade can move the instance. Set it to a release tag to hold one version and upgrade on your own schedule.
|
||||
|
||||
A pinned instance also pins its stack files. [Match the images to the stack files](#match-the-images-to-the-stack-files) has the `--ref` a pinned tag needs.
|
||||
|
||||
The tag applies only to the eleven Fluxer images. `caddy`, `postgres`, `valkey`, `nats`, `meilisearch`, `seaweedfs`, and `livekit` are pinned inside `docker-compose.yml` and move only when an upgrade refreshes that file.
|
||||
|
||||
## Change the domain or the passkey relying party
|
||||
|
||||
`FLUXER_DOMAIN` supplies the default `FLUXER_PASSKEY_RP_ID`, which is the domain a passkey is tied to. A passkey works only under the identifier it was created with.
|
||||
|
||||
:::danger[Changing either value invalidates every passkey]
|
||||
Passkeys registered under the old `FLUXER_PASSKEY_RP_ID` stop working and cannot be recovered. Every user has to register a new one, so keep another sign-in method working before you change it.
|
||||
:::
|
||||
|
||||
Changing `FLUXER_DOMAIN` also changes the certificate the edge requests, the CORS origins the API accepts, and the admin OAuth redirect URI.
|
||||
|
||||
## Reclaim disk
|
||||
|
||||
Each upgrade leaves the previous images behind, which adds up to a few gigabytes over time:
|
||||
|
||||
```bash
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
On the default moving tag, the pull moves `v1` onto the new images and leaves the ones it replaced untagged, so this removes them. Those are the images a rollback on a moving tag needs, so prune only once the upgrade satisfies you. A pinned release keeps its own tag and survives the prune. Remove one of those by naming its tag in `docker image rm`.
|
||||
|
||||
`docker system prune -a` reclaims more. It also deletes every image that no container references, including ones unrelated to Fluxer.
|
||||
|
||||
:::danger[Never add -a to a volume prune]
|
||||
`docker volume prune -a` removes named volumes no container uses. After a `docker compose down` that is every volume the instance has, which is the whole instance.
|
||||
:::
|
||||
Reference in new issue
Block a user