Deployment configuration for the WordOnline servers. Application code lives in the module repositories; this repository holds the blue/green deployer for the game server and the definitions of the slots it manages.
Secrets are never committed. env/*.example tracks the key names; the filled
files exist on the server only.
Replaces watchtower for the game module. Watchtower stops a container before
it starts the replacement, which kills every match in progress. This deployer
starts the new version on the idle slot first, then drains the live one and
lets it exit by itself once its session count reaches zero.
No proxy switching is involved: the client asks the lobby for a game server and
gets the protocol://domain:port the server registered in the Server table.
Blue and green register different hostnames, so handing matches to the new slot
is just the new container coming up ACTIVE and the old one going DRAINING.
Each environment owns two containers. Both exist at all times; one runs, the other sits stopped. A deploy rebuilds the stopped one on the new image, starts it, then drains the live one — which then sits stopped, ready to be the target of the next deploy.
ac-game |
dev-game |
|
|---|---|---|
| Image tag | :latest, pushed from WordOnlineServer deploy |
:dev, pushed from its main |
| Containers | ac-game-blue, ac-game-green |
dev-game-blue, dev-game-green |
| Hostnames | blue.game.ac.theevilent.com, green.game.ac.theevilent.com |
blue.game.dev.ac.theevilent.com, green.game.dev.ac.theevilent.com |
| Group label | wordonline.group=ac-game |
wordonline.group=dev-game |
| Poll | 300s | 60s |
| Drain limit | 1h | 5m |
Both slots run together only during the overlap, from the moment the new one reports healthy until the last match on the old one ends. Size the host for both environments overlapping at once — worst case four game servers.
Each slot needs its own hostname in nginx, pointing at that container. Two
slots sharing a hostname cannot overlap: nginx can only route it to one of
them, and both would write to the same Server row, since the server looks its
row up by domain and port.
Nowhere in this repository. A slot is defined by its own container.
The deployer rebuilds a slot the way watchtower does — inspect the container, hand the same config back to the create endpoint, swap only the image. Volumes, env, networks, labels and resource limits come from the container that was already running correctly, so they cannot drift from what the slot needs, and there is no list here to forget an entry from.
Config the image supplies is subtracted first. A container's inspected env and
labels mix what was asked for at create time with what the image itself
defined, and carrying the mix forward would let the old image's JAVA_HOME or
entrypoint shadow the new image's. Only the deliberate settings are carried
over.
The cost is that a converged slot lives only on the host. slots/*.yml is the
written-down version to fall back on — enough to recreate a slot from nothing,
but it does not track what the deployer has since converged the container to.
Two ways, both run once per environment.
From the checked-in definition:
docker compose -f slots/dev-game.yml createOr by cloning a game container that already runs correctly, which is the safer option when the running container has picked up settings the definition does not know about:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-e SOURCE=ac-game -e GROUP=ac-game \
-e BLUE_DOMAIN=blue.game.ac.theevilent.com \
-e GREEN_DOMAIN=green.game.ac.theevilent.com \
--entrypoint /usr/local/bin/seed.sh \
ghcr.io/apptive-game-team/arcane-casters-deployer:latestBoth slots are created stopped. The source container keeps running and serving
— seeding does not hand over. seed.sh prints the handover steps: start one
slot, confirm it registers, drain the source, then remove the source so its
restart policy or its compose project cannot bring it back.
Two things are changed on the way in. DOMAIN becomes the slot's own
hostname, because two slots sharing one address would fight over a single
Server row. And compose labels are dropped, so docker compose in the source
project does not treat the clones as its own service and remove them.
The host needs three things from this repository: docker-compose.yml, the
.env beside it, and slots/ while the slots are being created. swap.sh and
the rest are the image's source — CI bakes them into
arcane-casters-deployer, and the host only pulls that.
- Create
.envfromenv/deployer.env.examplewith the image tags and aCICD_TOKENper environment (a JWT carrying theWORDONLINE_CICDauthority; the dev token is not valid in production). docker login ghcr.iowith a token that hasread:packages. Both the game and deployer images are private. The deployer mounts the resulting~/.docker/config.json: registry credentials belong to the docker client, not the daemon, so a pull from inside the container is anonymous without it. SetDOCKER_CONFIG_DIRin.envif that file lives elsewhere.- Point nginx at the four slot hostnames.
- Seed the slots for each environment, and hand over from the old container.
docker compose up -d
To run only one environment: docker compose up -d ac-game-deployer.
Every POLL_INTERVAL seconds the deployer pulls IMAGE and compares its ID
against the live slot. When they differ:
- Rebuild the idle slot from its own config on the new image, and start it.
- Poll
HEALTH_PATHover the docker network until the body containsHEALTH_UP_PATTERN(HEALTH_TIMEOUT). On failure the new slot is stopped, its last 50 log lines are reported, and the live slot is left alone. - Wait
SETTLE_DELAYseconds with both slots up. The lobby decides availability from its own scheduled probe of each slot's public URL, not from theServertable, so the new slot only enters rotation on the lobby's next refresh. Draining without this wait empties the pool and matches fail with "no available game server". - Call
DRAIN_METHOD DRAIN_PATHon the live slot with theCICD_TOKENas a bearer token. - Wait for it to exit on its own (
DRAIN_TIMEOUT). If matches outlive that, stop it withSTOP_GRACEseconds of SIGTERM grace.
A swap can run longer than the poll interval, so swap.sh takes a lock around
its whole run and exits 75 if another swap already holds it. The lock is inside
the script rather than the polling loop because a hand-run swap has to be
serialised against a scheduled tick too — two at once read each other's
half-finished work as an interrupted swap and drain the slot the other just
started.
If both slots are found running — the deployer was restarted mid-drain — the one already on the new image is kept and the other is drained, finishing the interrupted swap instead of starting a third container.
If no slot is running at all, the idle one is rebuilt on the current image and started. The deployer doubles as the thing that brings the game server back.
The java process is PID 1, so a drained server exiting with status 0 stops the container by itself. The deployer only waits for that exit; it does not remove the container, because that container's config is what the next swap rebuilds the slot from.
Slots are seeded with on-failure, never always, even when the source
container used always. A drained server exits 0 on purpose, and always
would read that as a crash and bring the old version straight back up. The swap
also stops the container explicitly after it exits, in case a slot picked up a
reviving policy some other way.
docker compose exec ac-game-deployer swap.shExit 75 means a swap was already running and this one did nothing.
Nothing updates it automatically; it is excluded from watchtower and does not replace itself. After a new deployer image is pushed:
docker compose pull && docker compose up -dSafe at any time. Restarting a deployer mid-swap only abandons the wait — the new slot keeps running and the next tick finishes the drain.
The deployer mounts /var/run/docker.sock, which is equivalent to root on the
host. Do not publish any port on these containers, and only run an image built
from this directory.
| Image | Built by |
|---|---|
ghcr.io/apptive-game-team/arcane-casters-game |
WordOnlineServer — latest from deploy, dev from main |
ghcr.io/apptive-game-team/arcane-casters-deployer |
this repository, on every push touching game/ |
Both are private. The host needs docker login ghcr.io with a read:packages
token before either can be pulled.
Not filled in yet: compose for lobby, account, admin and website,
the watchtower service that keeps those up to date, and the Prometheus scrape
configuration. Those need the files currently living on the server.