peptide-pitstop

peptide-pitstop

Docker app from kavo's Repository

Overview

Peptide Pitstop is a free, open-source, self-hosted peptide & GLP-1 tracker. Log doses, titration schedules, protocols, blood-work, and wellness metrics in a single-user private dashboard. Data stays on your server in an encrypted SQLite database (AES-256-GCM field encryption). First run opens a /setup wizard to create your password + TOTP. Required environment variables (PT_FIELD_KEY and AUTH_SECRET) MUST be set to long random secrets before first run - see the Project page for how to generate them. Set COOKIE_SECURE to false if you access the UI over plain HTTP on your LAN (without a reverse proxy / HTTPS), otherwise login will fail.

Peptide Pitstop

License: AGPL-3.0 Next.js TypeScript Prisma Tailwind CSS PWA Buy Me a Coffee

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

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 /setup flow 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 Today

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

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

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

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
Today — light Today — mobile
Analytics — light Analytics — mobile

✨ 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:

  1. Make a folder for the app (e.g. peptide-pitstop) and open a terminal in it.
  2. 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)
    
  3. Create a file named docker-compose.yml in 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
    
  4. Start it:
    docker compose up -d
    
  5. Open http://localhost:3000 — the first visit runs a /setup wizard (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.

  • Unraiddeploy/unraid/peptide-pitstop.xml. Docker tab → Add Container → paste the template's raw URL, or install from Community Applications once it's listed. Maps /data to /mnt/user/appdata/peptide-pitstop.
  • CasaOSdeploy/casaos/peptide-pitstop/docker-compose.yml. App Store → Custom Install → import the compose (includes the x-casaos metadata 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 /data to /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 ./data volume 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

  1. Cloudflare Zero Trust → Networks → Tunnels → create a tunnel; put its token in CLOUDFLARE_TUNNEL_TOKEN.
  2. Public hostname → http://app:3000.
  3. 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

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.

Buy Me a Coffee

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.

Open the Apps tab on your Unraid server Search Community Apps for peptide-pitstop Review the template variables and paths Click Install

Requirements

You MUST set the PT_FIELD_KEY and AUTH_SECRET variables to strong random secrets before starting the container. Generate PT_FIELD_KEY (base64 32-byte key) with: openssl rand -base64 32 -- Generate AUTH_SECRET with: openssl rand -hex 32 -- Keep both secret and back them up; losing PT_FIELD_KEY makes encrypted data unrecoverable.

Categories

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/kavo91/peptide-pitstop:latest
Last Updated2026-07-02
First Seen2026-06-29

Runtime arguments

Web UI
http://[IP]:[PORT:3000]
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

HTTP port for the Peptide Pitstop web UI.

Target
3000
Default
3000
Value
3000
App Data (/data)Pathrw

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
PT_FIELD_KEYVariable

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.

AUTH_SECRETVariable

REQUIRED. Secret used to sign login sessions. Generate with: openssl rand -hex 32 -- Changing it logs out all sessions.

DATABASE_URLVariable

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
COOKIE_SECUREVariable

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
TZ (Timezone)Variable

OPTIONAL. IANA timezone for correct dose/log timestamps, e.g. America/New_York or Australia/Brisbane.

Target
TZ
HA_WEBHOOK_URLVariable

OPTIONAL. Home Assistant webhook URL to push dose/log events for automations. Leave blank to disable.

WELLNESS_IMPORT_TOKENVariable

OPTIONAL. Bearer token that authorizes the external wellness/Garmin import endpoint. Leave blank to disable that endpoint.

CLOUDFLARE_TUNNEL_TOKENVariable

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.