Health checks are the ops fundamental vibe coders always skip
By Seaworthy · April 20, 2026
Photo by Judy Beth Morris on Unsplash
When a vibe-coded app ships to production, the Docker image usually has no HEALTHCHECK, the Kubernetes deployment has no probes, and nobody notices until users start reporting that requests just hang. The orchestrator is blissfully routing traffic to a container that booted, printed a startup log, and then quietly deadlocked.
This is fixable in under ten minutes. Here is how.
Liveness, readiness, and startup probes
Kubernetes has three distinct probe types and conflating them causes real problems.
A readiness probe tells the scheduler whether a pod should receive traffic. If it fails, the pod is removed from the Service's endpoint list. Users never see the broken pod.
A liveness probe tells the kubelet whether the container is alive at all. If it fails repeatedly, the container is restarted. Use this for deadlock recovery, not for "is the app warm yet."
A startup probe is for slow-starting apps. It gives the container extra time to initialise before liveness checks kick in. Without it, a legitimate slow boot looks like a crash loop and triggers pointless restarts.
The common mistake: using only a liveness probe and setting the threshold low. A container that takes 20 seconds to warm up will be killed repeatedly before it ever serves a request.
What actually happens without a readiness probe
You deploy a new version. The pod starts. Kubernetes sees it is Running and immediately adds it to the load balancer rotation. Your app is still connecting to the database, compiling templates, seeding caches. Requests arrive. They time out or return 500s. Your rolling deploy has just introduced a window of failures that lasts as long as your startup time.
With a readiness probe, the pod stays out of rotation until it signals it is ready. The rollout pauses rather than causes an outage.
Writing a minimal /healthz endpoint
The endpoint should be cheap. Do not query the database on every probe; that turns a single unhealthy DB into a cascade of probe failures and restarts. Check what you need to check and nothing else.
Express:
import express from 'express';
const app = express();
// Minimal readiness check: extend with DB ping if you need it
app.get('/healthz', (_req, res) => {
res.status(200).json({ status: 'ok', ts: Date.now() });
});
app.listen(3000);
Fastify:
import Fastify from 'fastify';
const app = Fastify();
app.get('/healthz', async (_request, reply) => {
return reply.status(200).send({ status: 'ok', ts: Date.now() });
});
await app.listen({ port: 3000, host: '0.0.0.0' });
If you want to check database connectivity, do it lazily: set a module-level dbReady flag after a successful connection, and return 503 until it flips.
HEALTHCHECK in a Dockerfile
For Docker Compose or standalone Docker, HEALTHCHECK serves the same purpose. The syntax most people get wrong is the interval: they set it too short and the daemon marks the container unhealthy before it has finished starting.
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
EXPOSE 3000
HEALTHCHECK \
--interval=15s \
--timeout=5s \
--start-period=20s \
--retries=3 \
CMD wget -qO- http://localhost:3000/healthz || exit 1
CMD ["node", "dist/index.js"]
--start-period gives your app 20 seconds of grace before failures count. --retries=3 means three consecutive failures are needed before the container is declared unhealthy. Use wget rather than curl in Alpine images: curl is not installed by default.
Kubernetes probe configuration
Here is a production-ready deployment snippet with all three probes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/yourorg/api:latest
ports:
- containerPort: 3000
startupProbe:
httpGet:
path: /healthz
port: 3000
failureThreshold: 12
periodSeconds: 5
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 20
failureThreshold: 3
The startup probe here allows up to 60 seconds (12 x 5s) for the app to become healthy before liveness takes over. Once liveness is active, three failures over 60 seconds trigger a restart. Readiness is checked every 10 seconds: one success puts the pod back in rotation, three failures pull it out.
Set successThreshold on the readiness probe to something higher than 1 if you want a pod to prove stability before it gets traffic. For a flaky dependency, successThreshold: 2 means the pod has to pass twice consecutively before it rejoins the pool.
The threshold numbers matter
Default values from kubectl are not tuned for your app. A periodSeconds: 10 with failureThreshold: 3 means 30 seconds of bad responses before Kubernetes acts. For a payment processor that is too slow. For a background worker it is fine.
Think about your SLA before copying thresholds from a tutorial. The numbers in the snippet above are a starting point, not a recommendation for every service.
Seaworthy flags missing health check configuration as an ops basics issue during static analysis.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.