Self-Hosting Guide

Deploy your own Fluxer server with Docker, configure it with JSON, and put it behind Caddy as a reverse proxy.

Heads up: Self-hosting support is provided through GitHub Issues only. Support requests sent through the Fluxer World app will not be answered — the app is for chatting with your community, not a helpdesk!

Contents

  1. Prerequisites
  2. Ports & Network
  3. Directory Structure
  4. Configuration (config.json)
  5. Docker Compose Setup
  6. Caddy Reverse Proxy
  7. Starting the Server
  8. Updating
  9. Troubleshooting

1. Prerequisites

Before starting, make sure your server meets these requirements. If any of these are missing, the setup will not work.

Minimum hardware: 2 CPU cores, 2 GB RAM, 20 GB disk. A VPS from any provider works (Contabo, Hetzner, DigitalOcean, etc). Home networks and Windows/macOS are not supported.

Note: Caddy automatically provisions and renews TLS certificates via Let's Encrypt. No manual certificate setup is needed.

System Requirements

Fluxerworld runs as one app container plus two small dependencies (Valkey + NATS). It is RAM-bound — the app process is the floor. Approximate sizing for a single-instance, SQLite-backed deployment:

Tier RAM CPU Disk Good for
Minimum 2 GB 1 vCPU ~10 GB A small community (SQLite, optional search)
Comfortable 4 GB 2 vCPU 20 GB+ Headroom for search, voice, and growth

1 GB will not work. The application process alone uses ~1 GB of RAM, so a 1 GB server runs out of memory on startup. 2 GB is a hard floor.

Install Docker

# Install Docker (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# Verify installation
docker --version
docker compose version

Install Caddy

# Install Caddy (Ubuntu/Debian)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

2. Ports & Network

Understanding which ports are used and how traffic flows is important before deploying.

Required Ports

Port Protocol Service Exposure
80 TCP Caddy (HTTP) Public — redirects to HTTPS
443 TCP Caddy (HTTPS) Public — serves all external traffic
8080 TCP Fluxer API server Localhost only (127.0.0.1)

Traffic Flow

All external traffic goes through Caddy. The Fluxer server is never exposed directly to the internet.

User (browser/client)
  │
  │  HTTPS (:443)
  ▼
Caddy (reverse proxy)
  │
  │  ── HTTP redirect (:80 → :443)
  │
  │  HTTP (:8080, localhost only)
  ▼
Fluxer Server (Docker)
  │
  │  Internal Docker network
  ▼
Valkey · NATS · SQLite

Redirects

Caddy handles the following redirects automatically:

Optional www redirect in your Caddyfile:

www.fluxer.example.com {
    redir https://fluxer.example.com{uri} permanent
}

Firewall Setup

If you use ufw, open only the ports Caddy needs:

# Allow HTTP (for ACME challenges and redirect)
sudo ufw allow 80/tcp

# Allow HTTPS
sudo ufw allow 443/tcp

# Allow SSH (don't lock yourself out)
sudo ufw allow 22/tcp

# Enable firewall
sudo ufw enable

# Verify
sudo ufw status

Do not open port 8080 in your firewall. The Fluxer server binds to 127.0.0.1:8080 and should only be accessible through the Caddy reverse proxy. Exposing it directly bypasses TLS and security headers.

3. Directory Structure

Create a directory for your Fluxer deployment:

mkdir -p /opt/fluxer/{config,data}
cd /opt/fluxer

Your final directory structure will look like this:

/opt/fluxer/
├── docker-compose.yml
├── config/
│   └── config.json
└── data/            # created/managed by the container (sqlite db, s3, queue)

4. Configuration (config.json)

The main configuration file controls how your Fluxer server behaves. Create it at /opt/fluxer/config/config.json.

Minimal Configuration

A basic configuration to get started:

config/config.json
{
  "env": "production",
  "domain": { "base_domain": "fluxer.example.com", "public_scheme": "https", "public_port": 443 },
  "database": { "backend": "sqlite", "sqlite_path": "/usr/src/app/data/fluxer.db" },
  "internal": { "kv": "redis://valkey:6379/0", "kv_mode": "standalone" },
  "s3": { "access_key_id": "fluxer", "secret_access_key": "CHANGE_ME", "endpoint": "http://127.0.0.1:8080/s3" },
  "services": {
    "server": { "host": "0.0.0.0", "port": 8080 },
    "s3": { "data_dir": "/usr/src/app/data/s3" },
    "queue": { "data_dir": "/usr/src/app/data/queue" },
    "media_proxy": { "secret_key": "CHANGE_ME" },
    "admin": { "secret_key_base": "CHANGE_ME", "oauth_client_secret": "CHANGE_ME" },
    "gateway": { "port": 8082, "admin_reload_secret": "CHANGE_ME", "media_proxy_endpoint": "http://127.0.0.1:8080/media" },
    "nats": { "core_url": "nats://nats:4222", "jetstream_url": "nats://nats:4222" }
  },
  "auth": {
    "sudo_mode_secret": "CHANGE_ME",
    "connection_initiation_secret": "CHANGE_ME",
    "vapid": { "public_key": "CHANGE_ME", "private_key": "CHANGE_ME" }
  },
  "instance": { "self_hosted": true }
}

Secrets: Generate every CHANGE_ME value with openssl rand -hex 32. The auth.vapid pair must come from a web-push keypair generator (it is a public/private keypair, not a random string). The services.admin secrets are required even if you never open the admin panel — the server will refuse to start without them.

Full Configuration Reference

The same base configuration with an optional integrations.email block added for transactional email (verification, password resets). Leave enabled set to false if you don't have an SMTP server:

config/config.json
{
  "env": "production",
  "domain": { "base_domain": "fluxer.example.com", "public_scheme": "https", "public_port": 443 },
  "database": { "backend": "sqlite", "sqlite_path": "/usr/src/app/data/fluxer.db" },
  "internal": { "kv": "redis://valkey:6379/0", "kv_mode": "standalone" },
  "s3": { "access_key_id": "fluxer", "secret_access_key": "CHANGE_ME", "endpoint": "http://127.0.0.1:8080/s3" },
  "services": {
    "server": { "host": "0.0.0.0", "port": 8080 },
    "s3": { "data_dir": "/usr/src/app/data/s3" },
    "queue": { "data_dir": "/usr/src/app/data/queue" },
    "media_proxy": { "secret_key": "CHANGE_ME" },
    "admin": { "secret_key_base": "CHANGE_ME", "oauth_client_secret": "CHANGE_ME" },
    "gateway": { "port": 8082, "admin_reload_secret": "CHANGE_ME", "media_proxy_endpoint": "http://127.0.0.1:8080/media" },
    "nats": { "core_url": "nats://nats:4222", "jetstream_url": "nats://nats:4222" }
  },
  "auth": {
    "sudo_mode_secret": "CHANGE_ME",
    "connection_initiation_secret": "CHANGE_ME",
    "vapid": { "public_key": "CHANGE_ME", "private_key": "CHANGE_ME" }
  },
  "integrations": {
    "email": {
      "enabled": false,
      "provider": "smtp",
      "from_email": "noreply@example.com",
      "from_name": "Fluxer",
      "smtp": { "host": "smtp.example.com", "port": 587, "username": "noreply@example.com", "password": "CHANGE_ME", "secure": false }
    }
  },
  "instance": { "self_hosted": true }
}

Configuration Options

Public URL — The server derives its public URL from the domain block: domain.base_domain, domain.public_scheme, and domain.public_port. These must match the domain you configure in Caddy, and are used for generating invite links, email links, and API responses.

All other options — search, voice, captcha, payments and every remaining key are documented in the schema. See packages/config/src/ConfigSchema.json in the repository for the full, authoritative list of fields and their defaults.

Security: If you enable email, set integrations.email.enabled to true and fill in the integrations.email.smtp section with valid SMTP credentials. Without it, users won't be able to verify their accounts or reset passwords.

5. Docker Compose Setup

Create a docker-compose.yml in /opt/fluxer/:

docker-compose.yml
services:
  fluxer_server:
    image: ghcr.io/fluxerworld/fluxer-server:stable
    container_name: fluxer_server
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - FLUXER_CONFIG=/usr/src/app/config/config.json
      - NODE_ENV=production
    volumes:
      - ./config:/usr/src/app/config:ro
      - ./data:/usr/src/app/data
    depends_on:
      valkey:
        condition: service_healthy
      nats:
        condition: service_healthy

  valkey:
    image: valkey/valkey:8-alpine
    container_name: valkey
    restart: unless-stopped
    volumes:
      - valkey_data:/data
    healthcheck:
      test: ["CMD", "valkey-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  nats:
    image: nats:2-alpine
    container_name: nats
    restart: unless-stopped
    command: ["--jetstream", "--store_dir", "/data", "-m", "8222"]
    volumes:
      - nats_data:/data
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8222/healthz || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  valkey_data:
  nats_data:

Why 127.0.0.1:8080? — Binding to 127.0.0.1 ensures the API port is only accessible locally. Caddy will handle all external traffic and TLS termination. The valkey and nats services are not published to the host at all — they're reached over the internal Docker network.

6. Caddy Reverse Proxy

Caddy handles HTTPS and proxies requests to the Fluxer container. Edit the Caddyfile at /etc/caddy/Caddyfile.

Basic Setup

/etc/caddy/Caddyfile
fluxer.example.com {
    reverse_proxy 127.0.0.1:8080

    encode gzip zstd

    header {
        X-Content-Type-Options    nosniff
        X-Frame-Options           DENY
        Referrer-Policy           strict-origin-when-cross-origin
        -Server
    }

    log {
        output file /var/log/caddy/fluxer-access.log
        format json
    }
}

With WebSocket Support

If your Fluxer server uses WebSockets for real-time features, the basic config above already works — Caddy handles WebSocket upgrades automatically. For explicit configuration:

/etc/caddy/Caddyfile
fluxer.example.com {
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Timeouts for long-lived connections
        transport http {
            read_timeout  300s
            write_timeout 300s
        }
    }

    encode gzip zstd

    header {
        X-Content-Type-Options    nosniff
        X-Frame-Options           DENY
        Referrer-Policy           strict-origin-when-cross-origin
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        -Server
    }

    log {
        output file /var/log/caddy/fluxer-access.log {
            roll_size 100mb
            roll_keep 5
        }
        format json
    }
}

Multiple Services

If you're also hosting a web client on a different subdomain:

/etc/caddy/Caddyfile
# API / Server
fluxer.example.com {
    reverse_proxy 127.0.0.1:8080
    encode gzip zstd
}

# Web Client
app.example.com {
    root * /var/www/fluxer-web
    file_server
    try_files {path} /index.html
    encode gzip zstd
}

After editing the Caddyfile, reload Caddy:

sudo systemctl reload caddy

7. Starting the Server

1

Start the containers

cd /opt/fluxer
docker compose up -d
2

Check the logs

# Follow logs in real time
docker compose logs -f fluxer_server

# Check container status
docker compose ps
3

Verify Caddy

# Check Caddy status
sudo systemctl status caddy

# Test the endpoint
curl -I https://fluxer.example.com/_health

Done! Your Fluxer server should now be accessible at https://fluxer.example.com. Caddy handles TLS certificates automatically.

8. Updating

To update your Fluxer server to the latest stable version:

cd /opt/fluxer

# Pull the current stable image
docker compose pull

# Restart with the new stable image
docker compose up -d

# Verify it's running
docker compose ps
docker compose logs --tail=80 fluxer_server

Note: Self-hosted installs should use the :stable Docker image tag. Do not use :latest. If you see ERR_PNPM_NO_SCRIPT_OR_SERVER, check that your Compose file is using ghcr.io/fluxerworld/fluxer-server:stable.

Backups: Always back up your data/ directory before updating, especially the database. For SQLite: cp data/fluxer.db data/fluxer.db.bak

9. Troubleshooting

Container won't start

# Check logs for errors
docker compose logs fluxer_server

# Verify config is valid JSON
python3 -m json.tool config/config.json

# Check file permissions
ls -la config/ data/

502 Bad Gateway from Caddy

TLS certificate issues

Database permission errors

# Fix ownership of the data directory
sudo chown -R 1000:1000 /opt/fluxer/data

Check resource usage

docker stats fluxer_server

Need help? Open an issue on GitHub. Please note that self-hosting support is not provided through the Fluxer World app — all questions should go through GitHub Issues where the community and maintainers can help.

Join Fluxer World Back to Home