Fluxer looks like a Discord-style community chat app at first glance.

Under the hood, it is much more than a frontend. It is a full messaging and VoIP platform with a browser app, realtime gateway, API, background workers, media proxy, LiveKit voice, object storage, search, queues, and several Rust service components.

That makes it interesting for self-hosters, but it also means you should approach it like infrastructure.

What is Fluxer?

Fluxer is a free and open-source instant messaging and VoIP chat app for friends, groups, and communities.

I inspected commit:

522cf08e6152bfd2dee8015a1f8288786633efa1

The repository is AGPL licensed and is organized as a TypeScript/Rust monorepo.

Why Self-Host It?

Fluxer is for people who want a self-hostable community chat system rather than another hosted SaaS workspace.

Good reasons to look at it:

  • community text chat;
  • group and direct messaging;
  • voice/VoIP rooms;
  • media uploads;
  • reactions, embeds, and unfurling;
  • a browser client with PWA behavior;
  • desktop packaging through Electron;
  • a public AGPL codebase that can be inspected and modified.

The tradeoff is complexity. This is not a “one container and SQLite” weekend app. Fluxer’s official self-hosting setup is a multi-service deployment.

Architecture

The repo contains several major components:

  • fluxer_app: React browser client.
  • fluxer_app_proxy: Rust app proxy that serves the app, service worker, manifest, bootstrap data, and security headers.
  • fluxer_api: TypeScript API service.
  • fluxer_gateway: realtime gateway.
  • fluxer_media_proxy: media upload and delivery proxy.
  • fluxer_admin: admin interface.
  • fluxer_messages, fluxer_users, fluxer_gifs, fluxer_snowflakes, fluxer_unfurl: Rust services with router/shard roles.
  • fluxer_static: static web, emoji, avatar, desktop, and media assets.
  • fluxer_desktop: Electron desktop shell.
  • packages: shared TypeScript packages for config, schemas, limits, logging, i18n, voice engine, OpenAPI, and utilities.

The self-hosted stack then adds:

  • Caddy as edge proxy;
  • PostgreSQL;
  • Valkey;
  • NATS with JetStream;
  • Meilisearch;
  • SeaweedFS as S3-compatible object storage;
  • LiveKit for voice/WebRTC.

So the clean mental model is:

Browser/PWA -> Caddy edge -> app proxy/API/gateway/media/admin/livekit
Backend services -> Postgres, Valkey, NATS, Meilisearch, SeaweedFS

PWA Details

Fluxer has a real browser UI and PWA hooks.

The app HTML links /manifest.json, includes mobile web app meta tags, references app icons, and registers a service worker from the production browser client.

The service worker is built by:

fluxer_app/scripts/build-sw.mjs

It handles same-origin app navigations, app-shell caching, /manifest.json, and /version.json. It also includes push-notification and app-badge code paths.

That is useful, but it does not make Fluxer an offline chat server. The service worker helps the browser shell behave like an installed app. Messaging, login, uploads, realtime gateway traffic, push delivery, and voice still need the backend.

Does It Use Vite?

Not for the main production web app.

The browser client uses React and Rspack. The fluxer_app/package.json production build runs WASM codegen, generated CSS/i18n assets, rspack build --mode production, and then the service worker build script.

You will see Vite-adjacent pieces in the repo, especially Vitest and a vite-env.d.ts type file. That does not mean the main app is a Vite static site.

Does It Use WASM or Pyodide?

Fluxer uses browser-side WASM.

The inspected client includes:

  • a Rust libfluxcore package built with wasm-bindgen;
  • Rspack handling for .wasm assets;
  • onnxruntime-web WASM assets;
  • audio/noise-suppression dependencies that ship WASM/worklet assets;
  • DeepFilterNet-related static assets.

I did not find Pyodide usage.

Can It Be Deployed Statically to Cloudflare Pages?

Not with full functionality.

You could host some browser build artifacts on a static host, but Fluxer as a product needs the backend. The app needs API routes, auth/session state, realtime gateway behavior, media upload/download routing, LiveKit signaling/media, service discovery, storage, search, and background jobs.

The official self-hosting deployment expects a complete service stack behind one public origin. Cloudflare Pages alone does not provide that.

A practical Cloudflare setup is different:

Cloudflare Tunnel -> local Fluxer Caddy edge -> Docker Compose stack

That gives you Cloudflare-managed ingress while keeping the backend services alive behind it.

Self-Hosting with Docker

I mirrored Fluxer’s official self-hosting Compose files into the Home-Lab snippet instead of writing a tiny incomplete compose.

The reusable Home-Lab config is here:

The site includes the same Compose file from the local snippets folder:

assets/snippets/fluxer/docker-compose.yml
name: fluxer

x-fluxer-postgres-env: &fluxer-postgres-env
  FLUXER_DATABASE_BACKEND: postgres
  FLUXER_POSTGRES_HOST: ${FLUXER_POSTGRES_HOST:-postgres}
  FLUXER_POSTGRES_PORT: "${FLUXER_POSTGRES_PORT:-5432}"
  FLUXER_POSTGRES_DATABASE: ${FLUXER_POSTGRES_DATABASE:-fluxer}
  FLUXER_POSTGRES_USERNAME: ${FLUXER_POSTGRES_USERNAME:-fluxer}
  FLUXER_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
  FLUXER_POSTGRES_SSL: "${FLUXER_POSTGRES_SSL:-false}"
  FLUXER_POSTGRES_PREPARED_STATEMENTS: ${FLUXER_POSTGRES_PREPARED_STATEMENTS:-true}

x-fluxer-env: &fluxer-env
  <<: *fluxer-postgres-env
  FLUXER_ENV: production
  NODE_ENV: production
  LOG_LEVEL: ${LOG_LEVEL:-info}
  FLUXER_SELF_HOSTED: "true"
  FLUXER_BASE_DOMAIN: ${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}
  FLUXER_PUBLIC_SCHEME: ${FLUXER_PUBLIC_SCHEME:-https}
  FLUXER_PUBLIC_PORT: ${FLUXER_PUBLIC_PORT:-443}
  FLUXER_PUBLIC_ORIGIN: ${FLUXER_PUBLIC_ORIGIN:-}
  FLUXER_TRUST_CLIENT_IP_HEADER: "${FLUXER_TRUST_CLIENT_IP_HEADER:-true}"
  FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-x-forwarded-for}
  FLUXER_API_HEADERS_TIMEOUT_MS: ${FLUXER_API_HEADERS_TIMEOUT_MS:-30000}
  FLUXER_API_REQUEST_TIMEOUT_MS: ${FLUXER_API_REQUEST_TIMEOUT_MS:-120000}

  FLUXER_KV_URL: ${FLUXER_KV_URL:-redis://valkey:6379/0}
  FLUXER_NATS_URL: ${FLUXER_NATS_URL:-nats://nats:4222}
  FLUXER_NATS_JETSTREAM_URL: ${FLUXER_NATS_JETSTREAM_URL:-${FLUXER_NATS_URL:-nats://nats:4222}}
  FLUXER_NATS_AUTH_TOKEN: ${FLUXER_NATS_AUTH_TOKEN:-}
  FLUXER_SVC_NATS_URL: ${FLUXER_SVC_NATS_URL:-${FLUXER_NATS_URL:-nats://nats:4222}}
  FLUXER_SVC_SHARD_COUNT: "1"

  FLUXER_SEARCH_ENGINE: meilisearch
  FLUXER_SEARCH_URL: ${FLUXER_SEARCH_URL:-http://meilisearch:7700}
  FLUXER_SEARCH_API_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}

  FLUXER_S3_ENDPOINT: ${FLUXER_S3_ENDPOINT:-http://seaweedfs:8333}
  FLUXER_S3_PUBLIC_ENDPOINT: ${FLUXER_S3_PUBLIC_ENDPOINT:-${FLUXER_S3_ENDPOINT:-http://seaweedfs:8333}}
  FLUXER_S3_REGION: ${FLUXER_S3_REGION:-us-east-1}
  FLUXER_S3_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
  FLUXER_S3_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
  FLUXER_S3_FORCE_PATH_STYLE: "${FLUXER_S3_FORCE_PATH_STYLE:-true}"
  FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
  FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
  FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
  FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
  FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
  AWS_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
  AWS_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
  AWS_DEFAULT_REGION: ${FLUXER_S3_REGION:-us-east-1}
  AWS_EC2_METADATA_DISABLED: "true"

  FLUXER_LIVEKIT_ENABLED: "${FLUXER_LIVEKIT_ENABLED:-true}"
  FLUXER_LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}
  FLUXER_LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?set LIVEKIT_API_SECRET in .env}
  FLUXER_LIVEKIT_INTERNAL_URL: ${FLUXER_LIVEKIT_INTERNAL_URL:-http://livekit:7880}
  FLUXER_LIVEKIT_WEBHOOK_URL: http://api:8080/webhooks/livekit
  FLUXER_LIVEKIT_DEFAULT_REGION: '{"id":"default","name":"Default","emoji":"global","latitude":0,"longitude":0}'
  FLUXER_LIVEKIT_URL: ${FLUXER_LIVEKIT_URL:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}/livekit}

  FLUXER_KLIPY_API_KEY: ${FLUXER_KLIPY_API_KEY:-}

  FLUXER_EMAIL_ENABLED: ${FLUXER_EMAIL_ENABLED:-false}
  FLUXER_EMAIL_PROVIDER: ${FLUXER_EMAIL_PROVIDER:-none}
  FLUXER_EMAIL_FROM_EMAIL: ${FLUXER_EMAIL_FROM_EMAIL:-noreply@localhost}
  FLUXER_EMAIL_FROM_NAME: ${FLUXER_EMAIL_FROM_NAME:-Fluxer}
  FLUXER_EMAIL_APP_BASE_URL: ${FLUXER_EMAIL_APP_BASE_URL:-}
  FLUXER_EMAIL_SMTP_HOST: ${FLUXER_EMAIL_SMTP_HOST:-}
  FLUXER_EMAIL_SMTP_PORT: ${FLUXER_EMAIL_SMTP_PORT:-587}
  FLUXER_EMAIL_SMTP_USERNAME: ${FLUXER_EMAIL_SMTP_USERNAME:-}
  FLUXER_EMAIL_SMTP_PASSWORD: ${FLUXER_EMAIL_SMTP_PASSWORD:-}
  FLUXER_EMAIL_SMTP_SECURE: ${FLUXER_EMAIL_SMTP_SECURE:-true}

  FLUXER_SMS_ENABLED: "${FLUXER_SMS_ENABLED:-false}"
  FLUXER_CAPTCHA_ENABLED: ${FLUXER_CAPTCHA_ENABLED:-false}
  FLUXER_CAPTCHA_PROVIDER: ${FLUXER_CAPTCHA_PROVIDER:-none}
  FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY: ${FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY:-}
  FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY: ${FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY:-}
  FLUXER_CAPTCHA_TURNSTILE_SITE_KEY: ${FLUXER_CAPTCHA_TURNSTILE_SITE_KEY:-}
  FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY: ${FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY:-}
  FLUXER_STRIPE_ENABLED: "${FLUXER_STRIPE_ENABLED:-false}"
  FLUXER_NCMEC_ENABLED: "${FLUXER_NCMEC_ENABLED:-false}"
  FLUXER_CLAMAV_ENABLED: "${FLUXER_CLAMAV_ENABLED:-false}"
  FLUXER_DISCOVERY_ENABLED: ${FLUXER_DISCOVERY_ENABLED:-true}

  FLUXER_SUDO_MODE_SECRET: ${FLUXER_SUDO_MODE_SECRET:?set FLUXER_SUDO_MODE_SECRET in .env}
  FLUXER_CONNECTION_INITIATION_SECRET: ${FLUXER_CONNECTION_INITIATION_SECRET:?set FLUXER_CONNECTION_INITIATION_SECRET in .env}
  FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES: ${FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES:-false}
  FLUXER_VAPID_PUBLIC_KEY: ${FLUXER_VAPID_PUBLIC_KEY:?set FLUXER_VAPID_PUBLIC_KEY in .env}
  FLUXER_VAPID_PRIVATE_KEY: ${FLUXER_VAPID_PRIVATE_KEY:?set FLUXER_VAPID_PRIVATE_KEY in .env}
  FLUXER_VAPID_EMAIL: ${FLUXER_VAPID_EMAIL:-admin@${FLUXER_DOMAIN}}
  FLUXER_PASSKEY_RP_ID: ${FLUXER_PASSKEY_RP_ID:-${FLUXER_DOMAIN}}
  FLUXER_PASSKEY_RP_NAME: ${FLUXER_PASSKEY_RP_NAME:-Fluxer}
  FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: ${FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
  FLUXER_GATEWAY_RPC_AUTH_TOKEN: ${FLUXER_GATEWAY_RPC_AUTH_TOKEN:?set FLUXER_GATEWAY_RPC_AUTH_TOKEN in .env}
  FLUXER_MEDIA_PROXY_SECRET_KEY: ${FLUXER_MEDIA_PROXY_SECRET_KEY:?set FLUXER_MEDIA_PROXY_SECRET_KEY in .env}
  FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64:?set FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 in .env}
  FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ${FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64:-}
  FLUXER_ADMIN_SECRET_KEY_BASE: ${FLUXER_ADMIN_SECRET_KEY_BASE:?set FLUXER_ADMIN_SECRET_KEY_BASE in .env}
  FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ${FLUXER_ADMIN_OAUTH_CLIENT_SECRET:?set FLUXER_ADMIN_OAUTH_CLIENT_SECRET in .env}

  FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
  FLUXER_INTERNAL_GATEWAY_ENDPOINT: http://gateway:8080
  FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
  FLUXER_MARKETING_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
  FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
  FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
  FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media

x-fluxer-service: &fluxer-service
  restart: unless-stopped
  networks: [fluxer]

x-fluxer-svc-healthcheck: &fluxer-svc-healthcheck
  test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8090 && printf 'GET /_health HTTP/1.0\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
  interval: 10s
  timeout: 5s
  retries: 30
  start_period: 60s
  start_interval: 1s

services:
  edge:
    image: caddy:2.11-alpine
    deploy:
      resources:
        limits:
          memory: ${FLUXER_CADDY_MEMORY_LIMIT:-256mb}
    restart: unless-stopped
    networks: [fluxer]
    ports:
      - "${FLUXER_HTTP_PORT:-80}:80"
      - "${FLUXER_HTTPS_PORT:-443}:443"
      - "${FLUXER_HTTPS_PORT:-443}:443/udp"
    environment:
      FLUXER_EDGE_SITE_ADDRESS: ${FLUXER_EDGE_SITE_ADDRESS:-${FLUXER_CADDY_SITE_ADDRESS:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}}}
      FLUXER_EDGE_TRUSTED_PROXIES: ${FLUXER_EDGE_TRUSTED_PROXIES:-private_ranges}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - edge-data:/data
      - edge-config:/config
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
      interval: 10s
      timeout: 5s
      retries: 10
    depends_on:
      api: {condition: service_started}
      gateway: {condition: service_healthy}
      media-proxy: {condition: service_started}
      static-proxy: {condition: service_started}
      admin: {condition: service_started}

  postgres:
    image: postgres:16-alpine
    deploy:
      resources:
        limits:
          memory: ${FLUXER_POSTGRES_MEMORY_LIMIT:-5gb}
        reservations:
          memory: ${FLUXER_POSTGRES_MEMORY_RESERVATION:-3gb}
    restart: unless-stopped
    networks: [fluxer]
    command: >
      postgres
      -c max_connections=${FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS:-150}
      -c shared_buffers=${FLUXER_POSTGRES_SHARED_BUFFERS:-512MB}
      -c effective_cache_size=${FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE:-2GB}
      -c work_mem=${FLUXER_POSTGRES_WORK_MEM:-8MB}
      -c maintenance_work_mem=${FLUXER_POSTGRES_MAINTENANCE_WORK_MEM:-256MB}
      -c autovacuum_work_mem=${FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM:-128MB}
      -c random_page_cost=1.1
      -c effective_io_concurrency=200
      -c default_statistics_target=200
      -c jit=off
      -c min_wal_size=512MB
      -c max_wal_size=2GB
      -c checkpoint_completion_target=0.9
      -c wal_buffers=16MB
      -c wal_compression=zstd
      -c bgwriter_delay=50ms
      -c bgwriter_lru_maxpages=1000
      -c autovacuum_vacuum_scale_factor=0.05
      -c autovacuum_analyze_scale_factor=0.02
      -c autovacuum_vacuum_cost_limit=2000
      -c track_io_timing=on
      -c shared_preload_libraries=pg_stat_statements
    shm_size: 256mb
    environment:
      POSTGRES_DB: fluxer
      POSTGRES_USER: fluxer
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fluxer -d fluxer"]
      interval: 10s
      timeout: 5s
      retries: 10

  valkey:
    image: valkey/valkey:9.1-alpine
    deploy:
      resources:
        limits:
          memory: ${FLUXER_VALKEY_MEMORY_LIMIT:-256mb}
    restart: unless-stopped
    networks: [fluxer]
    command: ["valkey-server", "--appendonly", "yes", "--appendfsync", "everysec", "--dir", "/data",
              "--maxmemory", "${FLUXER_VALKEY_MAXMEMORY:-192mb}",
              "--maxmemory-policy", "${FLUXER_VALKEY_MAXMEMORY_POLICY:-noeviction}"]
    volumes:
      - valkey-data:/data
    healthcheck:
      test: ["CMD", "valkey-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10

  nats:
    image: nats:2.14-alpine
    deploy:
      resources:
        limits:
          memory: ${FLUXER_NATS_MEMORY_LIMIT:-256mb}
    restart: unless-stopped
    networks: [fluxer]
    command: ["-js", "-sd", "/data", "-m", "8222"]
    volumes:
      - nats-data:/data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
      interval: 10s
      timeout: 5s
      retries: 10

  meilisearch:
    image: getmeili/meilisearch:v1.53
    deploy:
      resources:
        limits:
          memory: ${FLUXER_MEILISEARCH_MEMORY_LIMIT:-768mb}
    restart: unless-stopped
    networks: [fluxer]
    environment:
      MEILI_ENV: production
      MEILI_NO_ANALYTICS: "true"
      MEILI_UPGRADE_DB: "true"
      MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-384mb}
      MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
    volumes:
      - meilisearch-data:/meili_data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:7700/health"]
      interval: 10s
      timeout: 5s
      retries: 10

  seaweedfs:
    image: chrislusf/seaweedfs:4.47
    deploy:
      resources:
        limits:
          memory: ${FLUXER_SEAWEEDFS_MEMORY_LIMIT:-2gb}
    restart: unless-stopped
    networks: [fluxer]
    environment:
      GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
    command: ["server", "-s3", "-dir=/data", "-master.telemetry=false"]
    volumes:
      - seaweedfs-data:/data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8333/healthz"]
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 60s

  seaweedfs-init:
    image: chrislusf/seaweedfs:4.47
    deploy:
      resources:
        limits:
          memory: ${FLUXER_SEAWEEDFS_INIT_MEMORY_LIMIT:-128mb}
    networks: [fluxer]
    depends_on:
      seaweedfs: {condition: service_healthy}
    restart: "no"
    environment:
      FLUXER_S3_ACCESS_KEY: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
      FLUXER_S3_SECRET_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
      FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
      FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
      FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
      FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
      FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
    entrypoint:
      - /bin/sh
      - -c
      - >
        buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_DOWNLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
        missing="$$buckets";
        for attempt in $$(seq 1 60); do
          if ! nc -z seaweedfs 9333 2>/dev/null; then
            sleep 2;
            continue;
          fi;
          listed=$$(echo "s3.bucket.list" | timeout 10 weed shell -master=seaweedfs:9333 2>&1);
          missing="";
          for b in $$buckets; do
            echo "$$listed" | grep -q "^[[:space:]]*$$b[[:space:]]" || missing="$${missing:+$$missing }$$b";
          done;
          if [ -z "$$missing" ]; then
            if ! echo "s3.configure -user=fluxer -access_key=$$FLUXER_S3_ACCESS_KEY -secret_key=$$FLUXER_S3_SECRET_KEY -actions=Admin,Read,Write,List,Tagging -apply" | timeout 10 weed shell -master=seaweedfs:9333 >/dev/null 2>&1; then
              echo "seaweedfs-init could not configure the S3 identity" >&2;
              exit 1;
            fi;
            echo "buckets ready";
            exit 0;
          fi;
          for b in $$missing; do
            echo "s3.bucket.create -name $$b" | timeout 10 weed shell -master=seaweedfs:9333 >/dev/null 2>&1;
          done;
          sleep 2;
        done;
        echo "seaweedfs-init could not verify buckets: $$missing" >&2;
        exit 1;

  livekit:
    image: livekit/livekit-server:v1.12.0
    deploy:
      resources:
        limits:
          memory: ${FLUXER_LIVEKIT_MEMORY_LIMIT:-512mb}
    restart: unless-stopped
    networks: [fluxer]
    environment:
      LIVEKIT_KEYS: "${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}: ${LIVEKIT_API_SECRET:?set LIVEKIT_API_SECRET in .env}"
      LIVEKIT_CONFIG: |
        port: 7880
        log_level: info
        rtc:
          tcp_port: ${FLUXER_LIVEKIT_TCP_PORT:-7881}
          udp_port: ${FLUXER_LIVEKIT_UDP_PORT:-7882}
          use_external_ip: ${FLUXER_LIVEKIT_USE_EXTERNAL_IP:-true}
          node_ip: "${FLUXER_LIVEKIT_NODE_IP:-}"
          stun_servers:
            - ${FLUXER_LIVEKIT_STUN_PRIMARY:-stun.l.google.com:19302}
            - ${FLUXER_LIVEKIT_STUN_SECONDARY:-stun1.l.google.com:19302}
        webhook:
          api_key: ${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}
          urls:
            - http://api:8080/webhooks/livekit
    ports:
      - "${FLUXER_LIVEKIT_TCP_PORT:-7881}:${FLUXER_LIVEKIT_TCP_PORT:-7881}"
      - "${FLUXER_LIVEKIT_UDP_PORT:-7882}:${FLUXER_LIVEKIT_UDP_PORT:-7882}/udp"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:7880/"]
      interval: 10s
      timeout: 5s
      retries: 10

  api:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-api:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_API_MEMORY_LIMIT:-2560mb}
        reservations:
          memory: ${FLUXER_API_MEMORY_RESERVATION:-1gb}
    environment:
      <<: *fluxer-env
      FLUXER_API_PORT: "8080"
      NODE_OPTIONS: --enable-source-maps${FLUXER_API_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_API_NODE_HEAP_MB}
      FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED: "true"
      FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
    healthcheck:
      test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:8080/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 90s
      start_interval: 1s
    depends_on:
      postgres: {condition: service_healthy}
      valkey: {condition: service_healthy}
      nats: {condition: service_healthy}
      meilisearch: {condition: service_healthy}
      seaweedfs-init: {condition: service_completed_successfully}
      gifs: {condition: service_healthy}
      gifs-shard: {condition: service_healthy}
      snowflakes: {condition: service_healthy}
      snowflakes-shard: {condition: service_healthy}
      messages: {condition: service_healthy}
      messages-shard: {condition: service_healthy}
      users: {condition: service_healthy}
      users-shard: {condition: service_healthy}

  worker:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-api:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_WORKER_MEMORY_LIMIT:-2560mb}
        reservations:
          memory: ${FLUXER_WORKER_MEMORY_RESERVATION:-1gb}
    working_dir: /usr/src/app/fluxer_api
    command: ["sh", "-c", "if [ -f dist/WorkerEntrypoint.js ]; then exec node dist/WorkerEntrypoint.js; else exec ./node_modules/.bin/tsx src/WorkerEntrypoint.ts; fi"]
    environment:
      <<: *fluxer-env
      NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}
      FLUXER_API_WORKER_MODE: all_lanes
      FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
      FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
    healthcheck:
      test: ["CMD", "node", "-e", "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 90s
      start_interval: 1s
    depends_on:
      postgres: {condition: service_healthy}
      valkey: {condition: service_healthy}
      nats: {condition: service_healthy}
      seaweedfs-init: {condition: service_completed_successfully}
      snowflakes-shard: {condition: service_healthy}
      messages-shard: {condition: service_healthy}
      users-shard: {condition: service_healthy}

  gateway:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-gateway:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_GATEWAY_MEMORY_LIMIT:-1gb}
        reservations:
          memory: ${FLUXER_GATEWAY_MEMORY_RESERVATION:-384mb}
    environment:
      <<: *fluxer-env
      FLUXER_GATEWAY_PORT: "8080"
      FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
      FLUXER_GATEWAY_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
      FLUXER_GATEWAY_LOGGER_LEVEL: info
      FLUXER_ERLANG_COOKIE: ${FLUXER_ERLANG_COOKIE:?set FLUXER_ERLANG_COOKIE in .env}
      FLUXER_ERLANG_SCHEDULERS_MIN: "${FLUXER_ERLANG_SCHEDULERS_MIN:-2}"
      FLUXER_ERLANG_SCHEDULERS_MAX: "${FLUXER_ERLANG_SCHEDULERS_MAX:-16}"
    healthcheck:
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/_health/ready"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 90s
    depends_on:
      nats: {condition: service_healthy}
      valkey: {condition: service_healthy}

  media-proxy:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-media-proxy:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_MEDIA_PROXY_MEMORY_LIMIT:-512mb}
    environment:
      <<: *fluxer-env
      FLUXER_MEDIA_PROXY_HOST: 0.0.0.0
      FLUXER_MEDIA_PROXY_PORT: "8080"
      FLUXER_MEDIA_PROXY_MODE: upload
      FLUXER_MEDIA_PROXY_STORAGE_BACKEND: s3
      FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
      FLUXER_MEDIA_PROXY_CORS_MODE: ${FLUXER_MEDIA_PROXY_CORS_MODE:-off}
      FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS: ${FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}}
      FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE: ${FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE:-off}
      FLUXER_S3_READ_SIGNED: "true"
    depends_on:
      seaweedfs-init: {condition: service_completed_successfully}
      nats: {condition: service_healthy}

  static-proxy:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-static:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_STATIC_PROXY_MEMORY_LIMIT:-256mb}
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/avatars/0.png"]
      interval: 10s
      timeout: 5s
      retries: 10

  app-proxy:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-app-proxy-self-hosted:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_APP_PROXY_MEMORY_LIMIT:-256mb}
    environment:
      FLUXER_APP_PROXY_HOST: 0.0.0.0
      FLUXER_APP_PROXY_PORT: "8080"
      FLUXER_BASE_DOMAIN: ${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}
      FLUXER_PUBLIC_SCHEME: ${FLUXER_PUBLIC_SCHEME:-https}
      FLUXER_PUBLIC_PORT: ${FLUXER_PUBLIC_PORT:-443}
      FLUXER_PUBLIC_ORIGIN: ${FLUXER_PUBLIC_ORIGIN:-}
      DISCOVERY_UPSTREAM_URL: http://edge:8088/.well-known/fluxer
      PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
      PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/api
      FLUXER_CSP_EXTRA_DEFAULT_SRC: ${FLUXER_CSP_EXTRA_DEFAULT_SRC:-}
      FLUXER_CSP_EXTRA_CONNECT_SRC: ${FLUXER_CSP_EXTRA_CONNECT_SRC:-}
      FLUXER_CSP_EXTRA_IMG_SRC: ${FLUXER_CSP_EXTRA_IMG_SRC:-}
      FLUXER_CSP_EXTRA_MEDIA_SRC: ${FLUXER_CSP_EXTRA_MEDIA_SRC:-}
      FLUXER_CSP_EXTRA_FONT_SRC: ${FLUXER_CSP_EXTRA_FONT_SRC:-}
      FLUXER_CSP_EXTRA_SCRIPT_SRC: ${FLUXER_CSP_EXTRA_SCRIPT_SRC:-}
      FLUXER_CSP_EXTRA_STYLE_SRC: ${FLUXER_CSP_EXTRA_STYLE_SRC:-}
      FLUXER_CSP_EXTRA_FRAME_SRC: ${FLUXER_CSP_EXTRA_FRAME_SRC:-}
      FLUXER_CSP_EXTRA_WORKER_SRC: ${FLUXER_CSP_EXTRA_WORKER_SRC:-}
      FLUXER_CSP_EXTRA_MANIFEST_SRC: ${FLUXER_CSP_EXTRA_MANIFEST_SRC:-}
      FLUXER_CSP_REPORT_URI: ${FLUXER_CSP_REPORT_URI:-}
    depends_on:
      api: {condition: service_healthy}
      edge: {condition: service_healthy}

  snowflakes:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-snowflakes:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_SNOWFLAKES_MEMORY_LIMIT:-128mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: snowflakes
      FLUXER_SVC_MODE: router
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  snowflakes-shard:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-snowflakes:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_SNOWFLAKES_SHARD_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: snowflakes
      FLUXER_SVC_MODE: shard
      FLUXER_SVC_SHARD_ID: "0"
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  users:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-users:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_USERS_MEMORY_LIMIT:-128mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: users
      FLUXER_SVC_MODE: router
      FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  users-shard:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-users:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_USERS_SHARD_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: users
      FLUXER_SVC_MODE: shard
      FLUXER_SVC_SHARD_ID: "0"
      FLUXER_POSTGRES_MAX_CONNECTIONS: "20"
      FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}
      postgres: {condition: service_healthy}

  gifs:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-gifs:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_GIFS_MEMORY_LIMIT:-128mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: gifs
      FLUXER_SVC_MODE: router
      FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  gifs-shard:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-gifs:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_GIFS_SHARD_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: gifs
      FLUXER_SVC_MODE: shard
      FLUXER_SVC_SHARD_ID: "0"
      FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  messages:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-messages:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_MESSAGES_MEMORY_LIMIT:-128mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: messages
      FLUXER_SVC_MODE: router
      FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  messages-shard:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-messages:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_MESSAGES_SHARD_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: messages
      FLUXER_SVC_MODE: shard
      FLUXER_SVC_SHARD_ID: "0"
      FLUXER_POSTGRES_MAX_CONNECTIONS: "20"
      FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}
      postgres: {condition: service_healthy}

  unfurl:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-unfurl:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_UNFURL_MEMORY_LIMIT:-128mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: unfurl
      FLUXER_SVC_MODE: router
      FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
      FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  unfurl-shard:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-unfurl:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_UNFURL_SHARD_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_SVC_NAME: unfurl
      FLUXER_SVC_MODE: shard
      FLUXER_SVC_SHARD_ID: "0"
      FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
      FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
    healthcheck: *fluxer-svc-healthcheck
    depends_on:
      nats: {condition: service_healthy}

  admin:
    <<: *fluxer-service
    image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-admin:${FLUXER_IMAGE_TAG:-v1}
    deploy:
      resources:
        limits:
          memory: ${FLUXER_ADMIN_MEMORY_LIMIT:-256mb}
    environment:
      <<: *fluxer-env
      FLUXER_ADMIN_HOST: 0.0.0.0
      FLUXER_ADMIN_PORT: "8080"
      FLUXER_ADMIN_BASE_PATH: /admin
      FLUXER_API_ENDPOINT: http://api:8080
      FLUXER_ADMIN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/admin
      FLUXER_APP_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
      FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
      FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
      FLUXER_ADMIN_OAUTH_REDIRECT_URI: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/admin/oauth2_callback
    healthcheck:
      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /_health HTTP/1.0\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
      interval: 10s
      timeout: 5s
      retries: 30
      start_period: 60s
      start_interval: 1s
    depends_on:
      api: {condition: service_healthy}

networks:
  fluxer:
    driver: bridge

volumes:
  edge-data:
  edge-config:
  postgres-data:
  valkey-data:
  nats-data:
  meilisearch-data:
  seaweedfs-data:

Start by preparing the environment:

cd assets/snippets/fluxer
cp .env.sample .env
$EDITOR .env

Generate secrets:

openssl rand -hex 32
openssl rand -base64 32

You also need VAPID keys for web push.

Then start the stack:

docker compose up -d

The default mode expects Fluxer to bind public HTTP/HTTPS ports and obtain certificates through Caddy. For most home-lab users, I would put it behind an existing reverse proxy or a tunnel instead.

Reverse Proxy Mode

For nginx, Traefik, HAProxy, or another Caddy instance in front:

COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml
FLUXER_EDGE_BIND=127.0.0.1:8080
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443

Your proxy should forward the whole public hostname to:

http://127.0.0.1:8080

Do not route only / to the app. Fluxer also needs /api, /gateway, /media, /livekit, /admin, static asset paths, and /.well-known/fluxer.

Cloudflare Tunnel Mode

For Cloudflare Tunnel:

COMPOSE_FILE=docker-compose.yml:tunnel.compose.yml
FLUXER_HTTP_PORT=127.0.0.1:8080
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443

Point the tunnel service at:

http://127.0.0.1:8080

That is the deployment mode that maps cleanly to Cloudflare without pretending this is a static Cloudflare Pages app.

Field Test

I validated the Docker Compose files with the sample environment:

docker compose --env-file .env.sample config >/tmp/fluxer-compose-config.yml
docker compose --env-file .env.sample -f docker-compose.yml -f docker-compose.proxy.yml config >/tmp/fluxer-compose-proxy-config.yml
docker compose --env-file .env.sample -f docker-compose.yml -f tunnel.compose.yml config >/tmp/fluxer-compose-tunnel-config.yml

All three rendered successfully.

I also checked OCI manifests for the default v1 images used by the stack:

ghcr.io/fluxerapp/fluxer-api:v1
ghcr.io/fluxerapp/fluxer-app-proxy-self-hosted:v1
ghcr.io/fluxerapp/fluxer-gateway:v1
ghcr.io/fluxerapp/fluxer-media-proxy:v1

I did not start the complete stack in this pass. It is a large production-style system with many services and required generated secrets, so the validation here is configuration rendering plus image availability, not a claim that I completed an end-to-end chat and voice test.

Operational Notes

Back up the volumes before upgrades:

  • Postgres;
  • Valkey;
  • NATS JetStream;
  • Meilisearch;
  • SeaweedFS;
  • the .env file.

The Valkey point is worth calling out because this deployment treats it as more than disposable cache for some durable queue state.

For production use, also plan:

  • outbound email if you want account email flows;
  • captcha if you expose public registration;
  • public HTTPS with correct forwarded client IP handling;
  • LiveKit TCP/UDP reachability for voice;
  • object storage capacity and backup policy;
  • monitoring on API, gateway, worker, and media services.

FAQ

Conclusion

Fluxer is one of the more serious open-source attempts at a modern community chat and VoIP stack.

It has the pieces self-hosters usually ask about: browser app, installable PWA behavior, desktop packaging, voice, media, admin, and a Docker Compose deployment.

The honest caveat is that it is infrastructure. If you want a tiny chat app, Fluxer is too much. If you want to run a Discord-like open chat system and are comfortable operating a multi-service stack, it is worth studying.