Self-Hosting Guide
Deploy your own Fluxer server with Docker, configure it with JSON, and put it behind Caddy as a reverse proxy.
Contents
1. Prerequisites
Before starting, make sure your server meets these requirements. If any of these are missing, the setup will not work.
- A Linux VPS or dedicated server — Ubuntu 22.04+ or Debian 12+ recommended. Other distros may work but are untested.
- Docker 24+ and Docker Compose v2 — older versions will cause issues
- Caddy v2.7+ — used as the reverse proxy with automatic HTTPS
- A domain name with an A record pointed at your server's public IP
- Ports 80 and 443 open and not used by another service
- Basic Linux knowledge — you should be comfortable with the terminal, editing files, and reading error messages
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.
- CPU is light — the stack is near-idle at rest, so 1 vCPU handles a small instance fine.
- Disk is dominated by uploads (the built-in object store), not the database. Start around ~10 GB and scale with how much media your users share.
- Search (optional): the Meilisearch profile adds ~250 MB. Avoid the Elasticsearch profile on small servers — its JVM alone wants ~1 GB.
- Voice (optional): the LiveKit profile is light (~55 MB) but needs its own UDP port range opened.
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:
- HTTP → HTTPS — All traffic on port 80 is redirected to port 443. Caddy does this by default when you specify a domain in the Caddyfile.
- www → non-www (optional) — Add a redirect block if you want
www.fluxer.example.comto redirect tofluxer.example.com.
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:
{
"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:
{
"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/:
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
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:
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:
# 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
Start the containers
cd /opt/fluxer
docker compose up -d
Check the logs
# Follow logs in real time
docker compose logs -f fluxer_server
# Check container status
docker compose ps
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
- Ensure the Fluxer container is running:
docker compose ps - Verify the port binding matches the Caddyfile:
docker compose port fluxer_server 8080 - Check if the health endpoint responds locally:
curl http://127.0.0.1:8080/_health
TLS certificate issues
- Ensure your domain's DNS A record points to your server
- Check ports 80 and 443 are open:
sudo ss -tlnp | grep -E ':80|:443' - Review Caddy logs:
sudo journalctl -u caddy --no-pager -n 50
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.