All apps · 0 apps
peptide-pitstop
Docker app from kavo's Repository
Overview
Readme
View on GitHubPeptide Pitstop
Self-hosted peptide therapy tracking that runs on your hardware, under your control.
Your data never leaves your server. Handing your weight, hormone, and dosing history to someone else's startup is a leap of faith — Peptide Pitstop removes the leap. No accounts in someone else's cloud. No telemetry. No third party between you and your health record. You host it, you back it up, you export it, you delete it — on your terms.
Peptide Pitstop is a private, self-hosted web app for managing peptide and GLP-1 therapy — reconstitution math, dose logging, prescriptions, bloodwork, and plasma-level modelling — installable as an offline PWA on your phone and living entirely on infrastructure you own. The dosing engine, the highest-stakes part, is exhaustively tested (600+ tests, pure decimal math, no floating-point drift).
ℹ️ Single-user today, with the data model already scoped for multi-user.
📑 Table of Contents
- 🔒 Own your own data
- 📸 Screenshots
- ✨ Features
- 🧱 Stack
- ✅ Prerequisites
- 🚀 Quickstart (local dev)
- 🐳 Deploy (self-hosted)
- 🔔 Push notifications
- 🔐 Locked out?
- 📚 Further documentation
- 🛠️ Project status & contributing
- ☕ Support
- 📄 License
- ⚠️ Disclaimer
🔒 Own your own data
This is the whole point. Health data this sensitive shouldn't live in a vendor's database you can't see.
- Runs on your own machine. A single Docker container on your own server (any Docker host — Linux, NAS, Raspberry Pi, etc.). No SaaS, no managed backend, no account on a service that can change its terms, get breached, or shut down.
- Local-only accounts. There is no public sign-up. The owner provisions the account locally; first run forces a
/setupflow to set a password and enrol TOTP. Login requires password + TOTP, with signed httpOnly session cookies. - Encryption in depth. Identifying free-text and lab values are encrypted at the application layer with AES-256-GCM before they ever touch disk; ideally the database file itself sits on an encrypted disk too. Encrypted columns are opaque — they're never used in query filters.
- No tracking, no analytics SDKs, no CDN. There is no Google Analytics, Sentry, PostHog, or any usage telemetry — nothing reports your behaviour to anyone. The app's analytics are computed locally from your database, and fonts are self-hosted (served from your own server, not Google Fonts or any CDN). The only outbound traffic is the services you configure — your Cloudflare Tunnel, your Home Assistant webhook, your Garmin sync — plus an optional dosage-reference lookup that runs only when you explicitly trigger it.
- You hold the backups. Continuous SQLite replication via Litestream to a backup location you own — plus your normal server backup routine.
- Export everything, any time. One-click CSV export for doses, lab panels, journal entries, and wearable data, plus a formatted PDF report. Your record is portable by design — never locked in.
- No open ports, no public surface. Reach it from your phone anywhere via your own Cloudflare Tunnel + Cloudflare Access policy — nothing is exposed to the open internet.
If you stop using Peptide Pitstop tomorrow, you walk away with a complete, readable copy of your data and an encryption key only you hold. That's the deal.
📸 Screenshots
All screenshots use demo seed data only (BPC-157, TB-500, Ipamorelin) — no real personal data.
Today — what's due and what's been logged, with one-tap actions

Log a dose — draw volume, syringe markings, and an injection-site map

Analytics & plasma — adherence, a dose-history heatmap, and plasma-level estimates

Bloodwork — biomarker panels, an in-range summary, and the comparison matrix

Light theme & mobile
The motorsport "pit-wall" dark theme ships alongside a clean light theme, and the whole app is self-hosted on your hardware — and on your phone.
| Light theme (Gulf) | On your phone |
|---|---|
![]() |
![]() |
![]() |
![]() |
✨ Features
Dosing — the safety-critical core
- Reconstitution engine. Concentration, draw volume, and syringe markings computed with
decimal.js— pure decimal maths, no floating-point drift. Handles reconstituted and premixed vials. - Exhaustively tested. The dosing and schedule logic is covered by a large vitest suite (600+ tests across the codebase) including property tests, real-world cases, unit-equivalence checks, and syringe-bound guardrails.
- Quick logging. Log a dose in seconds — on your phone — with a visual syringe picker and injection-site body map. Supports injections, oral peptides, and ad-hoc doses.
Protocols, prescriptions & inventory
- Protocols with titration & stacks. Multi-peptide schedules, ramping/titration steps, and stacked protocols with human-readable cadence and half-life shown inline.
- Prescriptions & vials. Full CRUD for prescriptions, vials, and preparations, with per-dose vial-volume accounting.
- Inventory & reorder. Depletion forecasting (doses remaining / days of supply) and lead-time-aware reorder status so you restock before you run dry.
Tracking & insight
- Today. A single screen of what's due and what's been logged today, with one-tap actions.
- Doses timeline. Week swimlanes, a month calendar, and day detail — with schedule rebasing (log off-schedule and snap the rest of the week back into line).
- Bloodwork. Biomarker panels with trends and a comparison matrix, backed by a curated biomarker library.
- Analytics & insights. Adherence tracking, streaks, heatmaps, and derived insights.
- Plasma modelling. Single-compartment, first-order-elimination plasma-level projections — quiet telemetry for your own regimen — from your dose history and each peptide's half-life (relative units — clearly labelled, not clinical serum levels).
- Journal & wellness. Free-text journal plus wellness logging, charted over time.
Integrations
- Push dose reminders. Native Web Push notifications from the installed PWA — per-slot reminders for multi-time schedules plus a configurable evening catch-up nag; tapping opens the app (iOS 16.4+). Optional Home Assistant webhook fallback for devices without a subscription. Free, no third-party notification service, and no dose amounts in the payload.
- Garmin wellness. A bundled sync sidecar pulls daily Garmin wellness data (steps, sleep, etc.) into the app using your own credentials, on your own schedule.
- Curated peptide library + enrichment. Built-in peptide reference data with an enrichment calculator.
Experience
- Installable PWA. Add to your home screen; works offline for the things that matter on the go.
- Pit-wall design language. A motorsport-inspired theme — carbon + race-orange, radial gauges, split headings — with light and dark modes.
🧱 Stack
Next.js (App Router) · TypeScript · Tailwind + design tokens · Prisma + SQLite · decimal.js dosing engine · otplib TOTP + jose sessions · Litestream backup · Docker + Cloudflare Tunnel.
The production deployment runs as one container bundling four services — the Next.js app, the Cloudflare tunnel, Litestream backup, and the Garmin sync sidecar — talking to each other over localhost.
✅ Prerequisites
- Node.js 22 and npm (for local development).
- Docker + Docker Compose (for self-hosting).
- A Cloudflare account with Zero Trust enabled (for secure external access — optional if you only run it on your LAN).
🚀 Quickstart (local dev)
npm install
cp .env.example .env # then fill PT_FIELD_KEY and AUTH_SECRET
npx prisma migrate dev --name init
npm run db:seed # optional: loads a sample regimen
npm run dev # http://localhost:3009
Generate the two required secrets:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" # PT_FIELD_KEY
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" # AUTH_SECRET
On first visit, /setup walks you through setting a password and enrolling TOTP.
Tests
npm test # vitest — dosing engine, schedule, analytics, auth, …
npm run typecheck
🐳 Deploy (self-hosted)
Easiest — prebuilt image (recommended, no building)
You only need Docker: install Docker Desktop on Mac/Windows (a normal point-and-click installer) or Docker Engine on Linux. Then:
- Make a folder for the app (e.g.
peptide-pitstop) and open a terminal in it. - Generate the two required secrets — run each command and copy the output:
# macOS / Linux / Git Bash — run twice openssl rand -base64 32# Windows PowerShell — run twice $b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b) - Create a file named
docker-compose.ymlin that folder, pasting your two secrets in:services: app: image: ghcr.io/kavo91/peptide-pitstop:latest container_name: peptide-pitstop restart: unless-stopped ports: - "3000:3000" volumes: - ./data:/data # your database lives here — back this folder up environment: - PT_FIELD_KEY=paste-your-first-secret-here - AUTH_SECRET=paste-your-second-secret-here - Start it:
docker compose up -d - Open http://localhost:3000 — the first visit runs a
/setupwizard (set a password + scan a QR code into an authenticator app).
Update later with docker compose pull && docker compose up -d. ⚠️ Keep PT_FIELD_KEY safe — if you lose it, encrypted fields can't be recovered.
Want it reachable from your phone outside home? Add the optional Cloudflare Tunnel (see Cloudflare Tunnel + Access below) instead of forwarding ports.
On a NAS (Unraid · CasaOS · Synology)
Ready-made templates live in deploy/ — all use the prebuilt image, default to port 3000 and a dedicated app-data volume, so they run happily alongside anything else on the box.
- Unraid —
deploy/unraid/peptide-pitstop.xml. Docker tab → Add Container → paste the template's raw URL, or install from Community Applications once it's listed. Maps/datato/mnt/user/appdata/peptide-pitstop. - CasaOS —
deploy/casaos/peptide-pitstop/docker-compose.yml. App Store → Custom Install → import the compose (includes thex-casaosmetadata so it shows an icon, description, and env prompts). - Synology (DSM 7.2+) —
deploy/synology/docker-compose.yml+ a step-by-step guide. Container Manager → Project → create from the compose. Maps/datato/volume1/docker/peptide-pitstop.
In every case: set PT_FIELD_KEY and AUTH_SECRET before first start (openssl rand -base64 32), and if you browse over plain http://<nas-ip>:3000 keep COOKIE_SECURE=false. First load runs the /setup wizard.
Build from source
Prefer to build it yourself? Two compose variants ship in this repo:
1. Simple (the default docker-compose.yml) — just the Next.js app:
# On your server, in this directory, with a populated .env:
docker compose up -d --build
- Serves on http://localhost:3000 by default.
- An optional Cloudflare Tunnel sidecar is included (commented out) to expose it publicly with no open ports — uncomment it and set
CLOUDFLARE_TUNNEL_TOKEN. - Your SQLite database lives in the
./datavolume you control (back it up; ideally keep it on an encrypted disk).
2. All-in-one (deploy/bundled/) — one "batteries-included" container running every service: the app, the Cloudflare tunnel, Litestream backup, and the Garmin sync sidecar, talking to each other over localhost. A supervisor starts them; the app is the critical process and the optional services (tunnel, Garmin) start only when their env is configured.
# Build + run the bundled image:
docker compose -f deploy/bundled/docker-compose.yml up -d --build
Set your timezone with TZ (e.g. TZ=America/New_York) so local-midnight schedules read correctly. If you use Garmin sync, make sure ./garmin-tokens is owned by uid 1001 (the in-container user).
Cloudflare Tunnel + Access
- Cloudflare Zero Trust → Networks → Tunnels → create a tunnel; put its token in
CLOUDFLARE_TUNNEL_TOKEN. - Public hostname →
http://app:3000. - Access → Applications → protect the hostname; policy = allow your email (one-time PIN) or your IdP.
Configuration
| Variable | Purpose |
|---|---|
PT_FIELD_KEY |
32-byte base64 key for AES-256-GCM field encryption |
AUTH_SECRET |
Session signing secret |
DATABASE_URL |
SQLite path (maps to the /data volume) |
OWNER_EMAIL |
Optional authenticator label for the first-run owner bootstrap (defaults to owner@example.com) |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT |
Web Push keys (npx web-push generate-vapid-keys) |
PUBLIC_APP_URL |
Absolute app URL used in relayed notification deep-links |
HA_WEBHOOK_URL |
Home Assistant webhook — fallback relay for dose reminders |
WELLNESS_IMPORT_TOKEN |
Bearer token the Garmin sidecar presents (fails closed if unset) |
CLOUDFLARE_TUNNEL_TOKEN |
Your Cloudflare Tunnel token |
GARMIN_EMAIL / GARMIN_PASSWORD |
Consumed only by the Garmin sync sidecar |
See .env.example for the full, commented list.
🔔 Push notifications
Dose reminders arrive as native push notifications from the installed app — no third-party notification service, no account, nothing leaves your server except the (amount-free) push payload. Tapping opens Peptide Pitstop directly.
What you get:
- Per-slot reminders — each scheduled time on a protocol reminds ±30 min around its own slot, including multi-time schedules (e.g. 08:00 + 20:00).
- Doses without a set time remind once at your chosen hour (default 08:00).
- Evening catch-up nag — one summary of anything still unlogged (default 18:00, or turn it off). Doses you've already logged never notify.
- Exactly-once — a claim ledger guarantees no double-sends, ever.
Setup (once per server):
npx web-push generate-vapid-keys
# Put the output in .env, then restart:
# VAPID_PUBLIC_KEY=… VAPID_PRIVATE_KEY=… VAPID_SUBJECT=mailto:you@example.com
Then on each device: install the PWA (iOS: Share → Add to Home Screen, iOS 16.4+), open it from the home-screen icon, and go to Settings → Notifications → Enable on this device → Send test. Reminder times and the nag toggle live in the same place.
Notes:
- Web Push needs HTTPS (a Cloudflare Tunnel works fine).
- Use a separate VAPID keypair per environment — push services authorise by key, not domain, so a staging box holding your production key and a copied database will push to your real phone.
- Run Home Assistant? An optional webhook relay covers devices without a subscription — see docs/ha-reminder-automation.md.
🔐 Locked out?
Single owner, no recovery email by design. Re-provision by clearing the password directly in the DB, then revisit /setup:
sqlite3 /path/to/peptides.db \
"UPDATE User SET passwordHash='', totpSecret=NULL WHERE role='owner';"
📚 Further documentation
- Dose reminder notifications — Web Push setup (VAPID + device enrolment) and the optional Home Assistant fallback relay.
Apple Health is intentionally not a built-in integration: HealthKit is device-only and a self-hosted web app cannot write to it.
🛠️ Project status & contributing
Peptide Pitstop is an actively developed, single-owner self-hosted project. Issues and discussion are welcome — please open an issue before submitting a large PR, as contributions may require a contributor agreement (see License below).
☕ Support
Peptide Pitstop is built and maintained by one person, for people who'd rather keep their health data on their own hardware than hand it to someone else's cloud (Yes, slightly hypocritical with the garmin pull).
If it's saved you from a spreadsheet — or from a subscription that wanted your bloodwork — a coffee goes a long way toward keeping an indie, self-hosted health tool alive and improving.
No pressure, and no paywalled features — every line of code stays open. Thank you. ☕
📄 License
GNU AGPL-3.0. You're free to self-host, study, modify, and redistribute — but if you run a modified version as a network service, you must make your source available under the same license. Copyright is retained by the project owner.
⚠️ Disclaimer
Peptide Pitstop is a personal tracking tool, not a medical device and not a substitute for professional medical advice. It does not diagnose, treat, or make dosing recommendations. You are responsible for your own therapy decisions. Dosing maths is provided to help you record and check your own calculations — always verify independently.
Install peptide-pitstop on Unraid in a few clicks.
Find peptide-pitstop in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Requirements
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/kavo91/peptide-pitstop:latestRuntime arguments
- Web UI
http://[IP]:[PORT:3000]- Network
bridge- Shell
sh- Privileged
- false
Template configuration
HTTP port for the Peptide Pitstop web UI.
- Target
- 3000
- Default
- 3000
- Value
- 3000
Persistent storage for the SQLite database and app data. Keep this on appdata and back it up.
- Target
- /data
- Default
- /mnt/user/appdata/peptide-pitstop
- Value
- /mnt/user/appdata/peptide-pitstop
REQUIRED. Base64-encoded 32-byte AES-256-GCM field-encryption key. Generate with: openssl rand -base64 32 -- Back this up; losing it makes encrypted fields unrecoverable. Never change it after first run.
REQUIRED. Secret used to sign login sessions. Generate with: openssl rand -hex 32 -- Changing it logs out all sessions.
REQUIRED. SQLite database location. Leave as the default so the DB lives on the persistent /data volume.
- Default
- file:/data/peptides.db
- Value
- file:/data/peptides.db
OPTIONAL. Set to false for plain-HTTP LAN access (e.g. http://tower-ip:3000) or login cookies will be rejected. Set to true only when serving over HTTPS via a reverse proxy / tunnel.
- Default
- false
- Value
- false
OPTIONAL. IANA timezone for correct dose/log timestamps, e.g. America/New_York or Australia/Brisbane.
- Target
- TZ
OPTIONAL. Home Assistant webhook URL to push dose/log events for automations. Leave blank to disable.
OPTIONAL. Bearer token that authorizes the external wellness/Garmin import endpoint. Leave blank to disable that endpoint.
OPTIONAL. Cloudflare Tunnel token to expose the app via a named tunnel from inside the container. Leave blank if you use Unraid/your own reverse proxy instead.



