Self-hosted homebrewing telemetry platform. Ingests gravity and pressure readings from physical sensors and tracks batches through their full lifecycle — fermenting → conditioning → kegged or bottled.
Runs entirely on your local network via Docker Compose. No cloud account required.
Website: www.brewgraph.com · Documentation: www.brewgraph.com/docs
Note: BrewGraph is the successor to BrewLogger. The brewlogger.com domain was already taken, so the project was renamed and rebuilt as BrewGraph. Existing BrewLogger backups can be imported directly via the restore feature. BrewGraph can be seen as BrewLogger v2.0 since it has a new datamodel and new features.
Security notice: BrewGraph is designed for trusted LAN deployment only. The shared API key authenticates the web UI and is delivered to the browser in a static file; IoT devices do not use it — they post to the open ingest endpoints with their own per-device tokens. Do not expose BrewGraph to the public internet. If remote access is needed, use a VPN (WireGuard, Tailscale) or a reverse proxy with its own auth layer. See Security model & accepted tradeoffs for the full threat model.
- Supported devices
- Features
- Requirements
- Quick start — pre-built images
- Optional services
- Quick start — build from source
- Services
- Environment variables
- HTTPS (optional)
- Updating
- Development
- Security model & accepted tradeoffs
- License
| Device | Protocol | Ingest endpoint | Firmware |
|---|---|---|---|
| iSpindel | HTTP | /ingest/ispindel |
Any version |
| GravityMon | HTTP / BLE | /ingest/gravitymon |
Any version |
| GravityMon Gateway | HTTP | /ingest/gravitymon |
Any version |
| PressureMon | HTTP / BLE | /ingest/pressuremon |
Any version |
| KegMon | HTTP | server only; the firmware adds /ingest/kegmon and /ingest/kegmon/beer |
Integration in development, not yet released |
| ChamberCtl | HTTP / BLE | /ingest/chamber |
Integration in development, not yet released |
Where a device type has no released firmware yet, the ingest endpoint is in place but has not been verified against released firmware.
- Ingest gravity and temperature readings from iSpindel-compatible devices over HTTP
- Ingest pressure readings from PressureMon devices
- BLE ingestion via a sidecar for wireless sensors (Linux only)
- Automatic device discovery via mDNS/Bonjour (Requires Linux host)
- Batch lifecycle management with fermentation-step and dry-hop tracking, plus per-batch notes
- Gravity, pressure, and temperature charts per batch, plus gravity comparison across batches
- Fermentation completion prediction via a machine learning model
- Yeast strain library with batch assignment (Used for improving machine learning result)
- Batch data export (gravity CSV, pressure CSV, batch JSON)
- Brewfather batch/recipe import
- BeerXML recipe import
- Tap and keg management with pour tracking
- Storage vessel management
- Public tap and bottle display
- Configurable measurement-forwarding integrations
- System, device, and ingestion event logs
- Backup and restore of all data
- REST API with interactive OpenAPI docs
- Vue 3 single-page web UI for desktops
- API key authentication
- PostgreSQL backend (SQLite for local development/testing only, not a deployment option)
- Docker Engine 24+ and Docker Compose v2
- Linux, macOS, or Windows (WSL2) host
- For BLE ingestion: Linux host with Bluetooth hardware and BlueZ
- For mDNS discovery:
network_mode: hostsupport (Linux required)
The recommended path for most users. Images are published to Docker Hub under mpse2/brewgraph-*.
1. Download the compose file
curl -o docker-compose.yaml https://raw.githubusercontent.com/mp-se/brewgraph/main/docker-compose.example.yaml2. Create a .env file with your API key
cat > .env <<'EOF'
API_KEY=replace-with-a-long-random-secret
POSTGRES_PASSWORD=replace-with-a-unique-database-password
EOFAPI_KEY may contain only printable ASCII characters (letters a-z A-Z without accents,
digits and symbols, no spaces). Characters such as Ö, å and ä are not accepted; the API
refuses to start and logs which position is invalid. See the log with
docker compose logs brewgraph-api.
3. Start the core services
docker compose up -dThe web UI is available at http://localhost:80. The API is accessible internally at brewgraph-api:8080 and through the Nginx proxy at http://localhost:80/api.
Note: The interactive API docs (
/docs) are blocked at the Nginx proxy for security. Access them directly on the API's own port instead:http://localhost:8080/docs.
The default Compose configuration starts all services. The API, database, Redis, web UI, and display service form the default stack; the remaining sidecars enable specific capabilities:
| Service | Required | When to enable |
|---|---|---|
brewgraph-api |
Yes | Always |
brewgraph-db |
Yes | Always |
brewgraph-redis |
Yes (default configuration) | Required for caching and integrations; can be disabled only when caching and integrations are unused |
brewgraph-web |
Yes | Needed for the web UI |
brewgraph-display |
Yes | Public tap and bottle display |
brewgraph-log |
No | Device log collection via device websocket API |
brewgraph-mdns |
No | Auto-discovery of devices on the LAN (Linux only) |
brewgraph-ble |
No | BLE ingestion for GravityMon / PressureMon / ChamberCtl (Linux only) |
brew_pgadmin |
No | Optional database management UI |
Redis is load-bearing for Integrations, not just an optional cache. Measurement
forwarding (Settings → Integrations) queues each job on Redis at ingest time and drains it in
the background; with CACHE_ENABLED=false or Redis unreachable, enqueue_forward logs a
warning and drops the job — the reading itself still saves, but nothing gets forwarded, with
no error surfaced anywhere in the UI. If you use Integrations, run brewgraph-redis.
Use this path if you want to modify the code or run the latest unreleased version.
git clone https://github.com/mp-se/brewgraph.git
cd brewgraph
cp docker-compose.example.yaml docker-compose.yaml
cat > .env <<'EOF'
API_KEY=replace-with-a-long-random-secret
POSTGRES_PASSWORD=replace-with-a-unique-database-password
EOF
docker compose build
docker compose up -d| Service | Image | Purpose |
|---|---|---|
brewgraph-api |
mpse2/brewgraph-api |
FastAPI backend, REST API, scheduler |
brewgraph-db |
postgres:17-alpine |
Primary database |
brewgraph-redis |
redis:7-alpine |
Ephemeral state cache |
brewgraph-web |
mpse2/brewgraph-web |
Vue 3 UI served by Nginx (HTTP or HTTPS) |
brewgraph-display |
mpse2/brewgraph-display |
Public tap and bottle display |
brewgraph-log |
mpse2/brewgraph-log |
Collects device serial logs over WebSocket |
brewgraph-mdns |
mpse2/brewgraph-mdns |
Auto-discovers devices via mDNS/Bonjour (Linux only) |
brewgraph-ble |
mpse2/brewgraph-ble |
BLE ingestion from wireless sensors (Linux only) |
brewgraph-bleandbrewgraph-mdnsusenetwork_mode: hostand only work on Linux. On macOS they start but cannot access LAN multicast or the host Bluetooth stack.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | SQLAlchemy URL for the PostgreSQL database. Set automatically by docker-compose.yaml |
API_KEY |
Yes | — | Bearer token for authenticated UI and management API requests; ingest endpoints use per-device tokens. Printable ASCII only, no spaces (no Ö, å, ä); the API refuses to start otherwise and logs the invalid position |
DB_POOL_SIZE |
No | 20 |
PostgreSQL connections kept open per API process (integer >= 1). Ignored for SQLite |
DB_MAX_OVERFLOW |
No | 20 |
Extra PostgreSQL connections a process may open beyond DB_POOL_SIZE under load (integer >= 0) |
DB_POOL_TIMEOUT |
No | 10 |
Seconds a request waits for a free connection before failing (integer >= 1) |
SCHEDULER_ENABLED |
No | true |
Enable background jobs |
CACHE_ENABLED |
No | true |
Enable Redis cache |
REDIS_HOST |
No | — | Redis hostname (required if CACHE_ENABLED=true) |
TRUST_PROXY_HEADERS |
No | false |
Trust X-Real-IP / X-Forwarded-For (rightmost entry) from a reverse proxy. Set true when behind Nginx. |
TRUSTED_PROXIES |
No | loopback and private ranges | Comma-separated IPs or CIDR ranges whose proxy headers are believed. Headers from any other peer, and values that are not IP addresses, are ignored. Narrow it to your proxy address if you can. |
SSE_MAX_CONNECTIONS_PER_IP |
No | 10 |
Concurrent live-update (/events) streams allowed per client IP; extra connections get 429. 0 = no limit |
SSE_MAX_CONNECTIONS |
No | 50 |
Concurrent live-update streams in total (the API key is shared, so this is also the per-key limit). 0 = no limit |
AUTH_MAX_FAILURES |
No | 10 |
Failed auth attempts before IP block |
AUTH_BLOCK_SECONDS |
No | 300 |
Seconds an IP stays blocked after too many failures |
BREWFATHER_USER_KEY |
No | — | Brewfather integration user key |
BREWFATHER_API_KEY |
No | — | Brewfather integration API key |
Each API process can open up to DB_POOL_SIZE + DB_MAX_OVERFLOW database connections (40 with the defaults). When several processes share one database, the sum of that figure over all of them must stay below PostgreSQL's max_connections (default 100), with headroom for migrations and admin sessions. An invalid value stops startup with an error naming the variable.
| Variable | Required | Description |
|---|---|---|
API_HOST |
Yes | Hostname of brewgraph-api (e.g. brewgraph-api) |
API_KEY |
Yes | Must match the API's API_KEY (printable ASCII only; the service exits with an error otherwise) |
| Variable | Default | Description |
|---|---|---|
API_HOST |
— | Hostname of brewgraph-api |
API_KEY |
— | Bearer token |
REDIS_HOST |
— | Redis hostname (optional) |
MAX_FILE_SIZE |
1048576 |
Bytes before log rotation |
MAX_LOG_FILES |
5 |
Retained files per device, including the active log |
| Variable | Default | Description |
|---|---|---|
API_HOST |
— | Use localhost with host networking |
API_KEY |
— | Bearer token |
REDIS_HOST |
— | Redis hostname (optional) |
MIN_INTERVAL |
900 |
Min seconds between posts per device |
Place your certificate and key in web/certs/, named exactly:
web/certs/server.crt
web/certs/server.key
Then, in docker-compose.yaml, uncomment both the brewgraph-web volume mount and the
443:443 port mapping:
brewgraph-web:
ports:
- "80:80"
- "443:443"
volumes:
- ./web/certs:/etc/nginx/ssl:roThe Nginx entrypoint detects the certificate files and switches to HTTP+HTTPS automatically. Without the port mapping above, HTTPS is enabled inside the container but unreachable from outside it.
Pre-built images:
docker compose pull
docker compose up -dBuilt from source:
git pull
docker compose build
docker compose up -dcd api
make install-test # install deps including test extras
make run # dev server on :8080 (SQLite, API key "devkey")
make test # run test suite
make lint # pylint
make fmt # autoformatcd web
npm install
VITE_APP_TOKEN=devkey npm run dev # dev server on :5173 (proxies API to localhost:8080)
npm run test # Vitest unit tests
npm run lint # ESLintBrewGraph OSS targets a single trusted operator on a private LAN. The items below are deliberate design decisions for that threat model — they are not findings, and should not be re-reported in security reviews of the OSS build.
The LAN is trusted: anyone who can reach the web UI is treated as the operator. Do not expose BrewGraph to the public internet. For remote access use a VPN (WireGuard/Tailscale) or a reverse proxy that adds its own authentication.
| Area | Decision | Rationale |
|---|---|---|
| API key in the SPA | The web container injects API_KEY into a static env-config.js that the browser reads. |
The key is a web-UI credential only. Any LAN client is already trusted, and without TLS (which most LAN deployments skip) a bootstrap handshake would add no real protection. |
| Open ingest endpoints | /ingest/* requires no API key. |
IoT firmware (iSpindel, GravityMon, PressureMon, KegMon…) authenticates with a per-device token in the payload; endpoints are per-IP rate-limited and per-device throttled. |
| Device tokens stored in plaintext | Device tokens are kept readable in the database and shown in the UI. | A token only ties a device's readings to a batch or tap; it grants no access to the API or to other data. The UI has to display it so it can be copied into the device firmware, and a hash could not be shown again. Tokens are 192-bit random values, unique per device, and can be regenerated at any time. |
CORS allow_origins: * |
Wildcard origin with allow_credentials=false. |
No cookies are used and auth is a bearer header, so a wildcard origin cannot drive cross-site credentialed attacks. Simplifies serving the UI and API from different origins/ports. |
| Single shared API key | One full-privilege bearer token, no per-user roles. | Single-operator deployment — there are no other users to isolate. |
| Best-effort rate limiting | Auth brute-force blocking, ingest rate limits, and device throttling require Redis (CACHE_ENABLED=true) and no-op if Redis is unavailable. |
The API key is a 128-bit secrets.token_urlsafe value, so online brute force is infeasible regardless. The limiter is defense-in-depth against device abuse, not the primary control. |
proxy_fetch reaches LAN hosts |
The authenticated UI can proxy requests to private (RFC1918) IPs; cloud-metadata, loopback, and link-local targets are blocked, and the resolved IP is pinned against DNS rebinding. | Configuring physical devices on the LAN is the intended feature, and the caller is already the trusted operator. |
- API key compared with
hmac.compare_digest(constant-time). proxy_fetchrejects public, loopback, and link-local targets and pins the resolved IP to prevent DNS rebinding.- Secrets are stored as
SecretStrand redacted from logs. - Containers run as a non-root user; CSP and security headers are set at the edge.
BrewGraph is licensed under the GNU General Public License v3.0 for open source use. For commercial use without source disclosure, a separate Commercial License is required.
© 2024–2026 Magnus Persson