A privacy-first, self-hosted, dead simple period tracking Progressive Web App. Data stays in your server of choice and you can use it on the web or on your phone as an app.
Periodt is built as a Progressive Web App, which is basically a website that can be "installed" on a device as a standalone application. This allows the users to have a nice mobile experience, without having to develop Android and iOS standalone applications and the hassle of publishing them on app stores.
- Offline support — service worker caches the app shell; API calls fall back gracefully when offline
- Home screen install — an install banner appears automatically in supported browsers (Chrome, Edge, Safari on iOS via "Add to Home Screen")
- Push notifications — Opt in in the Settings page; the backend stores subscriptions in SQLite
Create a (or add to your existing) docker-compose.yml file in a folder of your choosing:
services:
periodt:
container_name: periodt
image: ghcr.io/andreuinyu/periodt:latest # or, andreuinyu/periodt:latest
restart: unless-stopped
ports:
- "3111:8000"
volumes:
- ./periodt_data:/data
environment:
- TZ=UTC
- NOTIFY_DAYS_BEFORE=3
- NOTIFY_HOUR=9and bring it up with
docker compose up -dEdit docker-compose.yml to change the port:
ports:
- "2333:8000" # expose on port 2333or modify the environment variables to the values that suit you the best (if not provided at all, the values will default to these values shown above):
environment:
- TZ=Europe/Dublin
- NOTIFY_DAYS_BEFORE=5
- NOTIFY_HOUR=7TZshould be something like America/New_York or Europe/Dublin. Find out which one suits you best here.NOTIFY_DAYS_BEFOREconfigures how many days before the period is supposed to arrive (based off of the historic average) should notifications be sent out to subscribed users.NOTIFY_HOURconfigures at what time will the server check if notifications are to be sent out.
If instead of useing a real path of your choosing /path/to/your/periodt_data:/data to map the database file out of Docker, you are using a docker volume in the docker-compose.yml (like periodt_data:/data), you can still back it up with:
docker run --rm -v periodt_data:/data -v $(pwd):/backup alpine \
cp /data/tracker.db /backup/tracker_backup.db- Android (Chrome): tap the install banner or browser menu → "Add to Home Screen"
- iOS (Safari): Share → "Add to Home Screen"
There are many ways to route your self-hosted services through HTTPS. Amongst them:
- Reverse proxy: Nginx, Caddy, Traefik, ...
- Tunnel (mostly free) services: Cloudflare Tunnel, ngrok, Tailscale, ...
- Self-Managed TLS certificates
Any help is welcome, but especially:
- Translations: please, copy one of the existing .json in frontend/static/translations and translate all its entries to a missing language. Then, also add the necessary option in index.html (where
valueis the name of each .json):as well as in script.js, add an entry to the locale map for that language, so that dates get translated right:<select id="lang-select" class="settings-select"> <option value="en">English</option> <option value="cat">Català</option> <option value="es">Español</option> ... </select>
const localeMap = { en: 'en-US', cat: 'ca-ES', es: 'es-ES', ... };
- Design: Icons, styles, hell, even the name of this thing.
- Annoyingly obvious features a simple period tracker should have that this one doesn't.
| Layer | Tech |
|---|---|
| Backend | Python 3.14 + FastAPI |
| Database | SQLite |
| Frontend | Vanilla JS PWA |
| Container | Docker + Docker Compose |
Set yourself up with:
# 1. Clone / download this folder
cd periodt
# 2. Build and start
docker compose up --build
# 3. Open your browser
open http://localhost:3111| Method | Path | Description |
|---|---|---|
| GET | /api/cycles |
List all cycles |
| POST | /api/cycles |
Start a new cycle |
| PATCH | /api/cycles/{id} |
Update cycle (e.g. set end date) |
| DELETE | /api/cycles/{id} |
Delete a cycle |
| GET | /api/symptoms |
List symptom logs |
| POST | /api/symptoms |
Log symptoms |
| DELETE | /api/symptoms/{id} |
Delete a symptom log |
| GET | /api/predictions |
Get next period prediction |
| GET | /api/push/vapid-public-key |
Get key for push notifications |
| POST | /api/push/subscribe |
Register push subscription |
| POST | /api/push/unsubscribe |
Delete push subscription |
| GET | /api/version |
dev for local or vX.Y.Z for release |
| GET | /health |
for Docker integration |
Interactive API docs: http://localhost:3111/docs
period-tracker/
├── Dockerfile
├── docker-compose.yml # for local developing
├── README.md
├── backend/
│ ├── main.py # FastAPI app
│ ├── notifications.py # Notification handling
│ ├── log_config.json
│ └── requirements.txt
└── frontend/
├── index.html # PWA shell
├── sw.js # Service worker
└── static/
├── manifest.json
├── scripts.js
├── styles.css
├── translations/LANGUAGE.json
└── icons/
├── icon-192.png
└── icon-512.png



