Skip to content

Commit b08c25f

Browse files
authored
fix(compose): reserve resources on observability/WUD/mailpit + guardrail (#126)
The base stack, GlitchTip, and BullMQ all declare deploy.resources.reservations, but the observability overlay (8 services), WUD, and mailpit shipped only limits — so under host memory pressure those containers had no guaranteed floor and were first to be OOM-killed/throttled, losing exactly the observability an operator needs mid-incident. Adds env-overridable reservations (~20% cpus / 25% memory of limits, mirroring base) and a new check_resource_reservations guardrail (render_full_config; fails any restart!=no service without reservations) wired into the 'all' run and a per-step CI invocation. Dev-only mailpit gets reservations for consistency but stays outside the full-stack guardrail scope, like the other hardening checks. Audit: F001
1 parent 96e8829 commit b08c25f

5 files changed

Lines changed: 76 additions & 2 deletions

File tree

.github/workflows/infra-compose-validate-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ jobs:
124124
# so CI and local guardrails cannot drift.
125125
run: ../scripts/validate-guardrails.sh no-new-privileges
126126
working-directory: infra/compose/compose
127+
- name: Guardrail — long-running services reserve resources (no starvation)
128+
if: steps.filter.outputs.code == 'true'
129+
# Single-source: the same script runs in the local pre-push gate,
130+
# so CI and local guardrails cannot drift.
131+
run: ../scripts/validate-guardrails.sh resource-reservations
132+
working-directory: infra/compose/compose
127133
- name: Guardrail — prod rejects unpinned image tags (dev.sh fail-closed)
128134
if: steps.filter.outputs.code == 'true'
129135
# Single-source: the same script runs in the local pre-push gate,

infra/compose/compose/docker-compose.mailpit.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ services:
4444
limits:
4545
cpus: "${MAILPIT_LIMITS_CPUS:-0.25}"
4646
memory: "${MAILPIT_LIMITS_MEMORY:-128M}"
47+
reservations:
48+
cpus: "${MAILPIT_RESERVATIONS_CPUS:-0.05}"
49+
memory: "${MAILPIT_RESERVATIONS_MEMORY:-32M}"
4750

4851
volumes:
4952
mailpit_data:

infra/compose/compose/docker-compose.observability.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ services:
5050
limits:
5151
cpus: "${PROMETHEUS_LIMITS_CPUS:-0.5}"
5252
memory: "${PROMETHEUS_LIMITS_MEMORY:-512M}"
53+
reservations:
54+
cpus: "${PROMETHEUS_RESERVATIONS_CPUS:-0.1}"
55+
memory: "${PROMETHEUS_RESERVATIONS_MEMORY:-128M}"
5356

5457
alertmanager:
5558
profiles: ["observability"]
@@ -83,6 +86,9 @@ services:
8386
limits:
8487
cpus: "${ALERTMANAGER_LIMITS_CPUS:-0.25}"
8588
memory: "${ALERTMANAGER_LIMITS_MEMORY:-128M}"
89+
reservations:
90+
cpus: "${ALERTMANAGER_RESERVATIONS_CPUS:-0.05}"
91+
memory: "${ALERTMANAGER_RESERVATIONS_MEMORY:-32M}"
8692

8793
grafana:
8894
profiles: ["observability"]
@@ -123,6 +129,9 @@ services:
123129
limits:
124130
cpus: "${GRAFANA_LIMITS_CPUS:-0.5}"
125131
memory: "${GRAFANA_LIMITS_MEMORY:-512M}"
132+
reservations:
133+
cpus: "${GRAFANA_RESERVATIONS_CPUS:-0.1}"
134+
memory: "${GRAFANA_RESERVATIONS_MEMORY:-128M}"
126135

127136
loki:
128137
profiles: ["observability"]
@@ -147,6 +156,9 @@ services:
147156
limits:
148157
cpus: "${LOKI_LIMITS_CPUS:-0.5}"
149158
memory: "${LOKI_LIMITS_MEMORY:-512M}"
159+
reservations:
160+
cpus: "${LOKI_RESERVATIONS_CPUS:-0.1}"
161+
memory: "${LOKI_RESERVATIONS_MEMORY:-128M}"
150162

151163
promtail:
152164
profiles: ["observability"]
@@ -175,6 +187,9 @@ services:
175187
limits:
176188
cpus: "${PROMTAIL_LIMITS_CPUS:-0.25}"
177189
memory: "${PROMTAIL_LIMITS_MEMORY:-128M}"
190+
reservations:
191+
cpus: "${PROMTAIL_RESERVATIONS_CPUS:-0.05}"
192+
memory: "${PROMTAIL_RESERVATIONS_MEMORY:-32M}"
178193

179194
tempo:
180195
profiles: ["observability"]
@@ -200,6 +215,9 @@ services:
200215
limits:
201216
cpus: "${TEMPO_LIMITS_CPUS:-0.5}"
202217
memory: "${TEMPO_LIMITS_MEMORY:-512M}"
218+
reservations:
219+
cpus: "${TEMPO_RESERVATIONS_CPUS:-0.1}"
220+
memory: "${TEMPO_RESERVATIONS_MEMORY:-128M}"
203221

204222
postgres-exporter:
205223
profiles: ["observability"]
@@ -226,6 +244,9 @@ services:
226244
limits:
227245
cpus: "${POSTGRES_EXPORTER_LIMITS_CPUS:-0.1}"
228246
memory: "${POSTGRES_EXPORTER_LIMITS_MEMORY:-64M}"
247+
reservations:
248+
cpus: "${POSTGRES_EXPORTER_RESERVATIONS_CPUS:-0.05}"
249+
memory: "${POSTGRES_EXPORTER_RESERVATIONS_MEMORY:-32M}"
229250

230251
node-exporter:
231252
profiles: ["observability"]
@@ -256,6 +277,9 @@ services:
256277
limits:
257278
cpus: "${NODE_EXPORTER_LIMITS_CPUS:-0.1}"
258279
memory: "${NODE_EXPORTER_LIMITS_MEMORY:-64M}"
280+
reservations:
281+
cpus: "${NODE_EXPORTER_RESERVATIONS_CPUS:-0.05}"
282+
memory: "${NODE_EXPORTER_RESERVATIONS_MEMORY:-32M}"
259283

260284
volumes:
261285
prometheus_data:

infra/compose/compose/docker-compose.wud.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ services:
6666
limits:
6767
cpus: "${WUD_LIMITS_CPUS:-0.25}"
6868
memory: "${WUD_LIMITS_MEMORY:-256M}"
69+
reservations:
70+
cpus: "${WUD_RESERVATIONS_CPUS:-0.05}"
71+
memory: "${WUD_RESERVATIONS_MEMORY:-64M}"
6972

7073
volumes:
7174
wud_data:

infra/compose/scripts/validate-guardrails.sh

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
# ./validate-guardrails.sh [check]
1010
#
1111
# Checks: healthchecks | digest-pins | credential-fallbacks | valkey-auth
12-
# | rooted-caps | no-new-privileges | prod-image-tags | all (default)
12+
# | rooted-caps | no-new-privileges | resource-reservations
13+
# | prod-image-tags | all (default)
1314
#
1415
# Requires: docker (compose config rendering), python3.
1516

@@ -274,13 +275,49 @@ check_prod_image_tags() {
274275
ok "prod-image-tags"
275276
}
276277

278+
check_resource_reservations() {
279+
# Render the FULL stack (base prod + every prod-capable overlay) and require
280+
# that every long-running service declares deploy.resources.reservations, not
281+
# just limits. Limits cap a container; reservations guarantee its floor — the
282+
# scheduler honors them so an observability/exporter container can't be
283+
# starved or OOM-killed under host memory pressure, which is exactly when an
284+
# operator needs metrics and dashboards. The base stack, GlitchTip, and
285+
# BullMQ already set reservations; the observability + WUD overlays drifted.
286+
# One-shot jobs (restart: no) are exempt — same criterion as
287+
# check_healthchecks. Dev-only overlays (mailpit) are out of scope here, like
288+
# the other full-stack guardrails.
289+
render_full_config
290+
python3 - <<'EOF'
291+
import json
292+
293+
with open("/tmp/guardrails-full-config.json", encoding="utf-8") as handle:
294+
services = json.load(handle)["services"]
295+
296+
missing = sorted(
297+
name
298+
for name, svc in services.items()
299+
if svc.get("restart") != "no"
300+
and (svc.get("deploy") or {}).get("resources", {}).get("reservations") is None
301+
)
302+
303+
if missing:
304+
raise SystemExit(
305+
"services missing deploy.resources.reservations: " + ", ".join(missing)
306+
)
307+
308+
print("reservations present on: " + ", ".join(sorted(services)))
309+
EOF
310+
ok "resource-reservations"
311+
}
312+
277313
case "$CHECK" in
278314
healthchecks) check_healthchecks ;;
279315
digest-pins) check_digest_pins ;;
280316
credential-fallbacks) check_credential_fallbacks ;;
281317
valkey-auth) check_valkey_auth ;;
282318
rooted-caps) check_rooted_caps ;;
283319
no-new-privileges) check_no_new_privileges ;;
320+
resource-reservations) check_resource_reservations ;;
284321
prod-image-tags) check_prod_image_tags ;;
285322
all)
286323
check_digest_pins
@@ -289,10 +326,11 @@ case "$CHECK" in
289326
check_healthchecks
290327
check_rooted_caps
291328
check_no_new_privileges
329+
check_resource_reservations
292330
check_prod_image_tags
293331
c_green "✓ all compose guardrails passed"
294332
;;
295333
*)
296-
fail "unknown check: $CHECK (healthchecks|digest-pins|credential-fallbacks|valkey-auth|rooted-caps|no-new-privileges|prod-image-tags|all)"
334+
fail "unknown check: $CHECK (healthchecks|digest-pins|credential-fallbacks|valkey-auth|rooted-caps|no-new-privileges|resource-reservations|prod-image-tags|all)"
297335
;;
298336
esac

0 commit comments

Comments
 (0)