Zytholo - https://github.com/pstraebler/zytholo
1.3K
Formerly known as BeerTracker
A web app to track beer consumption with multi-user management, configurable alerts, and a rich statistics dashboard.
0 to disable that alertThe dashboard shows an estimated blood alcohol content (BAC, in g/L) for the current evening. It requires your weight and sex in the settings (leave the weight empty or set it to 0 to disable the estimate).
Each logged beer contributes a mass of pure alcohol:
alcohol (g) = volume (L) × strength (% ABV) × 7.89
(7.89 g is the mass of pure ethanol in one liter of beer per degree of alcohol.)
That mass is converted into a peak BAC contribution with the Widmark formula:
peak (g/L) = alcohol (g) / (weight (kg) × r)
where r is the body-water distribution ratio: 0.68 for men, 0.55 for women.
Two refinements make the estimate more realistic than a raw Widmark snapshot:
So at any instant t, the estimated BAC is:
BAC(t) = max(0, Σ peakᵢ × absorbedᵢ(t) − 0.15 × (hours since the first drink))
where absorbedᵢ(t) ramps from 0 to 1 over a duration proportional to the drink's volume (50cl in 30 minutes).
The can-I-drive? verdict compares this value to the configurable legal limit (default 0.5 g/L; e.g. 0.8 in the UK/USA, 0.0 for a probationary licence). The times to drop back under the limit and to reach 0 are projected from the fully-absorbed peak, so they stay correct even while a beer is still being absorbed.
Below the current value, a chart plots the modeled BAC curve for the whole evening — from the first drink until the level returns to 0. Because the model is linear between events, the curve is drawn as straight, piecewise-linear segments. It shows:
The curve is recomputed every time a beer is added or removed during the evening, and the live value and the marker follow it in real time.
⚠️ The estimate is indicative only. It relies on standard averages and cannot know food intake, drinking pace, individual metabolism, medication, etc. It does not replace a breathalyser and must never be used to decide whether to drive. When in doubt, don't drive.
The fastest way to run Zytholo is with the pre-built image published on Docker Hub — pierrestraebler/zytholo — so you don't need to clone the repository or build anything. All you need is Docker with the Compose plugin.
1. Create an empty folder and add a docker-compose.yml:
services:
mariadb:
image: mariadb:11
container_name: zytholo-db
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- mariadb_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-uroot", "-p${DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
zytholo:
image: pierrestraebler/zytholo:latest
container_name: zytholo-app
restart: unless-stopped
ports:
- "127.0.0.1:${APP_PORT:-8080}:${APP_PORT:-8080}"
depends_on:
mariadb:
condition: service_healthy
env_file:
- .env
logging:
driver: json-file
options:
max-size: "10m" # max size per log file before rotation
max-file: "5" # number of files kept (→ ~50 MB max)
volumes:
mariadb_data:
driver: local
2. Create a .env file next to it:
# --- Flask ---
SECRET_KEY=generate_me # REQUIRED — long random string (see below)
# --- Admin account (created/updated on every startup) ---
ADMIN_USERNAME=admin # optional, default: admin
ADMIN_PASSWORD=change_me # REQUIRED
# --- App ---
APP_PORT=8080 # host + container port
USE_HTTPS=0 # set to 1 only behind a TLS-terminating proxy
# --- Database (shared between the app and the MariaDB container) ---
DB_HOST=mariadb # must match the MariaDB service name above
DB_PORT=3306
DB_NAME=zytholo_db
DB_USER=zytholo_user
DB_PASSWORD=zytholo_password # app database user password
DB_ROOT_PASSWORD=change_root_password
Generate a strong SECRET_KEY:
python -c 'import secrets; print(secrets.token_hex(32))'
# OR
openssl rand -hex 32
3. Start it:
docker compose up -d
Open http://localhost:8080 (or your APP_PORT) and log in with the admin credentials.
.env — the single source of configuration, read by both services. The variables under Database are shared: MariaDB uses them to create the database and application user on first boot, while the app uses them to connect, so DB_NAME / DB_USER / DB_PASSWORD must be identical on both sides (they are, since they come from the same file). SECRET_KEY and ADMIN_PASSWORD are mandatory — the app refuses to start without them. Gunicorn can also be tuned via the optional variables below.ADMIN_USERNAME / ADMIN_PASSWORD. To use an external or managed database instead, remove the mariadb service and point DB_HOST / DB_PORT at it.mariadb_data (mounted at /var/lib/mysql). It survives docker compose down, container recreation, and image upgrades; it is only wiped if you explicitly run docker compose down -v or delete the volume. Logs go to stdout/stderr (docker logs zytholo-app) — there is no other writable state to persist.docker compose pull # fetch the latest image
docker compose up -d # recreate the app container; data in mariadb_data is kept
docker exec zytholo-db sh -c 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' > zytholo-backup.sql
If you'd rather build the image yourself instead of pulling the published one (e.g. to modify the code):
git clone https://github.com/pstraebler/zytholo.git
cd zytholo
cp .env.example .env
⚠️ Important: Update the following values in .env:
SECRET_KEY: Secret key for Flask sessions (generate a long random string):
python -c 'import secrets; print(secrets.token_hex(32))'
# OR
openssl rand -hex 32
APP_PORT: Port the app will listen on and expose locally (default: 8080)
ADMIN_USERNAME: Optional. Admin username (default: admin)
ADMIN_PASSWORD: Admin password
USE_HTTPS: Do not enable in local environments (default: 0)
DB_HOST: MariaDB host (Docker default: mariadb)
DB_PORT: MariaDB port (default: 3306)
DB_NAME: Database name
DB_USER: Application DB user
DB_PASSWORD: Application DB password
DB_ROOT_PASSWORD: MariaDB root password (used by Docker service)
Then :
docker-compose up -d --build
The container runs on Gunicorn, a production-grade WSGI server — not Flask's built-in development server, which is single-process and unsuitable for production.
Dockerfile): gunicorn -c gunicorn.conf.py app:app.gunicorn.conf.py and every value can be overridden with an environment variable (no image rebuild needed — just set it in .env).0.0.0.0:$APP_PORT inside the container. The app is meant to sit behind a reverse proxy / tunnel (e.g. a Cloudflare tunnel) that terminates TLS; ProxyFix is already configured so the real client IP and scheme are honored.Optional tuning variables:
| Variable | Default | Description |
|---|---|---|
GUNICORN_WORKERS | 3 | Number of worker processes. Rule of thumb: (2 × CPU cores) + 1. |
GUNICORN_THREADS | 2 | Threads per worker. |
GUNICORN_TIMEOUT | 60 | Seconds before a stalled request is killed. |
GUNICORN_GRACEFUL_TIMEOUT | 30 | Seconds allowed for workers to finish on restart. |
GUNICORN_MAX_REQUESTS | 1000 | Recycle a worker after this many requests (mitigates memory leaks). |
GUNICORN_MAX_REQUESTS_JITTER | 100 | Random spread added to MAX_REQUESTS so workers don't all recycle at once. |
GUNICORN_LOGLEVEL | info | Log verbosity. |
GUNICORN_FORWARDED_ALLOW_IPS | * | Proxy IPs trusted for X-Forwarded-* headers. |
Access and error logs are written to stdout/stderr, so they are available through docker logs zytholo-app.
For local development only, you can still run Flask's built-in server with
python app.py— do not use it in production.
The app is available at http://localhost:8080 by default, or at http://localhost:$APP_PORT if you changed the port.
MariaDB data is stored in the Docker volume mariadb_data.
User,Date,Time,Pints,HalfPints,33cl
baptiste,2026-01-15,20:30:00,2,1,0
guy,2026-01-15,21:00:00,0,2,1
YYYY-MM-DD formatHH:MM:SS format (optional, default 00:00:00)Content type
Image
Digest
sha256:b614da11d…
Size
59.9 MB
Last updated
14 days ago
docker pull pierrestraebler/zytholo