4.8 KiB
Dokku deployment
App: stride; database: stride-db; domain: stride.peterstockings.com.
Training-signal releases migrate private daily recovery and session preferences automatically. Back up PostgreSQL before release, then recompute older recordings:
dokku run stride env FLASK_APP=app flask backfill-analysis --batch-size 20
dokku run stride env FLASK_APP=app flask install-training-signals --email ACCOUNT
dokku run stride env FLASK_APP=app flask backfill-health --email ACCOUNT --days 90
The installer validates queries before saving, pins five analyses and preserves existing edits on repeat runs. Pace and long-run thresholds are editable CLI options and account queries. Recovery backfill uses the existing encrypted session and account lock, enables recovery only for that account and leaves missing watch observations missing. Other accounts can opt in through Settings; recovery failures do not stop activity imports. Never run a local copy of the same Garmin session concurrently with production. Analysis version 3 persists continuous windows; no dashboard query loads full recording samples. The Dockerfile installs locked dependencies on Python 3.12. Dokku runs database migrations in the predeploy step, then one web container and one sync worker.
Mount /var/lib/dokku/data/storage/stride:/storage with ownership 1000:1000 and
mode 0700. Set APP_ENV=production, INSTANCE_PATH=/storage/instance,
DATA_ROOT=/storage/data, SECRET_KEY (random secret), TOKEN_ENCRYPTION_KEY
(Fernet key). Linking PostgreSQL supplies DATABASE_URL. Preserve the encryption
key separately from database backups; losing it requires Garmin reconnection.
Push to Gitea, then deploy with dokku git:sync --build stride https://gitea.peterstockings.com/peterstockings/Stride.git main on the server.
Map the web port with dokku ports:set stride http:80:8000, then enable HTTPS
using dokku letsencrypt:enable stride.
Sync
Upload a garminconnect 0.3.17 session JSON in Settings & imports. Credentials are validated with Garmin, encrypted, and assigned to the authenticated owner. Manual sync queues durable work; the worker polls every 30 seconds. Automatic sync runs hourly and checks the latest 100 activities, reusing download manifests and unique activity source keys to avoid duplicates. Older activities can be imported with FIT uploads or the operator's local import command.
Shared storage file locks prevent overlapping jobs and disconnect races. A crash releases the lock; due or pending work is retried after restart. Refresh tokens are persisted before downloading and after download failures. Transient failures back off up to six hours; rejected authentication pauses scheduling until the user reconnects and enables it again. Disconnect removes credentials and keeps previously imported activities. Only garmin.com is supported by the web connector.
Inspect dokku logs stride -p worker, the status in Settings, and /health.
Run dokku run stride flask --app app sync-worker --once for an operator check.
Do not run the local sync simultaneously using a copy of the same Garmin session.
Back up PostgreSQL, /storage, and configuration securely. Rate limiting still
uses per-process memory; email verification and password recovery are not yet
implemented. For a larger public deployment configure shared rate-limit storage.
Validation
Local automated suite: 39 passing tests, including manual/scheduled jobs, encryption, retry/authentication failure, locking, CSRF, and access control. Live verification on 2026-10-01:
- Built and deployed on Dokku 0.38.23; all PostgreSQL migrations succeeded.
- HTTPS certificate issued; external
/healthreturned HTTP 200 andstatus: ok. - Migrated the existing local account and 18 activities, preserving its password hash.
- Reused the local Garmin session on the Linux server, saved it encrypted, and imported one new activity (19 total).
- Submitted a manual sync to the public HTTPS endpoint with an authenticated session, CSRF token, and same-origin Referer. The background worker completed it.
- Moved
next_syncinto the past withpending=falseand scheduling enabled. The running worker picked it up automatically and completed the scheduled run. This exercises the schedule due check without waiting an hour. - Both repeat syncs imported zero duplicates and reported zero failures.
- Authenticated dashboard returned 200; anonymous private activity access returned 404.
- Production uses one web container (two Gunicorn processes) and one worker; hourly scheduling remains enabled. The local session copy should no longer be used concurrently with the server.
- Restarted web and worker successfully; all 19 activities, encrypted connection, last success, and next hourly schedule persisted. Temporary migration database and plaintext session copies were removed from the server.