Trace Every Bite — A self-hosted personal nutrition tracker built for privacy and full data ownership.
NutriTrace runs entirely in a single Docker container on your own hardware. No accounts on external services, no data leaving your network, no subscriptions.
.zip with embedded image files for phone-to-phone transfer without a serverSmart Log is an experimental feature that lets you log food by pressing and holding the Trace button on any page and saying what you ate. The AI parses your sentence and matches each item against your saved foods, meals, recipes, or yesterday's diary.
| Source | What it matches | Example phrases |
|---|---|---|
| Foods (default) | Single foods from your library, then Open Food Facts | "2 eggs", "a slice of toast", "Greek yogurt" |
| Saved Meals | Multi-ingredient meals you've built in MealEditor | "my chicken caesar salad meal", "the pasta carbonara meal", "for lunch I had my morning bowl meal" |
| Saved Recipes | Recipes you've saved (with is_recipe: 1) |
"my chicken stir fry recipe", "made the pasta carbonara recipe", "from my lasagna recipe" |
| Yesterday's diary | Copy items from yesterday's matching meal slot | "same as yesterday for lunch", "yesterday's breakfast", "repeat yesterday's dinner", "what I had for breakfast yesterday" |
| Water | Adds to your water log (not the food diary) | "drank a glass of water", "500ml of water", "had my protein shaker", "two cups of water" |
The trigger words "meal", "recipe", and "yesterday" are how you tell the AI which kind of record to look for. Without those keywords, Smart Log defaults to searching individual foods.
You can mention the meal in your sentence and Smart Log will route the items there automatically:
Smart Log uses your actual configured meal slot names (visible in the AI prompt), so custom slots like "Snack 1 / 2 / 3", "Brunch", or "Late Night" all work. It also handles renamed defaults — if you renamed "Breakfast" to "Morning Bowl", saying "for breakfast" still routes there via fuzzy matching.
Smart Log uses a tightly-constrained prompt (~150 tokens in, ~50 out) so it's cheap. On GPT-4o mini or Claude Haiku, logging six meals a day for a year costs roughly $0.10 USD. Gemini's free tier covers it entirely.
docker-compose.yml from this repo, or copy it directly:services:
nutritrace:
image: ghcr.io/traceapps/nutritrace:latest
container_name: nutritrace
ports:
- "3000:3001"
volumes:
- ${DATA_DB_PATH}:/data/db
- ${DATA_UPLOADS_PATH}:/data/uploads
environment:
- DB_PATH=/data/db/nutritrace.db
- UPLOADS_PATH=/data/uploads
- JWT_SECRET=${JWT_SECRET}
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_SECURE=${SMTP_SECURE:-false}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASS=${SMTP_PASS:-}
- SMTP_FROM=${SMTP_FROM:-}
restart: unless-stopped
No changes to this file are needed — everything is driven by .env. If you want to pin to a specific version, change latest to a release tag.
.env.example to .env and fill in your paths:DATA_DB_PATH=/your/host/path/db
DATA_UPLOADS_PATH=/your/host/path/uploads
JWT_SECRET=your-long-random-secret
# Optional — SMTP for password reset emails and user invites
# If omitted, invites fall back to a copyable link instead of email
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_SECURE=false
# [email protected]
# SMTP_PASS=your-password
# SMTP_FROM=NutriTrace <[email protected]>
Generate a JWT secret:
openssl rand -base64 48
docker compose up -d
http://localhost:3000 in your browser.On first launch, a setup wizard walks you through enabling user management and creating your admin account. If you skip user management, the app runs in single-user mode with no login required.
| Variable | Required | Default | Description |
|---|---|---|---|
DATA_DB_PATH |
Yes | — | Host path for the SQLite database directory |
DATA_UPLOADS_PATH |
Yes | — | Host path for uploaded images and backups |
JWT_SECRET |
If using users | — | Secret key for signing auth tokens. Use a long random string. |
RECOVERY_TOKEN |
No | — | Passphrase required to disable user management from the login page (lockout recovery). Without this the recovery endpoint is disabled. |
LOG_LEVEL |
No | info |
Log verbosity: error | warn | info | debug. Use debug for detailed wellness sync output (Fitbit, Withings, Garmin, Health Connect). |
SMTP_HOST |
No | — | SMTP server hostname (for password reset & invites) |
SMTP_PORT |
No | 587 |
SMTP port |
SMTP_SECURE |
No | false |
true for SSL (port 465), false for STARTTLS |
SMTP_USER |
No | — | SMTP username |
SMTP_PASS |
No | — | SMTP password |
SMTP_FROM |
No | — | From address, e.g. NutriTrace <[email protected]> |
AI_PROVIDER |
No | — | Lock Trace to a specific provider for all users: claude | openai | gemini |
AI_API_KEY |
No | — | Shared AI API key. Key is server-side only — never sent to the browser. |
AI_MODEL |
No | provider default | Override the AI model (e.g. claude-haiku-4-5-20251001) |
AI_ENABLED |
No | — | Set to true to auto-enable Trace for all users |
SMTP and AI settings can also be configured in the Settings UI. Environment variables take priority over UI values and lock those fields for all users.
Two host directories must be bind-mounted:
DATA_DB_PATH) — SQLite file. Survives container restarts and redeployments.DATA_UPLOADS_PATH) — Food/meal photos and server-side backups (stored in uploads/backups/). Survives container restarts and redeployments.Nothing else needs to persist — the container is stateless beyond these two volumes.
docker compose pull
docker compose up -d
The database schema migrates automatically on startup.
| Layer | Technology |
|---|---|
| Frontend | Svelte 4, svelte-spa-router, Vite, PWA (service worker) |
| Backend | Node.js, Express, better-sqlite3 |
| Auth | JWT (httpOnly cookie), bcryptjs |
| Container | Docker, multi-stage Dockerfile |
| CI/CD | GitHub Actions → GitHub Container Registry |
NutriTrace can sync data from Fitbit, Withings, Garmin, and Android Health Connect. Each cloud provider (Fitbit/Withings/Garmin) requires registering a free OAuth application with the respective service and entering the credentials in Settings → Wellness. Health Connect is on-device and needs no developer setup.
https://your-nutritrace-domain.com/api/wellness/fitbit/callbackhttps://your-nutritrace-domain.com/api/wellness/withings/callbackGarmin Health API requires a partnership approval (not a free developer program). If you have access, set the callback URL to https://your-nutritrace-domain.com/api/wellness/garmin/callback.
Reads steps, sleep, heart rate, weight, and exercise directly from the Android Health Connect API. Works in the NutriTrace Android app without any server setup — useful for users running fully local/offline. Enable in Settings → Wellness → Health Connect on the Android app and grant the requested permissions.
Note: The callback URLs for Fitbit/Withings/Garmin must use your public domain (not
localhost). All three require HTTPS.
All external API calls are proxied server-side — no keys are exposed to the browser.
Coming soon:
Future:
NutriTrace surfaces three derived wellness scores. Where the source device exposes its own value via API, that value is used directly. Where it doesn't, NutriTrace computes one. The computed scores are prefixed Trace in this section to make the distinction explicit.
| Score | Fitbit | Garmin | Withings | Health Connect |
|---|---|---|---|---|
| Sleep | Trace Sleep Score (computed — Fitbit API doesn't expose its own) | Native overallSleepScore |
Native sleep score when present | Trace Sleep Score |
| Daily Readiness | Trace Readiness (computed) | Trace Readiness (computed) | Trace Readiness (computed) | Trace Readiness (computed) |
| Stress | Trace Stress (computed) | Garmin's native stress_avg is stored separately; Trace Stress is also computed |
Trace Stress (computed) | Trace Stress (computed) |
Trace Sleep Score combines sleep duration, deep / REM percentages, SpO₂, HRV, and efficiency into a single 0–100 value (formula in server/routes/fitbit.js). Trace Readiness weighs HRV against a 30-day baseline plus resting HR and last night's sleep, with an activity-spike penalty. Trace Stress is a 7-day-smoothed inverse of HRV + RHR + sleep (formula in server/lib/wellness-scores.js).
These scores prioritize day-to-day consistency across whatever data sources you've connected. They're not intended to match what each device's own app shows — readings may differ from device-native scores.
If a wellness integration on your device behaves wrong (missing data, weird numbers), file an Integration Test report — the more devices reported, the easier it is to spot integration-specific quirks.
Features marked Experimental in Settings (Smart Log, Goal Insights, Food Sharing, Dynamic Calorie Goal, Garmin integration) work but haven't been hammered enough to drop the label. Real-world bug reports help promote them to stable. The badge comes off when edge-case handling is solid, not on a calendar.
If you're filing a bug, logs make it 10× faster to fix. Easiest path first:
In-app logs (PWA + Android — recommended):
Settings → Diagnostics → View logs. A 500-line in-memory ring buffer captures console.log/info/warn/error/debug plus uncaught errors. Toggle Verbose to capture extra sync / DB / notification detail. The viewer has Copy / Share / Clear — Share opens the system share sheet (Gmail, Drive, Files) on Android, Web Share API on PWA. No USB cable, no DevTools needed.
Server logs (Docker):
docker logs nutritrace --tail 200
For deeper diagnosis, set LOG_LEVEL=debug in your .env and restart. Note: debug logs contain personal health data (HRV, RHR, sleep duration, calorie counts). Redact these before posting publicly.
Browser DevTools (PWA, advanced):
F12 → Console tab. Filter by [wellness], [sync], [diary], etc. depending on the area.
Android via chrome://inspect (advanced fallback): If the in-app log viewer doesn't capture what you need:
chrome://inspect/#devices in ChromeWhere to file: github.com/traceapps/nutritrace/issues. Templates are provided for bug reports, feature requests, and integration test reports.
NutriTrace is free to self-host and always will be. It's built and maintained by one person; donations help cover real costs like an Apple Developer account and Mac/iPhone hardware to enable an iOS port, plus ongoing infrastructure. Donations are appreciated but never required — starring the repo helps with discoverability and costs nothing.
NutriTrace was inspired by two excellent self-hosted nutrition trackers:
Thanks to both projects for showing what's possible.
AGPL-3.0 — entire codebase including the Android app source.