# Fluxer — Self-Hosting & VM Rebuild Guide This document covers how to spin up the Fluxer dev instance from scratch on a new VM. The running reference deployment is at `st.vish.gg` (seattle VM, `66.94.122.170` / `100.82.197.124`). --- ## Architecture overview All services run as Docker containers via `dev/compose.yaml`, connected on a shared Docker network (`fluxer-shared`). Caddy acts as the internal reverse proxy, routing all traffic on port `8088` to the appropriate service. Cloudflared tunnels external HTTPS traffic (no open inbound ports required). ``` Internet └── Cloudflare Tunnel (cloudflared) └── Caddy :8088 ├── /api/* → api:8080 (Node.js REST API) ├── /gateway/* → gateway:8080 (Erlang WebSocket gateway) ├── /media/* → media:8080 (media proxy) ├── /admin/* → admin:8080 (Elixir admin panel) ├── /marketing/* → marketing:8080 (Elixir marketing site) ├── /metrics/* → metrics:8080 (Rust metrics service) ├── /livekit/* → livekit:7880 (LiveKit voice/video) ├── /s3/* → minio:9000 (S3-compatible object storage) └── /* → /app/dist (built frontend SPA) ``` ### Services | Container | Image/Build | Purpose | |-----------|-------------|---------| | `caddy` | `caddy:2` | Internal reverse proxy | | `cloudflared` | `cloudflare/cloudflared` | Tunnel (no inbound ports) | | `api` | `node:24-bookworm-slim` | REST API (`tsx watch`) | | `worker` | `node:24-bookworm-slim` | Background job worker | | `gateway` | `erlang:28-slim` | WebSocket gateway (compiled with rebar3) | | `media` | build from `fluxer_media_proxy/` | Media proxy | | `admin` | build from `fluxer_admin/` | Admin panel | | `marketing` | build from `fluxer_marketing/` | Marketing site | | `docs` | `node:24-bookworm-slim` | Docs site | | `metrics` | build from `fluxer_metrics/` | Metrics aggregator | | `postgres` | `postgres:17` | Primary database | | `cassandra` | `scylladb/scylla:latest` | Time-series / message data | | `redis` | `valkey/valkey:latest` | Cache / pub-sub | | `minio` | `minio/minio` | S3-compatible object storage | | `meilisearch` | `getmeili/meilisearch:v1.25.0` | Full-text search | | `clamav` | `clamav/clamav:latest` | Virus scanning (can be disabled) | | `livekit` | `livekit/livekit-server:latest` | Voice & video | | `cassandra-migrate` | `debian:bookworm-slim` | One-shot DB migration runner | --- ## Prerequisites - Debian/Ubuntu VM (tested on Debian 12 bookworm) - Docker + Docker Compose plugin installed - `git` installed - A Cloudflare tunnel token (from Cloudflare Zero Trust dashboard) - Domain pointed at Cloudflare (e.g. `st.vish.gg`) - At least 4 vCPU / 8GB RAM recommended (ScyllaDB + ClamAV are hungry) ### Install Docker (if not present) ```bash curl -fsSL https://get.docker.com | sh systemctl enable --now docker ``` --- ## Step-by-step rebuild ### 1. Clone the repo ```bash git clone https://github.com/fluxerapp/fluxer /root/fluxer cd /root/fluxer ``` ### 2. Create the shared Docker network Must be created before starting any containers — it's declared `external: true` in the compose file. ```bash docker network create fluxer-shared ``` ### 3. Build the frontend app Caddy serves the pre-built SPA from `fluxer_app/dist/`. This must be built before starting Caddy, otherwise it will mount an empty directory and serve nothing. ```bash # Install pnpm corepack enable pnpm # Install workspace deps pnpm install # Build the app (outputs to fluxer_app/dist/) cd fluxer_app pnpm build cd .. ``` > Note: The build requires Node 22+ and pnpm. If the VM doesn't have Node installed globally, > run the build inside a container: > ```bash > docker run --rm -v $(pwd):/workspace -w /workspace/fluxer_app node:24-bookworm-slim \ > bash -lc "corepack enable pnpm && CI=true pnpm install && pnpm build" > ``` ### 4. Build the cassandra-migrate binary The Cassandra migration runner is a Rust binary that must be pre-compiled. ```bash # Install Rust if needed curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env # Build cd scripts/cassandra-migrate cargo build --release cd ../.. ``` The compose file expects the binary at `scripts/cassandra-migrate/target/release/cassandra-migrate`. ### 5. Set up the environment file ```bash cp dev/.env.example dev/.env ``` Edit `dev/.env` and fill in: | Variable | What to set | |----------|-------------| | `FLUXER_APP_ENDPOINT` | Your public URL e.g. `https://st.vish.gg` | | `FLUXER_API_PUBLIC_ENDPOINT` | `https://st.vish.gg/api` | | `FLUXER_GATEWAY_ENDPOINT` | `wss://st.vish.gg/gateway` | | `FLUXER_MARKETING_ENDPOINT` | `https://st.vish.gg` | | `FLUXER_ADMIN_ENDPOINT` | `https://st.vish.gg/admin` | | `FLUXER_MEDIA_ENDPOINT` | `https://st.vish.gg/media` | | `FLUXER_INVITE_ENDPOINT` | `https://st.vish.gg` | | `CLOUDFLARE_TUNNEL_TOKEN` | Token from Cloudflare Zero Trust dashboard | | `SECRET_KEY_BASE` | Generate: `openssl rand -hex 64` | | `GATEWAY_RPC_SECRET` | Generate: `openssl rand -hex 32` | | `GATEWAY_ADMIN_SECRET` | Generate: `openssl rand -hex 32` | | `MEDIA_PROXY_SECRET_KEY` | Generate: `openssl rand -hex 32` | | `SUDO_MODE_SECRET` | Generate: `openssl rand -hex 32` | | `LIVEKIT_API_KEY` | Generate: `openssl rand -hex 16` | | `LIVEKIT_API_SECRET` | Generate: `openssl rand -hex 32` | | `VAPID_PUBLIC_KEY` | Generate with web-push: `npx web-push generate-vapid-keys` | | `VAPID_PRIVATE_KEY` | (paired with above) | | `ADMIN_OAUTH2_CLIENT_ID` | OAuth2 app ID (from Fluxer admin panel after first boot) | | `ADMIN_OAUTH2_CLIENT_SECRET` | OAuth2 secret | | `PASSKEY_RP_ID` | Your domain e.g. `st.vish.gg` | | `PASSKEY_ALLOWED_ORIGINS` | `https://st.vish.gg` | Everything else can stay at its default dev value on first boot. ### 6. Set up the LiveKit config ```bash cp dev/livekit.yaml dev/livekit.yaml ``` Edit `dev/livekit.yaml` — replace the API key/secret with the values you set in `.env`: ```yaml keys: "": "" webhook: api_key: "" urls: - "http://api:8080/webhooks/livekit" ``` ### 7. Start the stack ```bash cd /root/fluxer/dev docker compose up -d ``` On first boot, `cassandra-migrate` will run automatically and apply schema migrations. ScyllaDB takes ~90 seconds to become healthy before migrations run. Watch progress: ```bash docker compose logs -f cassandra-migrate docker compose logs -f api ``` ### 8. Verify everything is up ```bash docker compose ps ``` Expected running containers: `caddy`, `cloudflared`, `api`, `worker`, `gateway`, `media`, `admin`, `marketing`, `docs`, `metrics`, `clamav`, `meilisearch`, `redis`, `minio`, `livekit` Expected exited (one-shot): `cassandra-migrate`, `minio-setup` The app should be accessible at your configured domain via the Cloudflare tunnel. --- ## Managing the stack ### Start / stop ```bash cd /root/fluxer/dev # Start all docker compose up -d # Stop all (keep volumes) docker compose down # Stop and wipe all data docker compose down -v ``` ### View logs ```bash # All services docker compose logs -f # Specific service docker compose logs -f api docker compose logs -f gateway ``` ### Rebuild a specific service after code changes ```bash # Rebuild and restart one service docker compose up -d --build media # Rebuild frontend app and restart caddy cd /root/fluxer pnpm --filter fluxer_app build docker compose -f dev/compose.yaml restart caddy ``` ### Update from upstream ```bash cd /root/fluxer git pull origin main # Rebuild frontend pnpm install pnpm --filter fluxer_app build # Restart services that auto-install deps (api, worker, docs) docker compose -f dev/compose.yaml restart api worker docs # Rebuild services with Dockerfiles docker compose -f dev/compose.yaml up -d --build media admin marketing metrics ``` --- ## Configuration reference ### Key files | File | Purpose | |------|---------| | `dev/compose.yaml` | Main docker compose for the dev stack | | `dev/.env` | All environment variables (not committed — see `.env.example`) | | `dev/Caddyfile.dev` | Caddy reverse proxy routing config | | `dev/livekit.yaml` | LiveKit server config (API keys, webhook URL) | ### Port mappings (host) | Port | Service | |------|---------| | `8088` | Caddy (all traffic enters here, then routed internally) | | `7880` | LiveKit HTTP | | `7882/udp` | LiveKit RTC | | `7999/udp` | LiveKit RTC | | `9042` | ScyllaDB CQL (direct access) | | `8123` | ClickHouse (only with `--profile clickhouse`) | All other services communicate internally over `fluxer-shared` Docker network. ### Optional profiles ```bash # Enable ClickHouse-backed metrics docker compose --profile clickhouse up -d # Enable search (meilisearch is included by default in dev) docker compose --profile search up -d ``` --- ## Cloudflare tunnel setup 1. Go to Cloudflare Zero Trust → Networks → Tunnels → Create tunnel 2. Name it (e.g. `fluxer-seattle`) 3. Copy the tunnel token → set as `CLOUDFLARE_TUNNEL_TOKEN` in `dev/.env` 4. Add a public hostname: - Subdomain: `st` (or your chosen subdomain) - Domain: `vish.gg` - Service: `http://localhost:8088` 5. The `cloudflared` container will connect automatically on `docker compose up` --- ## Troubleshooting ### Gateway won't start The Erlang gateway compiles from source on first start (`rebar3 compile`) — this takes 2-5 minutes. Watch with `docker compose logs -f gateway`. If it exits, check for missing deps or compilation errors. ### ScyllaDB/Cassandra migration fails ScyllaDB needs ~90 seconds to fully initialise. The `cassandra-migrate` container has a `sleep 30` but on slow machines it may still fail. Restart it manually: ```bash docker compose run --rm cassandra-migrate ``` ### API exits immediately Almost always a missing or malformed `.env` value. Check: ```bash docker compose logs api | head -30 ``` Common causes: `DATABASE_URL` unreachable, missing `SECRET_KEY_BASE`, malformed `REDIS_URL`. ### Frontend shows blank page The `fluxer_app/dist/` directory is empty — the app hasn't been built yet. Run the build step (Step 3 above) then restart Caddy: ```bash docker compose restart caddy ``` ### ClamAV takes forever to start ClamAV downloads its virus definition database on first start (~500MB). This is normal. It won't be marked healthy until the download completes (~5-10 min on first boot). ClamAV can be disabled by setting `CLAMAV_ENABLED=false` in `.env` — the other services don't depend on it. --- ## Seattle reference deployment The running instance at `st.vish.gg` uses this exact setup. Key details: | Property | Value | |----------|-------| | Host | Seattle VM (`66.94.122.170` / Tailscale `100.82.197.124`) | | SSH | `ssh root@seattle` or `ssh root@seattle-tailscale` | | Install path | `/root/fluxer/` | | Compose path | `/root/fluxer/dev/` | | Domain | `st.vish.gg` | | Tunnel | Cloudflare Zero Trust (`cloudflared`) | | `AUTO_JOIN_INVITE_CODE` | `FRIENDS` (anyone with this code can register) | To check status on seattle: ```bash ssh seattle-tailscale "cd /root/fluxer/dev && docker compose ps" ssh seattle-tailscale "cd /root/fluxer/dev && docker compose logs --tail 20 api" ```