Self-hosting OpenNutriTracker companion services with Docker
OpenNutriTracker is local-first, but the OFF mirror and backup target benefit from a tiny home server.
What this is and isn’t
OpenNutriTracker is a Flutter app that runs locally on your phone. There is no “OpenNutriTracker server” you can host. So this isn’t a “host the app” guide — it’s a guide to running the two companion services that make ONT meaningfully better when you’re on a self-hosted homelab:
- A local Open Food Facts mirror. Faster lookups, offline-capable, no rate limits, and no telemetry to OFF’s API.
- A WebDAV target for backups. ONT exports a JSON snapshot; we want it landing somewhere we control.
Total RAM: about 600 MiB. Total disk: about 12 GiB after first sync. Runs fine on a Pi 5.
Architecture
[Phone, OpenNutriTracker] ──╮
├──> [Caddy reverse proxy, TLS]
[Phone, file manager] ────╯ │
├──> [OFF mirror (read-only)]
└──> [WebDAV (Apache mod_dav)]
Caddy is the only thing exposed to the LAN. The OFF mirror and WebDAV are container-local. Caddy terminates TLS using a Tailscale-issued cert (or your CA of choice).
docker-compose.yml
services:
off-mirror:
image: openfoodfacts/openfoodfacts-server:nightly
restart: unless-stopped
environment:
- OFF_MIRROR_MODE=read-only
volumes:
- off-data:/var/data/off
- off-cache:/var/cache/off
expose:
- "8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v2/product/0"]
interval: 60s
timeout: 10s
retries: 3
webdav:
image: bytemark/webdav:latest
restart: unless-stopped
environment:
- AUTH_TYPE=Basic
- USERNAME=ont-backup
- PASSWORD_FILE=/run/secrets/webdav_password
volumes:
- webdav-data:/var/lib/dav
secrets:
- webdav_password
expose:
- "80"
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- off-mirror
- webdav
secrets:
webdav_password:
file: ./secrets/webdav_password.txt
volumes:
off-data:
off-cache:
webdav-data:
caddy-data:
caddy-config:
Caddyfile
ont.lan.example.org {
encode gzip
@off path /off/* /api/*
handle @off {
reverse_proxy off-mirror:8080
}
@webdav path /backup/*
handle @webdav {
reverse_proxy webdav:80
}
handle {
respond "OK" 200
}
log {
output file /var/log/caddy/access.log {
roll_size 10MB
roll_keep 5
}
}
}
The @off path serves the OFF mirror’s API behind a stable URL on your LAN. ONT can be pointed at this URL via the in-app “custom OFF endpoint” setting (added in ONT 1.6+).
OFF mirror first sync
The first sync downloads roughly 8 GiB of OFF JSON-Lines. Allow a couple of hours over a residential connection. Subsequent syncs are incremental and run nightly via the container’s built-in cron.
docker compose up -d off-mirror
docker compose logs -f off-mirror
# wait for "initial-sync-complete"
If the sync stalls, kill it and restart. The download resumes.
Backup workflow
In OpenNutriTracker, set the export schedule to weekly. Point it at:
https://ont.lan.example.org/backup/ont-backup.zip
ONT will PUT the zip on every export. Filenames are timestamped on the server side via a webdav post-receive hook (sample script below). We keep the last 12 weeks.
# /scripts/webdav-rotate.sh
cd /var/lib/dav/data/backup
TIMESTAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ)
mv ont-backup.zip "ont-backup-${TIMESTAMP}.zip" 2>/dev/null || true
ls -t ont-backup-*.zip | tail -n +13 | xargs -r rm
Run this from cron every 5 minutes. It’s idempotent.
Restore drill
Quarterly. We’ve all written backup scripts that don’t restore. Don’t be that person.
# pull most recent backup
curl -u ont-backup -o /tmp/ont-restore.zip \
https://ont.lan.example.org/backup/ont-backup-2026-01-12T03-15-00Z.zip
# verify integrity
unzip -t /tmp/ont-restore.zip
# import on a fresh ONT install (Settings → Import)
Confirm the diary days, custom recipes, and weight history all came back. We have hit two ONT minor-version bumps where the schema changed; both required a re-import on the new version, both worked.
Watch-outs
- ONT’s “custom OFF endpoint” setting is buried in Developer Settings and requires enabling Developer Mode (tap the build version 7 times, in true Android tradition).
- The OFF mirror image’s nightly sync runs at 03:00 UTC by default. If your ISP has off-peak limits, adjust.
- Caddy’s automatic HTTPS will fail if your LAN-only domain doesn’t resolve publicly. Use Tailscale’s
tailscale certor a private CA. See TLS for self-hosted nutrition apps.
What this gets you
Once it’s running, OpenNutriTracker on the phone:
- Looks up barcodes against your local OFF mirror in <100 ms (vs 400–800 ms against OFF’s public API).
- Works in airplane mode for any product the mirror has indexed.
- Backs up every export to your homelab automatically.
- Phones home to nothing.
References
- OpenNutriTracker: github.com/simonoppowa/OpenNutriTracker
- Open Food Facts mirror docker: github.com/openfoodfacts/openfoodfacts-server
- bytemark/webdav image: github.com/bytemark/docker-webdav
- Caddy: caddyserver.com