-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathrender.yaml
More file actions
396 lines (390 loc) · 15.6 KB
/
Copy pathrender.yaml
File metadata and controls
396 lines (390 loc) · 15.6 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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
previews:
generation: automatic
databases:
- name: mapsurvey-db
plan: basic-256mb
databaseName: mapsurvey
user: mapsurvey
postgresMajorVersion: 16
ipAllowList: []
previewPlan: basic-256mb
services:
- type: redis
name: mapsurvey-redis
plan: free
# Render allows one free Key Value instance per workspace and production
# holds it, so previews must use a paid plan (prorated for the hours the
# preview lives) or their whole sync fails on this resource.
previewPlan: starter
ipAllowList: []
- type: web
name: mapsurvey
runtime: docker
plan: starter
dockerfilePath: ./Dockerfile
dockerContext: .
# Migrations run here — after the new image is built, before the running instance is
# replaced — so schema changes land while the current version is still serving, and a
# failing migration aborts the deploy instead of taking the site down. The service
# mounts a disk, which rules out Render's zero-downtime deploy (a disk attaches to one
# instance at a time), so everything that can leave the start path has to.
#
# Only the web service gets this: the worker and the cron share the image and must not
# migrate.
#
# It has to be a script, not an inline command list: Render does NOT run this through a
# shell, so `migrate && createsuperuser || true` was parsed as arguments to migrate and
# the pre-deploy died with "unrecognized arguments: manage.py createsuperuser || true".
preDeployCommand: "sh ./predeploy.sh"
envVars:
- key: DATABASE_URL
fromDatabase:
name: mapsurvey-db
property: connectionString
- key: SECRET_KEY
generateValue: true
- key: DEBUG
value: "0"
- key: DJANGO_ALLOWED_HOSTS
value: "localhost mapsurvey.org mapsurvey.render.com *.render.com"
- key: MAPBOX_ACCESS_TOKEN
sync: false
previewEnvRenderServiceName: mapsurvey
# AI survey draft generation. The web service only needs the provider
# credentials to decide whether to render the AI panel; the worker below
# makes the actual calls. A provider with no key = feature simply off.
# AI_PROVIDER is declared (not left to the settings.py default) because
# the gate is per-provider: with 'anthropic' set and only a Gemini key
# present the panel would never render.
- key: AI_PROVIDER
value: gemini
- key: GEMINI_API_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
- key: GEMINI_MODEL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: ANTHROPIC_API_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
# Off in production since 2026-08-17 evening, by product decision, after
# a day of measuring Gemini's SSE in anger: 47% of streamed calls failed
# (mid-answer cuts, stalls) in the morning batch, and one creator hit
# four consecutive failures — a dead end even through the retries. The
# blocking call has neither stream-specific failure mode, and at thinking
# `low` the wait is short enough for an indeterminate bar. Previews keep
# streaming on so the path stays exercised; reconsider against fresh
# $ai_generation error-rate data, not memory.
- key: AI_STREAMING_ENABLED
value: "false"
previewValue: "true"
- key: DEFAULT_FROM_EMAIL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: DEMO_SURVEY_URL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: DJANGO_SUPERUSER_USERNAME
sync: false
previewEnvRenderServiceName: mapsurvey
- key: DJANGO_SUPERUSER_EMAIL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: DJANGO_SUPERUSER_PASSWORD
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_BACKEND
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_PORT
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_USE_SSL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST_USER
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST_PASSWORD
sync: false
previewEnvRenderServiceName: mapsurvey
- key: CELERY_BROKER_URL
fromService:
type: redis
name: mapsurvey-redis
property: connectionString
- key: CELERY_RESULT_BACKEND
fromService:
type: redis
name: mapsurvey-redis
property: connectionString
- key: REDIS_URL
fromService:
type: redis
name: mapsurvey-redis
property: connectionString
- key: TURNSTILE_SITE_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
- key: TURNSTILE_SECRET_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
- key: CLOUDFLARE_TRUSTED
value: "True"
# Attempts that pass form validation.
- key: REGISTRATION_RATE_LIMIT_HOUR
value: "3"
- key: REGISTRATION_RATE_LIMIT_DAY
value: "10"
# Attempts rejected by form validation — a separate, far looser budget so
# someone fighting the password field is never locked out. Nobody
# legitimate should reach these; watch AbuseEvent(detail='invalid_hour').
- key: REGISTRATION_INVALID_LIMIT_HOUR
value: "15"
- key: REGISTRATION_INVALID_LIMIT_DAY
value: "50"
# Kill switch for the split above. Set to "False" from the dashboard to
# restore the previous single-counter behaviour without a rebuild.
- key: REGISTRATION_SPLIT_RATE_LIMIT
value: "True"
- key: NEWSLETTER_SITE_URL
value: "https://mapsurvey.org"
# Gunicorn concurrency — tunable from the dashboard without an image rebuild.
# Slots = WEB_CONCURRENCY × GUNICORN_THREADS (gthread); sized for the 512 MB
# Starter instance (~110 MB per worker, measured 143 MB steady with one).
- key: WEB_CONCURRENCY
value: "2"
- key: GUNICORN_THREADS
value: "4"
- key: GUNICORN_TIMEOUT
value: "60"
# Internal product analytics. Deliberately WITHOUT previewEnvRenderServiceName:
# inheriting the production key would make every PR preview write test traffic
# into the real project. Previews leave it unset, and unset renders no snippet.
- key: POSTHOG_PROJECT_KEY
sync: false
- key: POSTHOG_API_HOST
sync: false
# First-party host for browser events only (managed reverse proxy). Unset falls
# back to POSTHOG_API_HOST, so a preview that never got the CNAME behaves as before.
- key: POSTHOG_CLIENT_HOST
sync: false
# Session replay of the editor. Unset = off, which is what previews get: they
# never carry a real key, and a recording posture should not be inherited.
- key: POSTHOG_SESSION_REPLAY
sync: false
# Media object storage. Unset USE_S3 = uploads go to the disk below, which
# is how this ships: the switch is flipped from the dashboard only after
# the existing files have been copied into the bucket and verified.
#
# Previews inherit the bucket and credentials but NOT the key prefix —
# MEDIA_S3_NAMESPACE is deliberately absent here, so settings.py derives
# previews/<service> from Render's own IS_PULL_REQUEST/RENDER_SERVICE_NAME.
# A preview therefore cannot address, overwrite or delete a production key.
- key: USE_S3
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_STORAGE_BUCKET_NAME
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_S3_REGION_NAME
value: ap-southeast-2
# Credentials belong to a bucket-scoped IAM user and are secrets: this
# repository is public, so they are never literals in the Blueprint.
- key: AWS_ACCESS_KEY_ID
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_SECRET_ACCESS_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
# No disk, on purpose — this is what buys zero-downtime deploys. Media lives
# in S3 (USE_S3 above); the old media-storage disk was copied there and
# verified object-for-object on 2026-08-27 before this block was removed.
# With no disk attached, Render starts the new instance alongside the old
# one and switches traffic only after the health check passes, so deploys
# stop dropping requests and the service is free to scale past one instance.
# Do not reintroduce a disk here without understanding that it brings the
# deploy 502 window back with it.
healthCheckPath: /
- type: worker
name: mapsurvey-celery
runtime: docker
plan: starter
# Previews DO get a worker: AI draft generation runs here, and a preview
# without it would leave the feature untestable in the one environment
# where changes are reviewed before merge. The metrics cron stays off —
# nothing on a preview reads its numbers.
dockerfilePath: ./Dockerfile
dockerContext: .
dockerCommand: celery -A mapsurvey worker -l info --concurrency 2
envVars:
- key: DATABASE_URL
fromDatabase:
name: mapsurvey-db
property: connectionString
- key: SECRET_KEY
generateValue: true
- key: CELERY_BROKER_URL
fromService:
type: redis
name: mapsurvey-redis
property: connectionString
- key: CELERY_RESULT_BACKEND
fromService:
type: redis
name: mapsurvey-redis
property: connectionString
# Error tracking: the task_failure receiver in mapsurvey/celery.py reports
# failed tasks. Same no-preview-inheritance rule as on the web service.
- key: POSTHOG_PROJECT_KEY
sync: false
- key: POSTHOG_API_HOST
sync: false
# Renders no templates, so this changes nothing here. Declared anyway so the two
# services cannot drift into different PostHog configurations.
- key: POSTHOG_CLIENT_HOST
sync: false
- key: DEFAULT_FROM_EMAIL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_BACKEND
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_PORT
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_USE_SSL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST_USER
sync: false
previewEnvRenderServiceName: mapsurvey
- key: EMAIL_HOST_PASSWORD
sync: false
previewEnvRenderServiceName: mapsurvey
- key: NEWSLETTER_SITE_URL
value: "https://mapsurvey.org"
# Media object storage. The worker writes files too — ZIP import and AI
# draft generation both save images — so it must resolve storage exactly
# like the web service, or an import would write to the disk while the web
# service reads from the bucket. Same rule on the namespace: absent here,
# derived per environment, so a preview worker cannot touch production keys.
- key: USE_S3
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_STORAGE_BUCKET_NAME
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_S3_REGION_NAME
value: ap-southeast-2
- key: AWS_ACCESS_KEY_ID
sync: false
previewEnvRenderServiceName: mapsurvey
- key: AWS_SECRET_ACCESS_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
# AI survey draft generation runs here — this is the service that talks
# to the model provider. Keep AI_PROVIDER identical to the web service:
# the web side gates the panel on it, the worker resolves the client from
# it, and a mismatch shows a panel whose every generation then fails.
- key: AI_PROVIDER
value: gemini
- key: GEMINI_API_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
- key: GEMINI_MODEL
sync: false
previewEnvRenderServiceName: mapsurvey
- key: ANTHROPIC_API_KEY
sync: false
previewEnvRenderServiceName: mapsurvey
# Off in production since 2026-08-17 evening, by product decision, after
# a day of measuring Gemini's SSE in anger: 47% of streamed calls failed
# (mid-answer cuts, stalls) in the morning batch, and one creator hit
# four consecutive failures — a dead end even through the retries. The
# blocking call has neither stream-specific failure mode, and at thinking
# `low` the wait is short enough for an indeterminate bar. Previews keep
# streaming on so the path stays exercised; reconsider against fresh
# $ai_generation error-rate data, not memory.
- key: AI_STREAMING_ENABLED
value: "false"
previewValue: "true"
# Daily acquisition-metrics sync for the funnel dashboard. There is no celery beat,
# so the schedule lives here (design D5). Re-runs a 7-day trailing window, which is
# both idempotent and how GSC's retroactive revisions get picked up.
#
# The provider credentials live ONLY on this service: the dashboard reads the local
# tables, never an API, so the web service has no reason to hold the keys.
- type: cron
name: mapsurvey-acquisition-sync
runtime: docker
plan: starter
previews:
generation: off
schedule: "0 5 * * *"
dockerfilePath: ./Dockerfile
dockerContext: .
dockerCommand: python manage.py sync_acquisition_metrics --days 7
envVars:
- key: DATABASE_URL
fromDatabase:
name: mapsurvey-db
property: connectionString
- key: SECRET_KEY
generateValue: true
- key: GSC_SITE
value: "sc-domain:mapsurvey.org"
- key: GSC_SERVICE_ACCOUNT_JSON
sync: false
- key: PLAUSIBLE_API_KEY
sync: false
- key: PLAUSIBLE_SITE_ID
value: "mapsurvey.org"
# Reclaims media left behind by destroyed PR previews. Render has no teardown
# hook, so this reconciles: it lists the previews/<service>/ prefixes in the
# bucket, asks Render which services still exist, and deletes only the orphans.
# Converges within a day of a pull request closing.
#
# An unreadable service listing deletes nothing — a partial answer would make
# live previews look dead. Without RENDER_API_KEY the job is a no-op, so an
# unconfigured token means objects accumulate, never that they are lost.
- type: cron
name: mapsurvey-preview-media-reclaim
runtime: docker
plan: starter
previews:
generation: off
schedule: "30 4 * * *"
dockerfilePath: ./Dockerfile
dockerContext: .
dockerCommand: sh ./reclaim.sh
envVars:
- key: DATABASE_URL
fromDatabase:
name: mapsurvey-db
property: connectionString
- key: SECRET_KEY
generateValue: true
# Reads and deletes objects, so it needs the bucket — but it must resolve
# production's own prefixes, never a preview namespace.
- key: USE_S3
sync: false
- key: AWS_STORAGE_BUCKET_NAME
sync: false
- key: AWS_S3_REGION_NAME
value: ap-southeast-2
- key: AWS_ACCESS_KEY_ID
sync: false
- key: AWS_SECRET_ACCESS_KEY
sync: false
- key: MEDIA_S3_NAMESPACE
value: ""
# Read-only Render API token, used solely to ask which services exist.
- key: RENDER_API_KEY
sync: false