翻GitHub找到了statistics-for-strava这个仓库,看着更新还挺频繁的,文档也非常完善,于是打算自己部署一下。
部署
-
首先要去Strava的API页面新创建一个APP,Authorization Callback Domain要填写自己将来部署的网站,我的是
strava.kyxie.me。创建APP成功后就会得到Client ID,Client Secret,Access Token和Refresh Token -
创建
docker-compose.yml,根据自己的情况修改,我接入了cloudflared网络部署在了公网,宿主机使用8000端口services: app: image: robiningelbrecht/strava-statistics:latest container_name: strava restart: unless-stopped volumes: - ./config:/var/www/config/app - ./build:/var/www/build - ./storage/database:/var/www/storage/database - ./storage/files:/var/www/storage/files env_file: ./.env ports: - 8000:8080 networks: - cloudflared networks: cloudflared: external: true -
创建
.env,目前STRAVA_REFRESH_TOKEN可以先不填,因为容器启动后才会使用到# The client id of your Strava app. STRAVA_CLIENT_ID=YOUR_CLIENT_ID # The client secret of your Strava app. STRAVA_CLIENT_SECRET=YOUR_CLIENT_SECRET # You will need to obtain this token the first time you launch the app. # Leave this unchanged for now until the app tells you otherwise. # Do not use the refresh token displayed on your Strava API settings page, it will not work. STRAVA_REFRESH_TOKEN=YOUR_REFRESH_TOKEN_OBTAINED_AFTER_AUTH_FLOW # Valid timezones can found under TZ Identifier column here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List TZ=America/Toronto # The UID and GID to create/own files managed by statistics-for-strava PUID=1000 PGID=1000 -
在反向代理添加域名,statistics-for-strava自动监听8080端口,我把容器名改为了
stravaSubdomain: strava Domain: your_domain Type: HTTP URL: strava:8080 -
此时可以看到创建了一个文件夹
config,进入文件夹后创建config.yaml文件,注意一定是config.yaml,不是config.ymlgeneral: # The URL on which the app will be hosted. This URL will be used in the manifest file. # This will allow you to install the web app as a native app on your device. appUrl: 'http://localhost:8080/' # Optional subtitle to display in the navbar. # Useful for distinguishing between multiple instances of the app. # Leave empty to disable. appSubTitle: null # Optional, a link to your profile picture. Will be used to display in the nav bar and link to your Strava profile. # Any image can be used; a square format is recommended. # Leave empty to disable this feature. profilePictureUrl: null # Optional, full URL with ntfy topic included. This topic will be used to notify you when a new HTML build has run. # Leave empty to disable notifications. ntfyUrl: null athlete: # Your birthday. Needed to calculate heart rate zones. birthday: 'YYYY-MM-DD' # The formula used to calculate your max heart rate. The default is Fox (220 - age). # Allowed values: arena, astrand, fox, gellish, nes, tanaka (https://pmc.ncbi.nlm.nih.gov/articles/PMC7523886/table/t2-ijes-13-7-1242/) # Or you can set a fixed number for any given date range. maxHeartRateFormula: 'fox' # If you're not sure about your zones, leave this unchanged — the defaults are sensible. heartRateZones: # Relative or absolute. # Relative will treat the zone numbers as percentages based on your max heart rate, while absolute will treat them as actual heartbeats per minute. # This mode will apply to all heart rate zones you define. mode: relative # The default zones for all activities. default: zone1: from: 50 to: 60 zone2: from: 61 to: 70 zone3: from: 71 to: 80 zone4: from: 81 to: 90 zone5: from: 91 to: null # Infinity and beyond. # 🔥 PRO tip: You can further refine your heart rate zones by specifying date ranges and sport types. # Read more about the possibilities on https://statistics-for-strava-docs.robiningelbrecht.be/#/configuration/main-configuration?id=athlete-heart-rate-zones # maxHeartRateFormula: # "2020-01-01": 198 # "2025-01-10": 193 # History of weight (in kg or pounds, depending on appearance.unitSystem). Needed to calculate relative w/kg. # Make sure to replace the YYYY-MM-DD examples with your own weight history. # Read more about the weight history on https://statistics-for-strava-docs.robiningelbrecht.be/#/configuration/main-configuration?id=athlete-weight-history weightHistory: "YYYY-MM-DD": 100 # Optional, history of FTP. Needed to calculate activity stress level. # ftpHistory # "2024-10-03": 198 # "2025-01-10": 220 # ftpHistory: [] appearance: # Allowed options: en_US, fr_FR, it_IT, nl_BE, de_DE, pt_BR, pt_PT or zh_CN locale: 'en_US' # Allowed options: metric or imperial unitSystem: 'metric' # Time format to use when rendering the app # Allowed formats: 24 or 12 (includes AM and PM) timeFormat: 24 # Date format to use when rendering the app # For valid PHP date formats: https://www.php.net/manual/en/datetime.format.php # If you don't know how to use these formats, leave this unchanged — the defaults are sensible. dateFormat: short: 'd-m-y' # This renders to 01-01-25 normal: 'd-m-Y' # This renders to 01-01-2025 import: # Strava API has rate limits (https://statistics-for-strava-docs.robiningelbrecht.be/#/troubleshooting/faq?id=why-does-it-take-so-long-to-import-my-data), # to make sure we don't hit the rate limit, we want to cap the number of new activities processed # per import. Considering there's a 1000 request per day limit and importing one new activity can # take up to 3 API calls, 250 should be a safe number. numberOfNewActivitiesToProcessPerImport: 250 # Sport types to import. Leave empty to import all sport types # With this list you can also decide the order the sport types will be rendered in. # A full list of allowed options is available on https://statistics-for-strava-docs.robiningelbrecht.be/#/configuration/main-configuration?id=supported-sport-types sportTypesToImport: [] # Activity visibilities to import. Leave empty to import all visibilities # This list can be combined with sportTypesToImport. # Allowed values: ["everyone", "followers_only", "only_me"] activityVisibilitiesToImport: [] # Optional, the date (YYYY-MM-DD) from which you want to start importing activities. # Any activity recorded before this date, will not be imported. # This can be used if you want to skip the import of older activities. Leave empty to disable. skipActivitiesRecordedBefore: null # An array of activity ids to skip during import. # This allows you to skip specific activities during import. # ["123456789", "987654321"] activitiesToSkipDuringImport: [] metrics: # By default, the app calculates your Eddington score for Rides, Runs, and Walks. # Each category includes a list of sport types used in the calculation. # This setting lets you customize which sport types are grouped together and how the Eddington score is calculated. # If you're not familiar with the Eddington score, it's best to leave this as is for now and explore it once the app is running. # 🔥 PRO tip: it's possible to use the same sport type over multiple eddington numbers. eddington: # The label to be used for the tabs on the Eddington page. - label: 'Ride' # A boolean to indicate if this score should be displayed in the side navigation. # You can only enable two of these at the same time. showInNavBar: true # The sport types to include in the Eddington score for this tab. # Only sport types that belong to the same activity type (category) can be combined. # For a complete list of supported sport and activity types, visit: https://statistics-for-strava-docs.robiningelbrecht.be/#/configuration/main-configuration?id=supported-sport-types sportTypesToInclude: ['Ride', 'MountainBikeRide', 'GravelRide', 'VirtualRide'] - label: 'Run' showInNavBar: true sportTypesToInclude: ['Run', 'TrailRun', 'VirtualRun'] - label: 'Walk' showInNavBar: false sportTypesToInclude: ['Walk', 'Hike'] zwift: # Optional, your Zwift level (1 - 100). Will be used to render your Zwift badge. Leave empty to disable this feature level: null # Optional, your Zwift racing score (0 - 1000). Will be used to add to your Zwift badge if zwift.level is filled out. racingScore: null -
启动容器
docker compose up -d -
就得到了文档里说的要求你更新Refresh Token,登录成功后会告诉你你的refresh token,然后粘贴在
.env中。如果返回了Bad Request,说明在创建APP的时候Callback Domain填错了 -
之后再回到项目部署的域名,会让你导入数据,运行
docker compose exec app bin/console app:strava:import-data由于Strava Rate Limit有限制,15min里最多Call 100个API,且一天做多2000 Call个API,且上传一条activity应该需要消耗3个API,因此这条命令是压着速度运行的。建议:
- 使用screen,防止一不小心中断丢失console
- 最开始在
config.yaml中设置numberOfNewActivitiesToProcessPerImport为250,等上传完第一轮后,假设还有activities没上传完,隔天再设置为500,这样系统会从断点继续上传,等初始的activities全部上传完之后再重新调回250
-
上传完之后构建Web页面
docker compose exec app bin/console app:strava:build-files -
重启容器
docker compose down docker compose up -d -
图表很漂亮,而且包含了很多Strava会员才有的功能
同步
上述操作步骤可以允许我们在内网和外网同时查看,如果希望每次骑完车都可以自动上传,上传后本地自动拉取,可以使用宿主机的crontab,官网文档有教程:Statistics for Strava | Scheduling
也可以使用Strava的Webhook,我自己写了个小工具:GitHub - Kyxie/strava-webhook,本地起一个服务,Strava向本地发送请求的时候转发给同网络的Strava容器,监督它自动更新。
-
首先修改
.env,添加一些字段# The client id of your Strava app. STRAVA_CLIENT_ID=YOUR_CLIENT_ID # The client secret of your Strava app. STRAVA_CLIENT_SECRET=YOUR_CLIENT_SECRET # You will need to obtain this token the first time you launch the app. # Leave this unchanged for now until the app tells you otherwise. # Do not use the refresh token displayed on your Strava API settings page, it will not work. STRAVA_REFRESH_TOKEN=YOUR_REFRESH_TOKEN_OBTAINED_AFTER_AUTH_FLOW # Valid timezones can found under TZ Identifier column here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List TZ=America/Toronto # The UID and GID to create/own files managed by statistics-for-strava PUID=1000 PGID=1000 # Webhook STRAVA_VERIFY_TOKEN=YOUR_VERIFY_TOKEN STRAVA_CALLBACK_URL=YOUR_STRAVA_CALLBACK_URL -
生成一个
verify token,把这个token保存到.env中openssl rand -hex 16 -
这个
STRAVA_CALLBACK_URL是要部署的url + /webhook,假设我项目部署在https://strava.kyxie.me,这里我就填https://strava.kyxie.me/webhook -
修改
docker-compose.yml,把webhook服务添加进来services: app: image: robiningelbrecht/strava-statistics:latest container_name: strava restart: unless-stopped volumes: - ./config:/var/www/config/app - ./build:/var/www/build - ./storage/database:/var/www/storage/database - ./storage/files:/var/www/storage/files env_file: ./.env ports: - 8000:8080 networks: - cloudflared # 新添加 webhook: image: kyxie/strava-webhook:latest container_name: strava-webhook restart: unless-stopped env_file: ./.env volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - 8001:8001 networks: - cloudflared networks: cloudflared: external: true -
需要把Cloudflare的Bot Fight Mode给关掉,位于Cloudflare → Security → Bots → Bot Fight Mode,关掉之后建议再加上一些WAF的防护措施
-
这个小工具还可以管理Strava Webhook的注册情况,在miniPC上运行下面代码就是注册Webhook
curl -X POST http://localhost:8001/subscription/register-
同样地,取消订阅Webhook
curl -X POST http://localhost:8001/subscription/unregister -
查看状态
curl http://localhost:8001/subscription/status
-
-
由于必须要暴露
/webhook接口,最好再在Cloudflare WAF上加点限流保护 -
部署好之后有了新的Activities就可以自动更新了
隐私
然而这样直接部署到公网我觉得太暴露隐私了,所有人都能看到你所有的运动记录,代码作者似乎觉得这个系统只能一个人用,因此并没有做登录系统,好在我们可以用Nginx来解决。
Nginx
-
创建
nginx文件夹,创建docker-compose.ymlmkdir nginx cd nginx touch docker-compose.yml -
生成一个密码,用于nginx的basic auth
openssl passwd -apr1 "your_password" echo '<your_username>:<output_password>' > .htpasswd -
编辑
docker-compose.yml,让nginx也接入cloudflared网络,宿主机使用8080端口services: nginx: image: nginx:alpine container_name: nginx restart: unless-stopped ports: - "8080:80" volumes: - ./conf.d:/etc/nginx/conf.d:ro - ./.htpasswd:/etc/nginx/.htpasswd networks: - cloudflared networks: cloudflared: external: true -
创建文件夹
conf.d,进入文件夹然后创建strava.confmkdir -p conf.d cd conf.d touch strava.conf -
编辑
strava.conf,由于都在同一个Docker网络下,只需要写容器名即可,且应该使用容器内的端口号,我们暴露图片和webhook的路径,以及给根目录加上basic authserver { listen 80; server_name strava.kyxie.me; location = /files/strava-badge.svg { proxy_pass http://strava:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location = /files/rides-this-week.svg { proxy_pass http://strava:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location = /files/monthly-distance.svg { proxy_pass http://strava:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location = /webhook { proxy_pass http://strava-webhook:8001; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /files/ { return 403; } location / { auth_basic "Restricted Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://strava:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } -
去cloudflared上修改
Subdomain: strava Domain: your_domain Type: HTTP URL: nginx:80 -
这样就可以既能公网访问图片,就禁止别人看我的所有训练活动了,如果自己想看的话可以登录密码
-
其他的用户登录控制见:为部署的服务添加用户登录 | Kunyang’s Blog
小工具
添加体重
-
写了个脚本使用命令行添加体重,如果不指定日期就是默认为今天
#!/bin/bash # 使用方法: sudo ./add-weight.sh 82 [2025-07-02] # 如果不指定日期,则默认使用今天 set -e # 参数 WEIGHT="$1" DATE="${2:-$(date +%F)}" # 空值检查 if [ -z "$WEIGHT" ]; then echo "错误:必须提供体重数值" echo "用法: ./add-weight.sh <体重> [日期,如 2025-07-02]" exit 1 fi # YAML 路径 CONFIG_FILE="config/config.yaml" # 插入体重记录 awk -v date="$DATE" -v weight="$WEIGHT" ' BEGIN { inserted = 0 } { print $0 if ($0 ~ /^ *weightHistory:/ && inserted == 0) { inserted = 1 print " \"" date "\": " weight } } ' "$CONFIG_FILE" > tmp.yaml && mv tmp.yaml "$CONFIG_FILE" echo "已插入体重记录 $DATE: $WEIGHT kg" -
更新权限
chmod +x add-weight.sh