prop/k8s/deployment.yaml
Graham McIntire 45d8466808
ops(prop): preStop sleep so Service drains before Phoenix shuts down
Deploys were causing 502 Bad Gateway on LiveView reconnect. The kubelet
sent SIGTERM to the old pod and Phoenix immediately stopped accepting
connections, but the Service endpoint controller and ingress hadn't
yet propagated the pod's removal from endpoints — the browser's
reconnect raced the endpoint update and bounced 502 against the dead
pod, then gave up.

Add a 10s preStop sleep on the container and bump
terminationGracePeriodSeconds to 40 so the load balancer has time to
drain this pod before Phoenix begins shutting down.
2026-04-29 14:13:22 -05:00

150 lines
5.3 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: prop
namespace: prop
spec:
# replicas is owned by the HPA in hpa.yaml (currently pinned at 1).
# Leaving it out here so the GitOps reconciler doesn't overwrite
# whatever replica count the autoscaler has settled on.
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
# Zero-downtime rollout with a single replica: surge to 2 first,
# wait for the new pod to pass its readinessProbe, THEN tear down
# the old one. maxUnavailable: 1 (the K8s default) was killing the
# only pod before the replacement was ready, taking the site down
# for the entire image pull + boot window on every deploy.
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: prop
template:
metadata:
labels:
# `tier: hot` distinguishes these pods from prop-backfill (same
# app: prop label) so the Service selector can exclude backfill
# from its endpoint pool — backfill runs PHX_SERVER=false and
# doesn't listen on :5000.
app: prop
tier: hot
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
serviceAccountName: prop
imagePullSecrets:
- name: forgejo-registry
# The deploy was producing 502s on LiveView reconnect: kubelet sent
# SIGTERM to the old pod and Phoenix immediately stopped accepting
# WebSocket connections, but the Service endpoint controller and
# ingress hadn't yet propagated the pod's removal from endpoints.
# The browser's reconnect attempt would route to the dead pod and
# bounce 502.
#
# 30s grace + a 10s preStop sleep on the container gives the
# endpoint controller a comfortable window to drain this pod from
# the load balancer before Phoenix starts shutting down.
terminationGracePeriodSeconds: 40
securityContext:
runAsUser: 65534
runAsNonRoot: true
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: prop
image: git.mcintire.me/graham/prop:main-1777300373-610892e
imagePullPolicy: IfNotPresent
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: PHX_SERVER
value: "true"
- name: PHX_HOST
value: "prop.w5isp.com"
- name: PORT
value: "5000"
- name: HRRR_BASE_URL
value: "http://skippy.w5isp.com:8080"
envFrom:
- secretRef:
name: prop-secrets
ports:
- name: http
containerPort: 5000
protocol: TCP
startupProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 3
failureThreshold: 10
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
timeoutSeconds: 2
# /live skips the DB — a saturated Ecto pool (e.g. a PromEx
# poller holding a connection >15s) would otherwise cause
# /health's `SELECT 1` to time out and the kubelet to SIGKILL
# every replica at once. Readiness still points at /health so
# DB outages drain the pod from Service endpoints gracefully.
# timeoutSeconds raised to 5s: under an IEM 429 retry storm
# Bandit's acceptor loop can stall briefly as the scheduler
# chews through retrying HTTP sleeps, and 3s was tripping
# failureThreshold across all 3 replicas at once.
livenessProbe:
httpGet:
path: /live
port: 5000
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
# Sleep before SIGTERM hits Phoenix so the Service endpoint
# controller has time to remove this pod from the LB pool —
# without it, in-flight LiveView reconnects raced the
# endpoint update and bounced 502 against the dying pod.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
# Bumped 2 → 3 on 2026-04-23: BEAM was getting 2 schedulers
# online on a 2-core limit, and the :05 propagation chain
# plus ScoreCache warming spiked the second run queue to
# ~10, starving /live and /health probe handlers and
# tripping liveness/readiness timeouts.
cpu: "3"
memory: 6Gi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
nfs:
server: 10.0.15.103
path: /data