Skip to content
Fizzlayer.
  • Docs
  • Self-host
  • Pricing
  • Live demo
  • GitHub
  • Sign in

Self-hosting

Run Fizzlayer with Docker

Pull two images, start the stack, and create an account. No Rust compiler. Chat, voice, phone calls, and card checkout stay off until you add them.

StartDownload three files and open the site. Send readingsHTTPS, WebSocket, and MQTT 1883. Your domainCaddy gets a certificate. Supabase CloudSkip the local database.

Start

  1. What you need
  2. Quick start
  3. Build from source
  4. First account

Devices

  1. Send readings
  2. Pair the CLI

Options

  1. HTTPS and a domain
  2. Supabase Cloud
  3. Chat, calls, billing
  4. Upgrade and backups
  5. Troubleshooting

What you need

  • Docker Engine 24 or newer, and Docker Compose 2.24 or newer (docker compose version).
  • A POSIX shell, curl, and id. Secrets are created inside the API image, so the host does not need openssl, python3, or git. git is only for building from source.
  • Free ports 8080 (site and API), 8081 (live WebSocket), and 1883 (MQTT). HTTPS also uses 80 and 443.
  • About 1 GB of free RAM and 4 GB of free disk. An idle stack used about 140 MB of RAM. The images are a little over 1 GB.

Images: fizzlayer/fizzlayer-api and fizzlayer/fizzlayer-worker. The same tags are on ghcr.io/fizzlayer/. latest tracks the main branch.

Quick start

Create an empty directory. Download the compose file, then let the API image write .env. You do not clone the repository and you do not compile anything.

mkdir fizzlayer && cd fizzlayer
curl -fsSL https://raw.githubusercontent.com/fizzlayer/fizzlayer/main/deploy/compose.yml -o compose.yml
curl -fsSL https://raw.githubusercontent.com/fizzlayer/fizzlayer/main/deploy/compose.env.example -o .env.example
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/out" fizzlayer/fizzlayer-api:latest init --out /out/.env
docker compose up -d

That docker run writes .env (mode 600) with a database password, a JWT secret, a service-role key, and CRON_SECRET. .env.example lists the same variables with placeholders. It will not overwrite .env. Add init --force to replace it, which breaks a database that already used the old password unless you delete the volume too. scripts/selfhost-init.sh runs the same command.

Open http://localhost:8080. Then:

curl -fsS http://localhost:8080/api/status
curl -fsS http://localhost:8081/health

You want "database":"connected" and a worker health of ok. docker compose down stops the stack and keeps the database. down -v deletes it. The full variable list is in the repository guide.

Build from source

Clone the repository to change the server. ./scripts/selfhost-init.sh then writes deploy/.env. The first build takes several minutes.

git clone https://github.com/fizzlayer/fizzlayer.git
cd fizzlayer
./scripts/selfhost-init.sh
docker compose -f deploy/docker-compose.yml up -d --build

That tags them fizzlayer/fizzlayer-api and fizzlayer/fizzlayer-worker. Later up commands reuse those local images. Pin a published commit with FIZZ_API_IMAGE and FIZZ_WORKER_IMAGE set to fizzlayer/fizzlayer-api:sha-… (the full 40-character commit) in deploy/.env.

First account

Open http://localhost:8080/app.html. Create an account with a password of at least 12 characters. No confirmation email is sent: MAILER_AUTOCONFIRM=true is the default. Sign in later on the same page.

From a terminal:

curl -fsS -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"action":"sign_up","email":"you@example.com","password":"choose-a-long-pass"}' \
  http://localhost:8080/api/auth

curl -fsS -b cookies.txt -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"action":"sign_in","email":"you@example.com","password":"choose-a-long-pass"}' \
  http://localhost:8080/api/auth

curl -fsS -b cookies.txt http://localhost:8080/api/me

A reply of "confirm_email":true means autoconfirm is off. Anyone who can open the site can register. To close registration after your own account exists:

docker compose exec db \
  psql -U postgres -d postgres \
  -c "update public.fizz_instance_settings set sign_up_open = false;"

Send a reading

Create an API sensor. The response includes sensor.id and a one-time api_key that starts with fizz_.

curl -fsS -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"kind":"temperature","name":"Bench","mode":"api"}' \
  http://localhost:8080/api/sensors

curl -fsS -H "Authorization: Bearer fizz_YOUR_KEY" -H 'Content-Type: application/json' \
  -d '{"sensor_id":"SENSOR_UUID","event_id":"reading-1","metrics":{"temperature_c":21.5}}' \
  http://localhost:8080/api/ingest

curl -fsS -b cookies.txt \
  "http://localhost:8080/api/readings?sensor_id=SENSOR_UUID&limit=20"

HTTP 202 with accepted means the reading was stored. Sending the same event_id again does not create a second row.

Ask where live clients should connect:

curl -fsS http://localhost:8080/api/stream

This stack returns ws://localhost:8081/v1/stream and mqtt://localhost:1883. MQTT 3.1.1 uses the sensor id as the username and the sensor key as the password. The topic is fizz/v1/<sensor-id>/readings.

mosquitto_pub -h 127.0.0.1 -p 1883 \
  -u SENSOR_UUID -P 'fizz_YOUR_KEY' \
  -t 'fizz/v1/SENSOR_UUID/readings' \
  -m '{"event_id":"mqtt-1","metrics":{"temperature_c":23.5}}'

A device WebSocket sends {"op":"auth","key":"fizz_YOUR_KEY"} and then {"op":"reading","sensor_id":"SENSOR_UUID","event_id":"ws-1","metrics":{"temperature_c":22.5}}. The CLI in the next section does that.

Pair the CLI

Install fizz and pair it with this server. The installer downloads the CLI from GitHub. FIZZ_API selects your machine.

curl -fsSL http://localhost:8080/install.sh | sh
FIZZ_API=http://localhost:8080 fizz pair

The terminal prints a short code and waits. Open the Agents page, enter the code, then answer y on the device when it asks you to approve. After that:

fizz send temperature_c 21.5
fizz send --stream temperature_c 22.5
fizz send --mqtt temperature_c 23.5
fizz tail

fizz send uses HTTPS. --stream uses the WebSocket. --mqtt publishes to port 1883. Use only one of those flags. fizz run --once sends CPU, load, memory, and disk where the operating system exposes them.

HTTPS and a domain

Add tls to COMPOSE_PROFILES and set the public name. Ports 80 and 443 must reach this machine. Let's Encrypt uses port 80.

COMPOSE_PROFILES=local,tls
FIZZ_DOMAIN=sensors.example.com
FIZZ_ACME_EMAIL=you@example.com
PUBLIC_URL=https://sensors.example.com
SUPABASE_PUBLIC_URL=https://sensors.example.com
FIZZ_PUBLIC_HOST=sensors.example.com
FIZZ_PUBLIC_SCHEME=https
FIZZ_STREAM_URL=https://sensors.example.com/live
FIZZ_STREAM_ORIGINS=https://sensors.example.com
FIZZ_MQTT_TCP_URL=mqtt://sensors.example.com:1883
docker compose up -d
curl -fsS https://sensors.example.com/api/status

FIZZ_ACME_EMAIL=internal makes a local certificate instead. Try that with curl -kfsS https://localhost/api/status. Keep using http://localhost:8080 in a browser unless you set FIZZ_PUBLIC_SCHEME=https. MQTT on 1883 is not wrapped in TLS.

Supabase Cloud

Apply the SQL yourself, then do not start the local database:

supabase link --project-ref YOUR_PROJECT_REF
supabase db push --linked
COMPOSE_PROFILES=
SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
SUPABASE_PUBLIC_URL=https://YOUR_PROJECT_REF.supabase.co
SUPABASE_SERVICE_ROLE_KEY=YOUR_SERVICE_ROLE_KEY
FIZZ_ALLOW_INSECURE_HTTP=0
FIZZ_AUTH_UPSTREAM=
docker compose up -d api worker

SMTP and GitHub or Google sign-in are configured in the Supabase dashboard in this mode. Pulling a new checkout does not migrate a Cloud database. Run supabase db push --linked again.

Chat, calls, and billing

Leave the keys empty and the site still stores readings.

  • Chat. Signed-in chat returns 503 and names OPENAI_BASE_URL or AI_GATEWAY_API_KEY. For Ollama on the Docker host: OPENAI_BASE_URL=http://host.docker.internal:11434/v1 and OPENAI_MODEL set to a model that accepts JSON mode. host.docker.internal is mapped to the host.
  • Voice. GET /api/voice is {"available":false} until ELEVENLABS_API_KEY is set.
  • Calls. AGENTPHONE_API_KEY places US voice calls. There is no self-hosted dialer. Hobbyist plans do not place calls.
  • Stripe. Set all four of STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_TINKERER, and STRIPE_PRICE_PRO, or none. Unset, sign-up hides the plan picker, skips checkout, and opens the dashboard. Billing says paid plans are not available on this server.

Restart after editing .env: docker compose up -d.

Upgrade, backups, and logs

docker compose pull
docker compose up -d

New SQL ships inside fizzlayer/fizzlayer-api. The migrate container applies files it has not applied before. An exited migrate container with code 0 is success.

docker compose exec -T db \
  pg_dump -U postgres -d postgres --format=custom > fizzlayer.dump
docker compose logs --tail 100 api
docker compose logs --tail 100 worker

Troubleshooting

database is unavailable.
Wait until docker compose logs migrate says migrations applied. Then check rest, gateway, and auth.
The host is not configured.
Set FIZZ_PUBLIC_HOST to the host and port in the browser, and FIZZ_PUBLIC_SCHEME to http or https.
Ingest returns 401.
The header is Authorization: Bearer plus the sensor key from creation. The key is shown once.
MQTT connection refused.
curl -fsS http://localhost:8081/health should include "mqtt_tcp":true, and port 1883 should be published.
Chat returns 503.
No model is configured. Readings and the dashboard do not need one.

More cases, including a password mismatch after --force, are in the repository guide.

Fizzlayer.The physical layer for AI agents.
DocsSelf-hostStatusEnterpriseSign inPrivacy PolicyTerms of ServiceFor AI agents