Upgrading
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 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.
Match the images to the stack files
Section titled “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 for 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 fluxer-api@2026.813.205040, 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
Section titled “What the script does”--update runs these steps in this order, and stops on the first one that fails:
| Step | What it does |
|---|---|
| Mint | Writes any key the refreshed stack requires that .env does not hold, listed under Run the upgrade |
| 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 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.
An instance set up before the edge served LiveKit at /livekit needs one edit that the upgrade does not make. Until you make it, voice does not connect. Voice signalling moved to /livekit has the edit.
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. Keep a local compose change has the file layout that survives one. When the compose file list names a missing file has the error Compose prints when COMPOSE_FILE names a file that does not exist.
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.
Upgrades change FLUXER_IMAGE_TAG and add missing required secrets. Existing values are preserved, except for the unusable upload-relay placeholder described below.
The refreshed stack requires FLUXER_ERLANG_COOKIE. --update generates a missing value as 64 hex characters. Without it, Compose commands fail with set FLUXER_ERLANG_COOKIE in .env.
To write it by hand, run the generator in a shell:
printf '\nFLUXER_ERLANG_COOKIE=%s\n' "$(openssl rand -hex 32)" >> .envDo not paste FLUXER_ERLANG_COOKIE=$(openssl rand -hex 32) into .env. Compose does not execute shell commands in that file.
api and media-proxy require FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 to decode to at least 32 bytes. --update replaces a missing or CHANGE_ME value with a generated secret. Both services must use the same value.
To write it by hand, run the generator in a shell the same way:
printf '\nFLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=%s\n' "$(openssl rand -base64 32)" >> .envThe script is the reference
Section titled “The script is the reference”Read the Linux and macOS installer or Windows installer before running it.
Keep a local compose change
Section titled “Keep a local compose change”An upgrade replaces the stack files, including direct edits to docker-compose.yml, docker-compose.proxy.yml, tunnel.compose.yml or the Caddyfile. Use --dry-run to see which files will change.
The supported way to hold a local choice is a separate file, listed in COMPOSE_FILE in .env:
COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:local.compose.ymlCompose merges the files left to right, so local.compose.yml wins over the ones before it. An upgrade refreshes the stack files and nothing else, so a file outside that set survives every upgrade untouched. A record copies .env and those files, and no other file from the working directory. An override file is never backed up with them, so keep it wherever the rest of the configuration lives.
Keep environment settings in .env. Upgrades preserve them apart from the tag and required-secret changes described above.
Enable the overlay has the overlay this is most often used for. Docker labels has a worked third file.
When the compose file list names a missing file
Section titled “When the compose file list names a missing file”COMPOSE_FILE names files relative to the working directory, and Compose refuses to load the set when one of them is absent:
stat /srv/fluxer/docker-compose.proxy.yml: no such file or directoryEvery docker compose command stops there. This can affect older installations whose .env names an overlay that has not been downloaded. The installer reports the missing file and a command to restore it.
Download the missing file at the ref the upgrade moves to, then run the upgrade:
curl -fsSL --proto '=https' --tlsv1.2 -o docker-compose.proxy.yml \ https://raw.githubusercontent.com/fluxerapp/fluxer/main/deploy/self-hosting/docker-compose.proxy.ymlmain is the ref for FLUXER_IMAGE_TAG=v1 and for latest. Match the images to the stack files has the ref a pinned tag needs.
Removing the line from .env also lets the upgrade run, and it is the wrong fix. Without the overlay, the edge publishes host ports 80 and 443, which the reverse proxy already on the host uses.
Run the upgrade
Section titled “Run the upgrade”Keep the installer beside the stack files. Get started has the download and the checksum check for a fresh copy.
See the plan first:
cd ~/fluxersh install.sh --update --dry-runIt prints:
- The images the stack runs now
- The backup it intends to take
- The stack files the ref changes
- The services those changes make it restart
- Any refusal that would stop the run
It writes nothing outside a temporary directory it removes on exit.
Then run it:
sh install.sh --updateOn Windows:
.\install.ps1 -UpdateThe 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 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.
Voice signalling moved to /livekit
Section titled “Voice signalling moved to /livekit”The edge serves LiveKit at /livekit/*. An instance set up before that routing served it under /gateway/livekit, and that path is gone. Voice stops connecting after the upgrade and the run still reports success, because the readiness poll and the /_health probe never reach voice.
Before:
wss://chat.example.com/gateway/livekitAfter:
wss://chat.example.com/livekitBoth https and wss work. Update FLUXER_LIVEKIT_URL in .env to the new path, or remove it to use the derived address, then run docker compose up -d api.
When api starts, it writes the new address to the existing default-server-1 server in the default region. For any other server, change Endpoint under Voice Servers in the admin dashboard. Correct .env first, or a later restart can restore the old address on the default server.
Voice media is unaffected. It never went through the edge, and 7881/tcp and 7882/udp still reach the host directly.
What the backup covers
Section titled “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:
| Artefact | 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 stack files sit in the record beside them, 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.
Nothing else is copied. The dump covers postgres-data. The other volumes either rebuild themselves or hold queued work, and Volumes and buckets lists them all 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:
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 above has a backslash.
Run a data store outside the stack
Section titled “Run a data store outside the stack”The stack ships its own Postgres and its own object store and points at both by service name. .env moves either one somewhere else, and the bundled service is used when the line is absent:
FLUXER_POSTGRES_HOST=db.example.comFLUXER_POSTGRES_PORT=5432FLUXER_POSTGRES_SSL=trueFLUXER_S3_ENDPOINT=https://s3.eu-central-1.amazonaws.comFLUXER_S3_REGION=eu-central-1FLUXER_S3_FORCE_PATH_STYLE=falseFLUXER_S3_PUBLIC_ENDPOINT follows FLUXER_S3_ENDPOINT when it is not set on its own, and the credentials stay FLUXER_S3_ACCESS_KEY and FLUXER_S3_SECRET_KEY. FLUXER_S3_BUCKET_CDN, FLUXER_S3_BUCKET_UPLOADS, FLUXER_S3_BUCKET_DOWNLOADS, FLUXER_S3_BUCKET_REPORTS and FLUXER_S3_BUCKET_HARVESTS name the buckets. The bundled object store creates whichever names they hold, and a store outside the stack needs those buckets to exist already.
FLUXER_KV_URL, FLUXER_NATS_URL, FLUXER_SEARCH_URL and FLUXER_LIVEKIT_INTERNAL_URL move the other bundled services the same way, and FLUXER_NATS_JETSTREAM_URL and FLUXER_SVC_NATS_URL follow FLUXER_NATS_URL when they are not set on their own.
Pointing the stack elsewhere leaves the bundled service defined and running with nothing reading it. Take it out with an override file listed in COMPOSE_FILE, which Keep a local compose change describes, rather than by editing docker-compose.yml, which the Place step replaces on every upgrade.
The installer backs up only the bundled database and object store. Arrange separate backups for external stores before upgrading, and keep them with the release’s backup record.
Roll back
Section titled “Roll back”A rollback puts the previous release back:
cd ~/fluxersh install.sh --rollbackOn 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 only once the upgrade is known good.
A rollback does not undo database migrations or restore data. If the previous release requires the old schema, restore the database backup separately.
Restore a backup
Section titled “Restore a backup”Restore both artefacts with the stack stopped, so nothing writes while they are replaced. Name your own record directory instead of the one below.
The database restores from the custom-format dump:
docker compose stopdocker compose up -d --wait postgresdocker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists \ < backups/record-20260831T120000Z/fluxer.dumpdocker 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:
docker compose stopdocker compose up -d --wait postgresdocker compose cp backups\record-20260831T120000Z\fluxer.dump postgres:/tmp/fluxer.dumpdocker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists /tmp/fluxer.dumpdocker compose exec -T postgres rm /tmp/fluxer.dumpdocker compose up -dThe uploads restore through the same helper container the backup used, in reverse:
docker compose stopdocker 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 -dtar extracts over whatever is already on the volume, so the find empties it first. In PowerShell write ${PWD} instead 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
Section titled “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:
docker compose exec -T postgres pg_dump -U fluxer -d fluxer --format=custom \ > backups/pre-major.dumpdocker compose downdocker volume rm fluxer_postgres-dataOn Windows, run pg_dump inside the container with -f /tmp/pre-major.dump and copy the file out with docker compose cp. Restore it with docker compose cp and pg_restore as Restore a backup shows.
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:
docker compose up -d --wait postgresdocker compose exec -T postgres pg_restore -U fluxer -d fluxer --clean --if-exists \ < backups/pre-major.dumpdocker compose up -dRun sh install.sh --update afterwards to finish the move on the refreshed files.
Pin a release
Section titled “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 has the --ref a pinned tag needs.
The tag applies only to the Fluxer images. caddy, postgres, valkey, nats, meilisearch, seaweedfs and livekit are pinned in docker-compose.yml, and they move only when an upgrade refreshes that file.
Change the domain or the passkey relying party
Section titled “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.
Changing FLUXER_DOMAIN also changes the certificate the edge requests, the CORS origins the API accepts, and the admin OAuth redirect URI.
Reclaim disk
Section titled “Reclaim disk”Each upgrade leaves the previous images behind, which adds up to a few gigabytes over time:
docker image prune -fOn the default moving tag, the pull moves v1 onto the new images and leaves the ones it replaced untagged, so this removes them. A rollback on a moving tag needs those images. Prune only once the upgrade is known good. 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.