docs(deployment): exclude UDP from the overlap and describe marker-based retirement
- commit
- 7d228517dc9ce791ae27ee74fdb1163786a69fa7
- author
- brice · · 2026-10-16 19:47 UTC
- parent
- 733740b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +16 -16
M README.md +1 -1
| @@ -154,7 +154,7 @@ gordon apps deploy blog --remote prod | |||
| 154 | 154 | - Private Docker/Podman registry on your VPS | |
| 155 | 155 | - Declarative apps: one TOML file per app, explicit apply then deploy | |
| 156 | 156 | - Domain-to-container routing through a smart TCP edge reverse proxy | |
| 157 | - | - Zero-downtime replacement of stateless services (start, verify readiness, switch traffic, stop the old one); services with volumes, binds, or devices are replaced sequentially | |
| 157 | + | - Zero-downtime replacement of stateless TCP/HTTP services (start, verify readiness, switch traffic, stop the old one); services with volumes, binds, devices, or UDP interfaces are replaced sequentially | |
| 158 | 158 | - Remote CLI management (daemon is the sole writer) | |
| 159 | 159 | - Declarative per-service volumes, retained across lifecycle operations | |
| 160 | 160 | - Per-service secrets in pass, app-wide public env in the manifest | |
| @@ -154,7 +154,7 @@ gordon apps deploy blog --remote prod | |||||
| 154 | - Private Docker/Podman registry on your VPS | 154 | - Private Docker/Podman registry on your VPS | ||
| 155 | - Declarative apps: one TOML file per app, explicit apply then deploy | 155 | - Declarative apps: one TOML file per app, explicit apply then deploy | ||
| 156 | - Domain-to-container routing through a smart TCP edge reverse proxy | 156 | - Domain-to-container routing through a smart TCP edge reverse proxy | ||
| 157 | - | - Zero-downtime replacement of stateless services (start, verify readiness, switch traffic, stop the old one); services with volumes, binds, or devices are replaced sequentially | 157 | + | - Zero-downtime replacement of stateless TCP/HTTP services (start, verify readiness, switch traffic, stop the old one); services with volumes, binds, devices, or UDP interfaces are replaced sequentially |
| 158 | - Remote CLI management (daemon is the sole writer) | 158 | - Remote CLI management (daemon is the sole writer) | ||
| 159 | - Declarative per-service volumes, retained across lifecycle operations | 159 | - Declarative per-service volumes, retained across lifecycle operations | ||
| 160 | - Per-service secrets in pass, app-wide public env in the manifest | 160 | - Per-service secrets in pass, app-wide public env in the manifest | ||
M docs/concepts.md +5 -5
| @@ -71,19 +71,19 @@ DATABASE_URL = "database-url" | |||
| 71 | 71 | ||
| 72 | 72 | ## Updates | |
| 73 | 73 | ||
| 74 | - | Gordon deploys an app's services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device. | |
| 74 | + | Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted. Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device, and a UDP service is excluded because UDP sessions stay pinned to the backend they started on. | |
| 75 | 75 | ||
| 76 | 76 | Preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations). If preflight fails, the running service is left untouched. | |
| 77 | 77 | ||
| 78 | - | A stateless service (no volume, bind, or device) is replaced with a short overlap: | |
| 78 | + | A stateless service (no volume, bind, device, or UDP interface) is replaced with a short overlap: | |
| 79 | 79 | ||
| 80 | 80 | ``` | |
| 81 | 81 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | |
| 82 | 82 | ``` | |
| 83 | 83 | ||
| 84 | - | The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own `stop_grace` (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning, and the next reconciliation retries it. | |
| 84 | + | The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own `stop_grace` (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning. Before the switch Gordon durably marks the old container for retirement, so the periodic recovery pass keeps retrying it, with the old container's `stop_grace`, once the new container is verified; later deploys do not lose the cleanup. | |
| 85 | 85 | ||
| 86 | - | A service with a volume, a bind, or a device is replaced sequentially: | |
| 86 | + | A service with a volume, a bind, a device, or a UDP interface is replaced sequentially: | |
| 87 | 87 | ||
| 88 | 88 | ``` | |
| 89 | 89 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | |
| @@ -93,7 +93,7 @@ If replacement fails after the old container was removed, the failure is explici | |||
| 93 | 93 | ||
| 94 | 94 | Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This covers TCP, UDP, mixed, and volume-owning services. An open UDP socket is not application readiness, and Gordon never restarts an old volume-owning image automatically after a replacement may have written data. | |
| 95 | 95 | ||
| 96 | - | A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; Gordon stops the old one at the next reconciliation, after re-applying routing to the published container. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed. | |
| 96 | + | A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; the retirement mark makes the periodic recovery pass stop the old one after it has verified and routed the published container. Boot recovery never routes a persisted port it has not verified. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed. | |
| 97 | 97 | ||
| 98 | 98 | Deploy is the only command that applies changes: it recreates a service whose image digest, spec, app environment, or secret values changed, and keeps an unchanged one, reported `unchanged`. | |
| 99 | 99 | ||
| @@ -71,19 +71,19 @@ DATABASE_URL = "database-url" | |||||
| 71 | 71 | ||||
| 72 | ## Updates | 72 | ## Updates | ||
| 73 | 73 | ||||
| 74 | - | Gordon deploys an app's services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device. | 74 | + | Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; every other service is replaced sequentially and may be briefly interrupted. Gordon never runs two Gordon-managed generations of a service that owns a volume, a bind, or a device, and a UDP service is excluded because UDP sessions stay pinned to the backend they started on. |
| 75 | 75 | ||||
| 76 | Preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations). If preflight fails, the running service is left untouched. | 76 | Preflight completes before anything is disrupted (image resolution and pull, secrets, volumes, networks, bind policy, reservations). If preflight fails, the running service is left untouched. | ||
| 77 | 77 | ||||
| 78 | - | A stateless service (no volume, bind, or device) is replaced with a short overlap: | 78 | + | A stateless service (no volume, bind, device, or UDP interface) is replaced with a short overlap: |
| 79 | 79 | ||||
| 80 | ``` | 80 | ``` | ||
| 81 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | 81 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | ||
| 82 | ``` | 82 | ``` | ||
| 83 | 83 | ||||
| 84 | - | The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own `stop_grace` (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning, and the next reconciliation retries it. | 84 | + | The old container keeps serving and stays routed until the new one is ready and traffic points at it. Only then does the old container receive SIGTERM, with its own `stop_grace` (default 30s) before it is killed. While both run, they share the service's network alias, so a request from another service of the app may reach either generation. A database migration must therefore stay backward compatible with the previous version of the service. If the new container never becomes ready, it is removed and the old one is not touched. If the old container cannot be removed after the switch, the deploy still succeeds and reports a cleanup warning. Before the switch Gordon durably marks the old container for retirement, so the periodic recovery pass keeps retrying it, with the old container's `stop_grace`, once the new container is verified; later deploys do not lose the cleanup. |
| 85 | 85 | ||||
| 86 | - | A service with a volume, a bind, or a device is replaced sequentially: | 86 | + | A service with a volume, a bind, a device, or a UDP interface is replaced sequentially: |
| 87 | 87 | ||||
| 88 | ``` | 88 | ``` | ||
| 89 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | 89 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | ||
| @@ -93,7 +93,7 @@ If replacement fails after the old container was removed, the failure is explici | |||||
| 93 | 93 | ||||
| 94 | Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This covers TCP, UDP, mixed, and volume-owning services. An open UDP socket is not application readiness, and Gordon never restarts an old volume-owning image automatically after a replacement may have written data. | 94 | Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. This covers TCP, UDP, mixed, and volume-owning services. An open UDP socket is not application readiness, and Gordon never restarts an old volume-owning image automatically after a replacement may have written data. | ||
| 95 | 95 | ||||
| 96 | - | A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; Gordon stops the old one at the next reconciliation, after re-applying routing to the published container. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed. | 96 | + | A deploy interrupted after the replacement container was created but before it was published leaves the failure explicit: the candidate is recorded in the deployment journal, and Gordon removes it before it creates or rebuilds any generation of that app — at boot and before every mutation. For a stateless service the old container keeps serving and is kept. A deploy interrupted after the switch but before the old container was stopped leaves two containers running; the retirement mark makes the periodic recovery pass stop the old one after it has verified and routed the published container. Boot recovery never routes a persisted port it has not verified. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. The next recovery pass republishes routing within its 15-second cadence if the final traffic publication was the step that failed. |
| 97 | 97 | ||||
| 98 | Deploy is the only command that applies changes: it recreates a service whose image digest, spec, app environment, or secret values changed, and keeps an unchanged one, reported `unchanged`. | 98 | Deploy is the only command that applies changes: it recreates a service whose image digest, spec, app environment, or secret values changed, and keeps an unchanged one, reported `unchanged`. | ||
| 99 | 99 | ||||
M docs/config/apps.md +2 -2
| @@ -114,9 +114,9 @@ Image registry names and digest syntax are validated during manifest apply, reso | |||
| 114 | 114 | ||
| 115 | 115 | A deploy recreates a service whose image digest, spec, environment, or secret values changed and keeps an unchanged one. | |
| 116 | 116 | ||
| 117 | - | - A service with no volume, bind, or device (stateless) is replaced without downtime. The new container starts and passes its readiness probe while the old one keeps serving; traffic then switches to the new container; only afterwards does the old container receive SIGTERM, and it is killed once its `stop_grace` (default `30s`) runs out. If the new container is not ready, it is removed and the old one is left untouched. | |
| 117 | + | - A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime. The new container starts and passes its readiness probe while the old one keeps serving; traffic then switches to the new container; only afterwards does the old container receive SIGTERM, and it is killed once its `stop_grace` (default `30s`) runs out. If the new container is not ready, it is removed and the old one is left untouched. | |
| 118 | 118 | - During the overlap both containers answer to the same network alias, so other services of the app may reach either generation for a short time. Database migrations must be backward compatible with the previous version of the service. | |
| 119 | - | - A service with a volume, bind, or device is replaced sequentially: traffic is withdrawn, the old container is stopped and removed, then the new one starts. It may be briefly interrupted, and two generations of it never run at once. | |
| 119 | + | - A service with a volume, bind, device, or UDP interface is replaced sequentially: traffic is withdrawn, the old container is stopped and removed, then the new one starts. It may be briefly interrupted, and two generations of it never run at once. UDP services are excluded from the overlap because UDP sessions stay pinned to the backend they started on. | |
| 120 | 120 | ||
| 121 | 121 | ## Environment and Secrets | |
| 122 | 122 | ||
| @@ -114,9 +114,9 @@ Image registry names and digest syntax are validated during manifest apply, reso | |||||
| 114 | 114 | ||||
| 115 | A deploy recreates a service whose image digest, spec, environment, or secret values changed and keeps an unchanged one. | 115 | A deploy recreates a service whose image digest, spec, environment, or secret values changed and keeps an unchanged one. | ||
| 116 | 116 | ||||
| 117 | - | - A service with no volume, bind, or device (stateless) is replaced without downtime. The new container starts and passes its readiness probe while the old one keeps serving; traffic then switches to the new container; only afterwards does the old container receive SIGTERM, and it is killed once its `stop_grace` (default `30s`) runs out. If the new container is not ready, it is removed and the old one is left untouched. | 117 | + | - A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime. The new container starts and passes its readiness probe while the old one keeps serving; traffic then switches to the new container; only afterwards does the old container receive SIGTERM, and it is killed once its `stop_grace` (default `30s`) runs out. If the new container is not ready, it is removed and the old one is left untouched. |
| 118 | - During the overlap both containers answer to the same network alias, so other services of the app may reach either generation for a short time. Database migrations must be backward compatible with the previous version of the service. | 118 | - During the overlap both containers answer to the same network alias, so other services of the app may reach either generation for a short time. Database migrations must be backward compatible with the previous version of the service. | ||
| 119 | - | - A service with a volume, bind, or device is replaced sequentially: traffic is withdrawn, the old container is stopped and removed, then the new one starts. It may be briefly interrupted, and two generations of it never run at once. | 119 | + | - A service with a volume, bind, device, or UDP interface is replaced sequentially: traffic is withdrawn, the old container is stopped and removed, then the new one starts. It may be briefly interrupted, and two generations of it never run at once. UDP services are excluded from the overlap because UDP sessions stay pinned to the backend they started on. |
| 120 | 120 | ||||
| 121 | ## Environment and Secrets | 121 | ## Environment and Secrets | ||
| 122 | 122 | ||||
M docs/deployment/index.md +5 -5
| @@ -146,15 +146,15 @@ docker push gordon.example.com/myapp:$VERSION | |||
| 146 | 146 | ||
| 147 | 147 | ## Updates | |
| 148 | 148 | ||
| 149 | - | Gordon deploys an app's services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime; a service that owns one of them is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. | |
| 149 | + | Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; a service that owns a volume, bind, or device is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. A UDP service is also replaced sequentially: UDP sessions stay pinned to the backend they started on, so they could not follow the switch. | |
| 150 | 150 | ||
| 151 | - | Stateless service (no volume, bind, or device): | |
| 151 | + | Stateless service (no volume, bind, device, or UDP interface): | |
| 152 | 152 | ||
| 153 | 153 | ``` | |
| 154 | 154 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | |
| 155 | 155 | ``` | |
| 156 | 156 | ||
| 157 | - | Service with a volume, bind, or device: | |
| 157 | + | Service with a volume, bind, device, or UDP interface: | |
| 158 | 158 | ||
| 159 | 159 | ``` | |
| 160 | 160 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | |
| @@ -162,11 +162,11 @@ Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─ | |||
| 162 | 162 | ||
| 163 | 163 | Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service untouched. | |
| 164 | 164 | ||
| 165 | - | For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its `stop_grace` (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy, and reconciliation retries it. | |
| 165 | + | For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its `stop_grace` (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy; it stays durably marked for retirement, and the periodic recovery pass retries it with its own `stop_grace` once the new container is verified, even after later deploys. | |
| 166 | 166 | ||
| 167 | 167 | For a sequential replacement, a failure after the old container was removed is explicit: Gordon does not recreate the old container during that operation and does not promise automatic data rollback. A stateful service's recovery inhibition stays in place until its never-published candidate is confirmed removed; a later boot/start/restart then clears it and rebuilds the generation `ACTIVE` records from its pinned digest, while a candidate that cannot be removed fails recovery closed. Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. | |
| 168 | 168 | ||
| 169 | - | If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding any generation of that app — at boot and before every mutation; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the next reconciliation re-applies routing and stops the old container. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default. | |
| 169 | + | If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding any generation of that app — at boot and before every mutation; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the periodic recovery pass verifies and routes the published container and then stops the old one. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default. | |
| 170 | 170 | ||
| 171 | 171 | ## Related | |
| 172 | 172 | ||
| @@ -146,15 +146,15 @@ docker push gordon.example.com/myapp:$VERSION | |||||
| 146 | 146 | ||||
| 147 | ## Updates | 147 | ## Updates | ||
| 148 | 148 | ||||
| 149 | - | Gordon deploys an app's services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime; a service that owns one of them is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. | 149 | + | Gordon deploys an app's services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime; a service that owns a volume, bind, or device is replaced sequentially and may be briefly interrupted, because Gordon never runs two Gordon-managed generations of such a service at the same time. A UDP service is also replaced sequentially: UDP sessions stay pinned to the backend they started on, so they could not follow the switch. |
| 150 | 150 | ||||
| 151 | - | Stateless service (no volume, bind, or device): | 151 | + | Stateless service (no volume, bind, device, or UDP interface): |
| 152 | 152 | ||||
| 153 | ``` | 153 | ``` | ||
| 154 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | 154 | Preflight ─► start new ─► readiness ─► publish ACTIVE ─► route traffic ─► SIGTERM old ─► stop_grace ─► kill + remove old | ||
| 155 | ``` | 155 | ``` | ||
| 156 | 156 | ||||
| 157 | - | Service with a volume, bind, or device: | 157 | + | Service with a volume, bind, device, or UDP interface: |
| 158 | 158 | ||||
| 159 | ``` | 159 | ``` | ||
| 160 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | 160 | Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─► readiness ─► publish ACTIVE ─► route traffic | ||
| @@ -162,11 +162,11 @@ Preflight ─► withdraw traffic ─► stop + remove old ─► start new ─ | |||||
| 162 | 162 | ||||
| 163 | Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service untouched. | 163 | Preflight (image resolution and pull, secrets, volumes, networks, bind policy, reservations) completes before anything is disrupted, so a preflight failure leaves the running service untouched. | ||
| 164 | 164 | ||||
| 165 | - | For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its `stop_grace` (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy, and reconciliation retries it. | 165 | + | For a stateless service the old container serves until the new one is ready and routed, then receives SIGTERM and is killed after its `stop_grace` (default 30s). During the overlap both containers share the service's network alias, so other services of the app may reach either one. Database migrations must stay backward compatible with the previous version of the service. A new container that fails readiness is removed and the old one is not touched. An old container that cannot be removed is reported as a cleanup warning without failing the deploy; it stays durably marked for retirement, and the periodic recovery pass retries it with its own `stop_grace` once the new container is verified, even after later deploys. |
| 166 | 166 | ||||
| 167 | For a sequential replacement, a failure after the old container was removed is explicit: Gordon does not recreate the old container during that operation and does not promise automatic data rollback. A stateful service's recovery inhibition stays in place until its never-published candidate is confirmed removed; a later boot/start/restart then clears it and rebuilds the generation `ACTIVE` records from its pinned digest, while a candidate that cannot be removed fails recovery closed. Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. | 167 | For a sequential replacement, a failure after the old container was removed is explicit: Gordon does not recreate the old container during that operation and does not promise automatic data rollback. A stateful service's recovery inhibition stays in place until its never-published candidate is confirmed removed; a later boot/start/restart then clears it and rebuilds the generation `ACTIVE` records from its pinned digest, while a candidate that cannot be removed fails recovery closed. Deployment stops at the first service failure: services already deployed in that run are kept and are not rolled back. | ||
| 168 | 168 | ||||
| 169 | - | If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding any generation of that app — at boot and before every mutation; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the next reconciliation re-applies routing and stops the old container. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default. | 169 | + | If a deploy is interrupted between creating the replacement container and publishing it, that candidate is recorded durably and Gordon removes it before creating or rebuilding any generation of that app — at boot and before every mutation; a stateless service's old container keeps serving. If it is interrupted after the switch but before the old container was stopped, the periodic recovery pass verifies and routes the published container and then stops the old one. An interrupted operation is finalized instead of staying in flight: as a failure, or as the success it was when every step had already completed. If a recorded candidate cannot be removed, reconciliation fails closed: the operation is not finalized, stays the latest journal, and every later mutation is refused so a newer operation can never mask the orphan. A failed final traffic publication is republished by the next recovery pass, within 15 seconds by default. |
| 170 | 170 | ||||
| 171 | ## Related | 171 | ## Related | ||
| 172 | 172 | ||||
M docs/deployment/rollback.md +1 -1
| @@ -12,7 +12,7 @@ gordon apps apply --file blog.toml --remote prod | |||
| 12 | 12 | gordon apps deploy blog --remote prod | |
| 13 | 13 | ``` | |
| 14 | 14 | ||
| 15 | - | A rollback deploy replaces services one at a time like any other deploy: a stateless service is replaced without downtime, while a service with a volume, bind, or device is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If a sequential replacement fails after the old container was removed, Gordon does not recreate it and does not promise automatic data rollback. | |
| 15 | + | A rollback deploy replaces services one at a time like any other deploy: a stateless TCP or HTTP service is replaced without downtime, while a service with a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If a sequential replacement fails after the old container was removed, Gordon does not recreate it and does not promise automatic data rollback. | |
| 16 | 16 | ||
| 17 | 17 | ## Re-push the Previous Image as latest | |
| 18 | 18 | ||
| @@ -12,7 +12,7 @@ gordon apps apply --file blog.toml --remote prod | |||||
| 12 | gordon apps deploy blog --remote prod | 12 | gordon apps deploy blog --remote prod | ||
| 13 | ``` | 13 | ``` | ||
| 14 | 14 | ||||
| 15 | - | A rollback deploy replaces services one at a time like any other deploy: a stateless service is replaced without downtime, while a service with a volume, bind, or device is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If a sequential replacement fails after the old container was removed, Gordon does not recreate it and does not promise automatic data rollback. | 15 | + | A rollback deploy replaces services one at a time like any other deploy: a stateless TCP or HTTP service is replaced without downtime, while a service with a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If a sequential replacement fails after the old container was removed, Gordon does not recreate it and does not promise automatic data rollback. |
| 16 | 16 | ||||
| 17 | ## Re-push the Previous Image as latest | 17 | ## Re-push the Previous Image as latest | ||
| 18 | 18 | ||||
M docs/getting-started.md +1 -1
| @@ -197,7 +197,7 @@ gordon images push myapp --build --remote prod | |||
| 197 | 197 | gordon apps deploy blog --remote prod | |
| 198 | 198 | ``` | |
| 199 | 199 | ||
| 200 | - | Gordon deploys services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, or device is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched. | |
| 200 | + | Gordon deploys services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched. | |
| 201 | 201 | ||
| 202 | 202 | ## Next Steps | |
| 203 | 203 | ||
| @@ -197,7 +197,7 @@ gordon images push myapp --build --remote prod | |||||
| 197 | gordon apps deploy blog --remote prod | 197 | gordon apps deploy blog --remote prod | ||
| 198 | ``` | 198 | ``` | ||
| 199 | 199 | ||||
| 200 | - | Gordon deploys services one at a time in sorted order. A service with no volume, bind, or device is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, or device is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched. | 200 | + | Gordon deploys services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched. |
| 201 | 201 | ||||
| 202 | ## Next Steps | 202 | ## Next Steps | ||
| 203 | 203 | ||||
M docs/index.md +1 -1
| @@ -10,7 +10,7 @@ Gordon runs on your VPS and provides: | |||
| 10 | 10 | - **Declarative Apps** - One TOML file per app, explicit apply then deploy | |
| 11 | 11 | - **HTTP Reverse Proxy** - Routes app hosts to containers | |
| 12 | 12 | - **Push, Apply, Deploy** - Push stores images; deploy is always explicit | |
| 13 | - | - **Zero-Downtime Stateless Deploys** - Stateless services start the new generation, verify readiness, switch traffic, then stop the old one; services with volumes, binds, or devices are replaced sequentially | |
| 13 | + | - **Zero-Downtime Stateless Deploys** - Stateless services start the new generation, verify readiness, switch traffic, then stop the old one; services with volumes, binds, devices, or UDP interfaces are replaced sequentially | |
| 14 | 14 | - **Single Binary** - ~15MB RAM footprint | |
| 15 | 15 | ||
| 16 | 16 | ## How It Works | |
| @@ -10,7 +10,7 @@ Gordon runs on your VPS and provides: | |||||
| 10 | - **Declarative Apps** - One TOML file per app, explicit apply then deploy | 10 | - **Declarative Apps** - One TOML file per app, explicit apply then deploy | ||
| 11 | - **HTTP Reverse Proxy** - Routes app hosts to containers | 11 | - **HTTP Reverse Proxy** - Routes app hosts to containers | ||
| 12 | - **Push, Apply, Deploy** - Push stores images; deploy is always explicit | 12 | - **Push, Apply, Deploy** - Push stores images; deploy is always explicit | ||
| 13 | - | - **Zero-Downtime Stateless Deploys** - Stateless services start the new generation, verify readiness, switch traffic, then stop the old one; services with volumes, binds, or devices are replaced sequentially | 13 | + | - **Zero-Downtime Stateless Deploys** - Stateless services start the new generation, verify readiness, switch traffic, then stop the old one; services with volumes, binds, devices, or UDP interfaces are replaced sequentially |
| 14 | - **Single Binary** - ~15MB RAM footprint | 14 | - **Single Binary** - ~15MB RAM footprint | ||
| 15 | 15 | ||||
| 16 | ## How It Works | 16 | ## How It Works | ||