Building a Multi-Platform Calorie Tracker: FastAPI + Docker + Expo
The Problem with Existing Apps
Most calorie trackers are either too complex or lack a proper Turkish food database. I also wanted something that runs on both phone and computer, deploys with a single Docker command, and optionally works offline. That became KiloTakip (Weight Tracker).
Architecture: "One Brain, Many Clients"
The core design decision: a single FastAPI backend serving multiple clients.
FastAPI (Python) ← single source of truth
↓
┌───┼───┐
Web PWA Mobile
(React)(nginx)(Expo RN)
Business logic lives once in Python: calorie calculations, BMR/TDEE formulas (Mifflin-St Jeor), data validation. Tested once, used everywhere.
Gemini AI Food Recognition
The standout feature: take a photo of your meal and ask "how many calories?"
@app.post("/api/photo-recognize")
async def recognize_food(file: UploadFile):
image_data = base64.b64encode(await file.read()).decode()
response = genai.GenerativeModel('gemini-1.5-flash').generate_content([
"What food is this? Return calories, protein, carbs, fat as JSON.",
{"inline_data": {"mime_type": "image/jpeg", "data": image_data}}
])
return parse_gemini_response(response.text)
Gemini's multimodal capabilities shine here — it recognizes Turkish dish names and estimates reasonable portion sizes.
607 Turkish Foods Database
The biggest gap in existing apps: Turkish cuisine. I built a 607-item database covering home cooking, street food, and restaurant meals. A custom normalizer handles Turkish character variations (ş/s, ğ/g) for fuzzy search.
Docker: One-Command Startup
services:
backend:
build: ./backend
ports: ["8000:8000"]
volumes: ["kilo-data:/app/data"]
frontend:
build: ./frontend
ports: ["3000:80"]
docker compose up -d --build — and it's running. Open in browser and "Add to Home Screen" for the PWA version.
Offline-First Mobile with Expo
The Expo app is fully independent — SQLite on-device, Gemini API key embedded, no backend required. A built APK (72MB) installs directly on Android.
Key Learnings
- Docker volumes: Named volumes are essential for SQLite persistence outside containers.
- Expo + local API: Must use the machine's IP address, not
localhost, when testing on a real device. - PWA manifest:
"display": "standalone"and a correctstart_urlare critical for proper "Add to Home Screen" behavior. - Gemini rate limits: Free tier is 15 requests/minute — show users a clear error when throttled.
View the project: GitHub