Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
258 lines (247 loc) · 13.1 KB
/
Copy path.env.example
File metadata and controls
258 lines (247 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# Copy this file to ".env" and adjust for your setup, then: docker compose up -d
#
# ── Local testing (default) ────────────────────────────────────────────────
# Works out of the box on the machine running Docker. Open http://localhost:8080
RP_ID=localhost
ORIGIN=http://localhost:8080
WEB_PORT=8080
RP_NAME=openGym
# ── Real deployment behind your own HTTPS domain ───────────────────────────
# Passkeys are bound to the exact hostname, and browsers only allow them over
# HTTPS (localhost is the one exception). Put openGym behind a reverse proxy /
# tunnel that terminates TLS for your domain, then set:
#
# RP_ID=gym.example.com
# ORIGIN=https://gym.example.com
# WEB_PORT=8080
# RP_NAME=openGym
#
# (Point your proxy at the web container's WEB_PORT. See docs/SELF_HOSTING.md.)
# ── Admin dashboard & invite-only signup (optional) ────────────────────────
# Both off by default — a fresh instance has open signup and no admin.
#
# Make yourself an admin: register your own passkey profile first, find your
# user id in ./data/db.json ("users"[].id), then list it here (comma-separated
# for several admins). Admins get an "Admin dashboard" link in Settings to see
# users, disable accounts, generate invite codes and view workout history.
#
# ADMIN_UIDS=youruserid,anotheradmin
#
# Require an invite code to create new profiles (existing accounts keep working;
# generate the codes from the admin dashboard):
#
# INVITE_ONLY=1
#
# Remove the "Continue without account" button, so a profile is the only way in.
# Guest mode keeps everything in the browser and never touches this server, but
# on an instance meant for a known set of people it is still the wrong front
# door. Anyone already inside as a guest is signed out on their next visit; the
# data they made stays in that browser. Defaults to on (guests allowed):
#
# ALLOW_GUEST=0
# ── Default language (optional) ────────────────────────────────────────────
# For an instance whose people share a language: the sign-in screen, and every
# profile that has never picked a language in Settings, start in this one
# instead of English (or the browser's language). Anyone's own choice in
# Settings still wins. Use a code from Settings → Language: en, de, de-CH, es,
# fr, it, pt, pt-BR, pl, tr, ru, uk, zh, ko, hi, th, hu, ar.
#
# DEFAULT_LANG=pt-BR
# ── Password sign-in (optional, off by default) ────────────────────────────
# Lets people sign in with their profile name and a password next to passkeys:
# for browsers that cannot make a passkey (plain http:// on a LAN address, some
# Firefox setups) and for recovery — an admin can hand out a one-time reset
# code from the dashboard. Nobody has a password until they set one in
# Settings. Passkeys stay the default and the safer choice; read
# docs/SELF_HOSTING.md ("Password sign-in") before turning this on.
#
# PASSWORD_LOGIN=1
#
# Wrong passwords are throttled per name and per address. The address is read
# from X-Forwarded-For / CF-Connecting-IP only when TRUST_PROXY is on; the
# bundled docker-compose.yml turns it on, because there the API is reachable
# only through the web container, which overwrites those headers. Running the
# API some other way, leave it off unless whatever sits in front overwrites
# (never appends to or passes through) those headers:
#
# TRUST_PROXY=1
# ── Activity log (optional) ────────────────────────────────────────────────
# The admin dashboard shows an activity log: who signed in, who tried and
# failed, and every admin action (disabling an account, creating or revoking an
# invite code). It lives in ./data/audit.log, one JSON object per line, so you
# can also just read it with `tail` or `jq`.
#
# It is ON by default. It records strictly less than this instance already
# holds — every profile is in db.json and every workout is in state-<uid>.json,
# both readable by any admin — and a log that ships switched off tells you
# nothing on the day you need it. Turn it off completely with:
#
# AUDIT_LOG=0
#
# Retention is a cap, not an archive. Whichever limit is reached first wins;
# set either to 0 to disable that limit:
#
# AUDIT_MAX=5000 # events kept
# AUDIT_DAYS=90 # days kept
#
# IP addresses are the one thing that is OFF unless you ask, because they are
# the only field here that says where somebody physically is. They are also the
# only way to tell one failed sign-in from twenty. Three settings:
#
# AUDIT_IP=off # default — no addresses at all
# AUDIT_IP=net # network only: 203.0.113.0/24, 2001:db8:1::/48
# AUDIT_IP=full # the whole address
#
# The address is read from CF-Connecting-IP, then the first entry of
# X-Forwarded-For, then X-Real-IP, and finally the connecting socket itself if
# none of those is present.
#
# A forwarded-for header is only worth anything if whatever sits in front
# OVERWRITES it instead of passing a client-supplied one through — otherwise a
# caller simply states the address it would like logged. The web container now
# overwrites X-Forwarded-For and X-Real-IP with the real peer, and DROPS
# CF-Connecting-IP, so the bundled stack records an address nobody can choose.
# (Before, X-Forwarded-For was appended to rather than replaced, and
# CF-Connecting-IP was passed straight through — both were forgeable.)
#
# Cloudflare is first in that list on purpose: a Cloudflare tunnel does not put
# the visitor in X-Forwarded-For at all, so that header would otherwise record
# the tunnel itself and look perfectly plausible. If — and only if — Cloudflare
# is genuinely in front of you, let its header through again:
#
# CF_CONNECTING_IP=$http_cf_connecting_ip
#
# That is safe there because Cloudflare sets the header itself, replacing
# anything the client sent. Anywhere else, leave it unset.
# ── Session length (optional) ──────────────────────────────────────────────
# How long a sign-in lasts, in days. Default 90 — long enough that people who
# train regularly never see a login screen, short enough that a stolen cookie
# doesn't stay good forever. Lower it for an instance on the open internet,
# raise it for a private one. Only affects new sign-ins; cookies already handed
# out keep the lifetime they were issued with. Users can end every session on
# every device themselves with "Sign out everywhere" in Settings.
#
# SESSION_DAYS=90
# ── Fitting into an existing stack (optional) ──────────────────────────────
# The defaults assume openGym is on its own: an API service called `api` on
# port 3000, and nginx listening on 80 inside the web container. If you are
# merging this into a compose file that already has an `api`, or you front the
# web container with your own reverse proxy, move them here. The web image
# renders its nginx config at start-up, so these apply to a prebuilt image and
# need no rebuild. BACKEND and PORT together are what /api is proxied to, so
# they must name a service the web container can reach on your network:
#
# NGINX_PORT=80
# BACKEND=api
# PORT=3000
#
# Serving openGym under a subpath (https://example.com/gym/)? If your reverse
# proxy strips the prefix before the container sees it, set nothing — the app
# asks its own address for the API. If it passes the prefix through, name it
# here without a trailing slash:
#
# BASE_PATH=/gym
# ── Where the data lives (optional) ────────────────────────────────────────
# DATA_DIR is the one directory the API stores anything in: db.json (accounts,
# passkey credentials, push subscriptions, invites), state-<uid>.json per
# profile, `secret` (the key every session cookie is signed with), vapid.json,
# audit.log, uploads/ (the photos and videos of custom exercises), and the
# Coach's coach.json and coach/ when it is on. Back up that
# directory and you have backed up the instance; lose `secret` and everyone is
# signed out. Default /data.
#
# Under docker compose it is pinned to /data in docker-compose.yml and the host
# side of the volume is ./data — so to keep the data somewhere else, change the
# volume line rather than this variable, which the compose file overrides. The
# variable is what you set when the API runs outside that file: `node server.js`
# on a host, or a stack of your own.
#
# It has to be writable by the API: every save goes through it, and a read-only
# mount or a full disk is a write that cannot land.
#
# DATA_DIR=/data
# ── Photos and videos of custom exercises (optional) ───────────────────────
# People can give an exercise they made a photo, a GIF or a short video. It is
# stored on this server under ./data/uploads/<profile>/ and only its owner can
# read it back. On by default with the limits below; every MB is 1024×1024
# bytes. Turn it off entirely (the app then offers a link field only):
#
# MEDIA_UPLOADS=0
#
# Space per profile (0 = no cap), and per file. Photos are shrunk on the
# device before upload (1600 px), so 2 MB is plenty; videos are not shrunk.
#
# MEDIA_QUOTA_MB=200
# MEDIA_IMAGE_MAX_MB=2
# MEDIA_GIF_MAX_MB=8
# MEDIA_VIDEO_MAX_MB=40
# MEDIA_VIDEO_MAX_SEC=60
#
# A file nobody's exercise uses any more is deleted after this many days (other
# devices may still show it until they sync):
#
# MEDIA_GC_GRACE_DAYS=14
#
# Uploads per profile per hour, and the free disk space below which uploads
# are refused so a full disk cannot take the rest of the instance with it.
# On an instance with open signup, lower MEDIA_QUOTA_MB too: the worst case is
# the quota times the number of profiles.
#
# MEDIA_UPLOADS_PER_HOUR=600
# MEDIA_MIN_FREE_MB=512
#
# The web container's own cap on an upload. Keep it above MEDIA_VIDEO_MAX_MB,
# and give any reverse proxy in front the same room (docs/SELF_HOSTING.md):
#
# MEDIA_UPLOAD_MAX=48m
# ── Notifications (optional) ───────────────────────────────────────────────
# Push works out of the box — nothing to set. VAPID keys are generated on first
# run and saved to ./data/vapid.json. Each user's browser reports its own
# timezone automatically, so the workout-day reminder fires at their local
# time regardless of where the server runs or where they're travelling.
#
# Push services want a contact address for whoever operates the server, in case
# they need to get in touch about your pushes. Defaults to your ORIGIN, which is
# fine; set a mailto: if you would rather they mailed you:
#
# VAPID_SUBJECT=mailto:you@example.com
# ── AI Coach (optional, off by default) ────────────────────────────────────
# The Coach stays off until an admin turns it on in Settings → Admin and
# connects a provider. An instance that never does is byte-for-byte the same
# app it was before the feature existed.
#
# Anthropic, OpenAI, Gemini and any OpenAI-compatible endpoint (Ollama, LM
# Studio, vLLM, OpenRouter…) are driven over plain HTTPS with an API key and
# need nothing that is not already in the default image. Nothing to set here.
#
# API_TARGET only matters if you want the Claude Agent SDK or the Codex CLI
# running inside the container. `default` carries no AI runtime at all — no
# Agent SDK, no Codex, no bubblewrap. `coach` adds them (~300 MB):
#
# API_TARGET=coach docker compose up -d --build api
#
# COACH_DISABLED is the kill switch that outranks the admin toggle. Set it when
# you want the Coach off for certain — after an incident, or on an instance
# where nobody should be able to turn it on:
#
# COACH_DISABLED=1
#
# COACH_JOB_TIMEOUT_MS gives a job longer than the default five minutes. Only a
# local model on a CPU needs it — and normally only for the first request after
# the model server restarts (the api pre-warms the rules cache after that; a
# warm review on a 3B model takes one to a few minutes, depending on the CPU).
# A hosted API that needs
# more has a problem. Never goes below one minute.
#
# COACH_JOB_TIMEOUT_MS=1500000
#
# Credentials never live here. They are encrypted into ./data/coach.json, one
# per provider, or for the Codex provider cached in ./coach-auth — a sibling of ./data, never inside it,
# so a live sign-in does not ride along in the documented backup archive.
# See docs/AI_COACH.md.
# ── Hevy map regeneration (developers only) ────────────────────────────────
# Not used by the running app. End users paste their own key in Settings →
# Import from Hevy. Set this only to regenerate frontend/src/lib/hevy-id-map.js:
#
# HEVY_API_KEY=
# node scripts/build-hevy-id-map.mjs