Deploys and the restart window
vulnboxd puts every restart in a window at the end of each tick, tick.window long (5 seconds by default), so a service is only ever unreachable then.
The window is vulnboxd's own setting, not something every game has. Some games do define one, such as the ECSC's 5 seconds at the end of each tick; set tick.window to match. Where the game has none, vulnboxd still uses it as the place for restarts.
The end of the tick is the right place when the game's checkers do their round early in the tick, so that nothing checks the service while it restarts. If your game spreads checks over the whole tick, some deploys will meet a check wherever the window is; the grace period turns most of those into a slow answer rather than a failed one.
What happens when you push
- On the next poll, vulnboxd notices the service's files changed.
- It runs
docker compose buildanddocker compose pullstraight away. The old version keeps serving throughout; a build can take as long as it needs. - When the window opens, it runs
docker compose up -dwith the override, which recreates only the containers whose image or configuration changed.
Pulling matters as much as building. compose build does not fetch images of services that only have an image:, and fetching those inside the window would eat most of it.
If a build fails, the error is logged, vulnboxd_builds_total{result="failed"} goes up, and the running version stays.
Before replacing the containers, vulnboxd closes the connections it was keeping to the old ones: a connection to a container that is about to go away is no use to the next request, and it can keep docker from letting go of that container's port in time. A deploy that still cannot publish a port is retried a few times, because docker sometimes needs a moment longer to release it than vulnboxd takes to ask. One cause is not docker's — a backend port inside the kernel's ephemeral range; see backend_port_base.
Keeping deploys inside the window
Once the images are ready, what is left in the window is mostly stopping the old container. Docker sends it SIGTERM and waits deploy.stop_timeout (default 1s) before killing it.
Services that stop slowly
A process running as pid 1 in a container gets no default signal handling, so an app started directly by command: ignores SIGTERM and always waits out the full timeout. Adding init: true to the compose service fixes that. On one test service it cut the stop from 2.7 s to 0.6 s.
services:
web:
build: .
init: trueWhen a deploy overruns
It does not have to cost SLA. The proxy holds the public port the whole time, so a connection that arrives while the container is restarting is not refused. For up to deploy.grace (default 15s), vulnboxd keeps retrying the service until it listens again. The checker sees one slow request instead of a failed one.
Only requests already in flight when the old container stops are lost. In a test that deployed a service under constant load, 451 requests succeeded across the deploy and 1 failed.
Lining up with the game
vulnboxd computes ticks from tick.start and tick.interval:
tick:
start: 2026-09-27T09:00:00Z # when the game began, or any tick boundary
interval: 60s
window: 5sstart can be any moment a tick began; vulnboxd only needs one boundary to know where all the others are. If your windows seem to fall in the middle of ticks, this is the setting to check.
Blocking during the window
A port with end_of_tick: drop refuses new connections and datagrams while the window is open. stall holds them open instead, until timeouts.stall runs out, so the seconds cost the client as well; limits.stalled bounds how many are held, and datagrams are dropped either way.
If your game's checkers are done before the window, those seconds are used by attackers alone, and blocking them takes a slice out of every attack for free. If a checker does arrive in the window, it fails, so only turn this on once you know when the checks happen, for example from the scoreboard staying green with it on for a while:
end_of_tick: drop # the whole service
ports:
"8080":
end_of_tick: allow # but not this port