Perform a rolling update
When you re-apply a manifest whose deployment declares at least one health check, Ring rolls the change out instance-by-instance: the old version keeps serving traffic until each new instance passes its checks. No flag is needed; the path is automatic.
For the underlying mechanism (parent/child deployments, readiness gate, drain logic), see Reconciliation → rolling updates.
When does ring apply roll?
| Condition | Strategy |
|---|---|
Manifest has ≥1 health_checks: entry, exactly one active deployment shares name+namespace, no --force | Rolling update |
--force is set | Immediate replacement |
| No health checks declared | Immediate replacement |
Multiple active deployments share name+namespace | Immediate replacement (clean up duplicates first) |
Manifest publishes a host port (ports[].published) | Immediate replacement (recreate), see below |
Immediate replacement stops every old instance before starting the new ones, causing brief downtime. Rolling update keeps traffic flowing.
Why a published host port forces recreate
A host port can be bound by only one container at a time. A rolling update creates the new container before stopping the old one, so the new bind would collide with the old (port is already allocated) and the deployment would loop in instance_creation_failed. To avoid that, Ring automatically recreates any deployment that publishes a host port: it stops the old container first, then starts the new one. This means a brief downtime during the swap, which is unavoidable while a single host port is shared. The switch is logged as a warning event on the new deployment so it's visible in ring deployment inspect.
Trigger a rollout
Edit the manifest (typically the image tag) and re-apply:
# web.yaml deployments: web-app: name: web-app namespace: production runtime: docker image: "myapp:v1.2.4" # was v1.2.3 replicas: 3 health_checks: - type: http url: "http://localhost:8080/health" interval: "10s" timeout: "5s" threshold: 3 on_failure: restart
ring apply -f web.yaml # Deployment subcommands take an ID, so look it up: DEPLOYMENT_ID=$(ring deployment list -n production -o json | jq -r '.[] | select(.name=="web-app") | .id' | head -1) ring deployment events "$DEPLOYMENT_ID" --follow
ring apply prints the child deployment's UUID. The parent stays visible in ring deployment list until the rollout finishes.
Watch a rollout end-to-end
Three terminals:
# Terminal 1: keep traffic flowing (no errors during rollout) while true; do curl -s -o /dev/null -w "%{http_code} " http://your-host/health; sleep 0.2; done # Terminal 2: events stream (`events` takes an ID) DEPLOYMENT_ID=$(ring deployment list -n production -o json | jq -r '.[] | select(.name=="web-app") | .id' | head -1) ring deployment events "$DEPLOYMENT_ID" --follow # Terminal 3: apply ring apply -f web.yaml
In terminal 2 you'll see, in order:
- A child deployment is created with
parent_idpointing at the old one - The child's instances boot
- Health checks on the child pass
- Parent instances are removed one at a time
- Once the parent has zero instances, it's marked
deleted
Terminal 1 should keep returning 200s the whole time.
When the rollout fails
If the child's instances never pass health checks within the threshold window, Ring stops and leaves the parent untouched:
- Child marked
failed errorevents accumulate (HealthCheckAlert,HealthCheckInstanceRestart)- Parent keeps serving traffic
Inspect:
ring deployment list --status failed ring deployment events <CHILD_ID> --level error ring deployment health-checks <CHILD_ID> ring deployment logs <CHILD_ID>
After fixing the manifest, re-apply (creates a new child, ignores the failed one) or delete the failed child explicitly:
ring deployment delete <CHILD_ID>
Roll back
There's no rollback command, and no automatic rollback if a healthy rollout later degrades. To go back to a previous image, set the tag in the manifest to the older version and ring apply again. Ring rolls forward to the older tag through the same mechanism.
sed -i 's|myapp:v1.2.4|myapp:v1.2.3|' web.yaml ring apply -f web.yaml
Force immediate replacement
When you want brief downtime, whether to clear a stuck rollout, recreate from scratch, or apply a manifest you know breaks compatibility:
ring apply -f web.yaml --force
Every old container is stopped, then the new ones are created. Use sparingly in production.
Gate the drain on real readiness
The default drain triggers as soon as the new container reaches Running: fast, but it ignores app-level boot time (warmup, migrations, cache priming). To wait for genuine readiness, hit a real endpoint on your app:
health_checks: - type: http url: "http://localhost:8080/ready" # your app's readiness endpoint interval: 5s timeout: 2s threshold: 3 on_failure: alert readiness: true # gate the drain on this passing
Your application returns 200 OK on /ready only after migrations have run, caches are warm, and downstream dependencies respond. Until then, the endpoint returns 503 (or refuses the connection), Ring keeps the old version serving traffic, and the rollout waits.
With readiness: true, Ring waits for at least one success on every readiness check, kept green for min_healthy_time (default 10s, configurable per check), before draining a parent instance. For slow-warming services, bump it with min_healthy_time: "30s" on the readiness check. See how-to: configure health checks → slow-warming services and the proxy-integration bonus.
What counts as "the same deployment"?
Ring identifies a deployment by (namespace, name). Anything else can change without breaking the rolling-update match: image, replicas, environment, labels, volumes, command, resources, even the health-check definitions themselves.
Inspect the parent/child relationship
curl -H "Authorization: Bearer $TOKEN" \ "http://localhost:3030/deployments/$CHILD_ID" | jq '.parent_id'
There's no built-in tree view across rollouts. In the steady state, only the most recent deployment is running and previous ones are deleted.
Recipe: deploy a new image safely
# 1. Update the tag sed -i 's|myapp:v1.2.3|myapp:v1.2.4|' web.yaml # 2. Apply (rolling) ring apply -f web.yaml # 3. Find the child CHILD_ID=$(ring deployment list -n production -o json \ | jq -r '.[] | select(.name=="web-app") | .id' | head -1) # 4. Watch ring deployment events "$CHILD_ID" --follow # 5. Verify health checks ring deployment health-checks "$CHILD_ID" --latest # 6. If something looks wrong, inspect logs ring deployment logs "$CHILD_ID" --tail 200
Caveats
- No traffic shifting. Ring doesn't weight traffic between old and new. For percentage rollouts, canary, or A/B routing, layer Traefik / Nginx / Envoy on top.
- No surge / max-unavailable knobs. The rollout swaps one instance at a time after the new one is healthy.
replicaschange ≠ surge. Bumpingreplicas: 3 → 5in the same apply means the child boots 5 while the parent drains from 3 → 0, so you transiently run up to 8.- Stateful workloads assume instances are interchangeable. A Postgres primary shouldn't be rolled: either declare no health check (so updates go via immediate replacement) or accept manual cutover. Ring has no
StatefulSetordering equivalent. - Health-check definition changes. Removing all health checks from a manifest sends the next apply through immediate replacement. Adding the first health check means that apply is the first to get a rolling update, so you only get the benefit going forward.
- Apply timeout.
RING_APPLY_TIMEOUT(default 300s) bounds a single instance's runtime call inside one scheduler tick, not the whole rolling update. For very slow-booting workloads, raise it explicitly.
See also
- Reconciliation: the loop that drives this
- Health checks (design): readiness gate, failure model
- How-to: configure health checks
- Manifest reference:
health_checks