Get Started¶
Run your own Fluxer instance with Docker Compose. This guide takes you from a fresh server to a working self-hosted instance with the web app, API, gateway, admin dashboard, media uploads, search, storage, and voice signaling behind one public hostname.
What you'll run¶
The self-hosted stack is one Docker Compose project:
- Caddy terminates public HTTP(S) or receives traffic from a Cloudflare Tunnel.
- App proxy serves the Fluxer web client and injects instance bootstrap data.
- API handles accounts, auth, communities, messages, uploads, admin APIs, and instance discovery.
- Admin dashboard is required and is served at
/admin. - Gateway handles WebSocket sessions, presence, dispatch, push fanout, and realtime events.
- Messages service builds message responses and serves message history.
- Media proxy handles attachment upload relay, media metadata, thumbnails, and object reads.
- Static proxy serves Fluxer fonts, icons, emoji, badges, default avatars, and voice client assets from the same hostname.
- LiveKit handles voice and video signaling and WebRTC media.
- Postgres, Valkey, NATS, Meilisearch, and SeaweedFS provide data, cache, events, search, and S3-compatible object storage.
The app bundle is served by the self-host app-proxy image; shared static assets are served by the standalone static-proxy container. The stack does not depend on Fluxer's public static asset host.
Requirements¶
- A Linux server or VM that can run Docker Engine.
- Docker Engine plus the Docker Compose plugin.
- A hostname for the instance, for example
chat.example.com. - Either public inbound
80/tcpand443/tcp, or a Cloudflare Tunnel that routes the hostname to the Caddy container. - For production voice and video media, a public path to
7881/tcpand7882/udp. - At least 2 vCPU, 4 GB RAM, and 20 GB disk. Use 4 vCPU and 8 GB RAM or more for a small active community.
The stack idles around a few GB of memory, and startup is the heaviest point because all service images initialize at once.
Step 1: Install Docker¶
Install Docker Engine from Docker's official instructions for your distribution:
Confirm the versions:
Use Docker Engine 24 or newer and the Compose v2 plugin.
Step 2: Download the stack¶
Create a working directory and download the stack files:
mkdir fluxer
cd fluxer
base=https://raw.githubusercontent.com/fluxerapp/fluxer/main/deploy/self-hosting
curl -fsSLO "$base/docker-compose.yml"
curl -fsSLO "$base/Caddyfile"
curl -fsSLO "$base/livekit.yaml"
curl -fsSL "$base/.env.example" -o .env
You should now have:
Step 3: Configure .env¶
Set the public hostname at the top of .env.
For a normal public server where Caddy obtains certificates directly:
FLUXER_DOMAIN=chat.example.com
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443
FLUXER_CADDY_SITE_ADDRESS=chat.example.com
FLUXER_VAPID_EMAIL=admin@example.com
For a Cloudflare Tunnel where Cloudflare terminates HTTPS and forwards HTTP to Caddy:
FLUXER_DOMAIN=chat.example.com
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443
FLUXER_CADDY_SITE_ADDRESS=:80
FLUXER_VAPID_EMAIL=admin@example.com
FLUXER_PUBLIC_SCHEME and FLUXER_PUBLIC_PORT describe what users see in their browser. FLUXER_CADDY_SITE_ADDRESS describes what Caddy listens on inside the stack.
Generate the required secrets:
for key in POSTGRES_PASSWORD MEILI_MASTER_KEY FLUXER_S3_SECRET_KEY \
FLUXER_SUDO_MODE_SECRET FLUXER_CONNECTION_INITIATION_SECRET \
FLUXER_GATEWAY_RPC_AUTH_TOKEN FLUXER_MEDIA_PROXY_SECRET_KEY \
FLUXER_ADMIN_SECRET_KEY_BASE FLUXER_ADMIN_OAUTH_CLIENT_SECRET \
LIVEKIT_API_SECRET; do
sed -i "s|^$key=.*|$key=$(openssl rand -hex 32)|" .env
done
sed -i "s|^FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=.*|FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=$(openssl rand -base64 32)|" .env
VAPID=$(docker run --rm node:24-alpine npx --yes web-push generate-vapid-keys --json)
pub=$(printf '%s' "$VAPID" | grep -o '"publicKey":"[^"]*"' | cut -d'"' -f4)
priv=$(printf '%s' "$VAPID" | grep -o '"privateKey":"[^"]*"' | cut -d'"' -f4)
sed -i "s|^FLUXER_VAPID_PUBLIC_KEY=.*|FLUXER_VAPID_PUBLIC_KEY=$pub|" .env
sed -i "s|^FLUXER_VAPID_PRIVATE_KEY=.*|FLUXER_VAPID_PRIVATE_KEY=$priv|" .env
Keep these defaults unless you know you need to change them:
LIVEKIT_API_KEY=fluxer; the secret isLIVEKIT_API_SECRET.FLUXER_S3_ACCESS_KEY=fluxer; the secret isFLUXER_S3_SECRET_KEY.- Email starts disabled. Enable SMTP later from
.envand the admin dashboard.
Keep .env private
.env contains every secret for the instance. Do not commit it, paste it into support tickets, or put it in screenshots.
Step 4: Publish the hostname¶
Create DNS records for the hostname:
Arecord fromchat.example.comto the server IPv4 address.- Optional
AAAArecord fromchat.example.comto the server IPv6 address.
Leave FLUXER_CADDY_SITE_ADDRESS=chat.example.com. Caddy will request and renew certificates automatically when 80/tcp and 443/tcp can reach the server.
Use this when the server should not expose public web ports.
- Set
FLUXER_CADDY_SITE_ADDRESS=:80. - In Cloudflare, create a Tunnel public hostname for your Fluxer domain.
- If
cloudflaredruns inside the Compose project, point the public hostname service tohttp://caddy:80. - If
cloudflaredruns directly on the host, point the public hostname service tohttp://127.0.0.1:80.
A temporary Compose override keeps the tunnel next to Caddy without saving the token in your main stack:
cat > cloudflared.compose.yml <<'YAML'
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run --token ${CLOUDFLARED_TOKEN:?set CLOUDFLARED_TOKEN}
depends_on:
- caddy
YAML
export CLOUDFLARED_TOKEN='paste-your-tunnel-token-here'
docker compose -f docker-compose.yml -f cloudflared.compose.yml up -d cloudflared
Voice media is not carried by a normal public hostname tunnel
The web app, API, admin dashboard, gateway WebSocket, media proxy HTTP routes, and LiveKit signaling can work through the tunnel. LiveKit WebRTC media still needs reachable 7881/tcp and 7882/udp, or a TURN deployment.
Step 5: Open the firewall¶
If you are using a direct public server, allow inbound:
22/tcpor your SSH port.80/tcpand443/tcpfor Caddy.7881/tcpand7882/udpfor LiveKit media.
If you are using a Cloudflare Tunnel for web traffic, you can block inbound 80/tcp and 443/tcp at the provider firewall. Keep LiveKit media closed too unless you are intentionally exposing voice/video media or using a TURN server.
Provider firewall first
Docker-published ports can bypass host firewalls such as UFW because Docker installs its own packet-filtering rules. Prefer your cloud provider's firewall or security group for internet-facing policy.
Step 6: Start the stack¶
Start Fluxer:
If you are using the Cloudflare override from above, start both files together:
Watch the startup:
The first start can take several minutes while images download and services initialize. seaweedfs-init exits after creating object-storage buckets; that is expected.
Step 7: Verify the instance¶
Set your domain in the shell:
Check every public HTTP entry point:
for path in /_health /api/_health /gateway/_health /media/_health /admin/_health; do
curl -fsS -o /tmp/fluxer-check -w "$path %{http_code}\n" "https://$FLUXER_DOMAIN$path"
done
Expected result:
Check instance discovery:
curl -fsS "https://$FLUXER_DOMAIN/api/.well-known/fluxer" | jq '.features.self_hosted, .endpoints.admin, .endpoints.gateway, .endpoints.media, .endpoints.static_cdn'
You should see true, an admin URL ending in /admin, a gateway URL ending in /gateway, a media URL ending in /media, and a static asset URL equal to the instance origin.
Check the web app, admin login page, app bundle, and static asset container:
curl -fsSI "https://$FLUXER_DOMAIN" | sed -n '1,8p'
curl -fsSI "https://$FLUXER_DOMAIN/admin/login" | sed -n '1,8p'
asset=$(curl -fsS "https://$FLUXER_DOMAIN" | grep -o 'src="[^"]*/assets/[^"]*"' | head -n1 | cut -d'"' -f2)
case "$asset" in
http*) curl -fsSI "$asset" | sed -n '1,8p' ;;
/*) curl -fsSI "https://$FLUXER_DOMAIN$asset" | sed -n '1,8p' ;;
esac
curl -fsSI "https://$FLUXER_DOMAIN/fonts/ibm-plex.css?v=3" | sed -n '1,8p'
curl -fsSI "https://$FLUXER_DOMAIN/web/favicon-32x32.png" | sed -n '1,8p'
If you are using Cloudflare Tunnel and see HTTP 530, the tunnel connector is not currently connected or the public hostname route points at the wrong service.
Step 8: Create the owner account¶
Open the web app:
Register the first account. On a self-hosted instance, the first accepted registration receives wildcard admin access. Use that account for the initial admin login:
Complete the initial setup from the admin dashboard. At minimum, review:
- Branding and instance name.
- Registration mode: open, approval, or closed.
- Email delivery.
- Captcha policy if you open public registration.
- Single-community mode if you want one default community instead of many user-created communities.
- Voice regions and LiveKit reachability if you are enabling voice.
Email¶
Email is disabled by default. To enable SMTP, set these in .env and restart api, worker, and admin:
FLUXER_EMAIL_ENABLED=true
FLUXER_EMAIL_PROVIDER=smtp
FLUXER_EMAIL_FROM_EMAIL=noreply@example.com
FLUXER_EMAIL_FROM_NAME=Fluxer
FLUXER_EMAIL_SMTP_HOST=smtp.example.com
FLUXER_EMAIL_SMTP_PORT=587
FLUXER_EMAIL_SMTP_USERNAME=example
FLUXER_EMAIL_SMTP_PASSWORD=example-secret
FLUXER_EMAIL_SMTP_SECURE=true
Then test the SMTP configuration from /admin/instance-config.
Voice and video¶
Fluxer uses LiveKit for voice and video. Caddy routes /livekit to LiveKit's HTTP/WebSocket signaling port, but browser media flows over WebRTC:
7882/udpis the normal media path.7881/tcpis the TCP fallback path.7880/tcpstays private behind Caddy for signaling.
On a VPS with 7881/tcp and 7882/udp open, LiveKit can usually auto-detect the public IP. Behind NAT, Cloudflare Tunnel, or restrictive networks, add a TURN server and configure LiveKit for it.
Backups¶
Back up these items before upgrades and on a regular schedule:
.envpostgres-dataseaweedfs-data
For a cold backup:
docker compose stop api worker gateway admin app-proxy media-proxy static-proxy livekit
docker run --rm -v fluxer_postgres-data:/data -v "$PWD/backups:/backup" alpine tar czf /backup/postgres-data.tgz -C /data .
docker run --rm -v fluxer_seaweedfs-data:/data -v "$PWD/backups:/backup" alpine tar czf /backup/seaweedfs-data.tgz -C /data .
docker compose up -d
For production, prefer a Postgres-native dump plus object-storage backup so you do not need to stop the instance.
Upgrading¶
The default image tag is v1, which tracks the latest compatible release:
The fluxer-static image is part of the default stack, so static asset updates are picked up by the same pull-and-restart flow.
To pin a specific release, set FLUXER_IMAGE_TAG in .env to the release tag you want, then pull and restart.
Getting help¶
- File issues and follow development on GitHub.
- For direct access to the team, see Operator Pass.