Running nodes
Operating a node
Most actions on an installed node are requests from the controller to the agent. ankor.sh shows where each action is in ankor.sh.
Launch a test server
Before a node runs real matches, have the controller launch a server without a match on it. This
is an start_server with match set to null. Join the server to check that CS2 is
installed, the ports are open and players can get in. The node needs an idle slot whose login token
Steam has not refused.
Automatic updates
A node keeps itself up to date while auto_update is on, as it is by default. It follows
the game build and both images together, since a CS2 update can break the plugin or SwiftlyS2. The
break lasts until a fixed ankor-cs2 release is out.
| What | Source of new versions | When it updates |
|---|---|---|
| The CS2 game build | Steam’s public build | Once every slot is idle |
| The CS2 image | Tags of the cs2_image repository in its registry |
At the next registry read. Running servers carry on |
| The agent image | Tags of the agent_image repository in its registry |
At the next registry read. Running servers carry on |
Update CS2 describes the game update. Follow image releases describes the image updates.
update_cs2, update_cs2_image and update_agent work whether the setting is
on or off. While it is on, a manual update to an older release of a line does not last. At the
next registry read, the image moves back to the newest release of its
line. That read follows the
manual update immediately. To keep an older release, name it by digest in agent.yml or turn the
setting off.
Keep a node on its versions
You can keep a node on its installed build and images. This suits a LAN event, or any event where
the players’ clients stay on one build. Your controller turns auto_update off with
set_config. Alternatively, set it to false in agent.yml and
restart the agent.
The node then updates nothing by itself, and it accepts launches while a newer public build is out. Players whose clients have updated cannot join its servers. Manual updates still run.
When the setting is turned back on, a node behind the public build updates as soon as every slot is idle. The images update immediately.
Update CS2
Once CS2 is installed, the node keeps it up to date by itself, unless you keep it on its versions. When Valve releases a new build, the node stops accepting launches, since players on the new client cannot join the old build. Running servers play on, and the node updates as soon as every slot is idle. It makes one attempt per build, and one more after an agent restart. After a failed attempt, the node accepts no launches until the next build or a manual update succeeds.
A manual update, update_cs2, also installs CS2 the first time. It too is refused while any
slot is busy. It takes one of the update modes. verify
repairs a damaged installation. reinstall starts over when updates keep failing.
An update usually takes a few minutes and sometimes an hour. The time depends on the host’s CPU, its connection and the size of the update. Each mode refuses to start without enough free space on the install volume:
| Mode | Free space needed on the install volume |
|---|---|
update, verify |
20 GB |
reinstall |
70 GB after the wipe |
Follow image releases
While auto_update is on, the agent reads the tags of the cs2_image and agent_image
repositories from their registry. It waits an hour between
reads. It also reads them when the agent starts, when the setting is turned on, and after a manual
update that names other images.
When the registry has a newer X.Y.Z release on the node’s release line, the agent updates to it.
A node on latest updates to any newer release.
It updates as update_cs2_image and update_agent would, the CS2 image first. Without
a newer release, the agent compares the registry’s digest for the tag with the image it runs. It
updates to a weekly rebuild of its release the same way. The node reports
each update in node_status, as it reports a manual one.
- The agent reads the running version from the image’s
org.opencontainers.image.versionlabel, which release images carry. It leaves an image without the label alone. - The new reference keeps the precision of the tag in
agent.yml.ghcr.io/ankorcs/ankor-cs2:1stays as it is and is pulled again.:1.2becomes:1.3, and:1.2.0becomes:1.3.0. lateststayslatest, so the node moves to every new line by itself. ankor.sh issueslatestbecause it deploys each new line as it ships. A controller that pins a line issues its tag, such as:0.9.- An image named by digest, or with a tag other than
X,X.Y,X.Y.Zorlatest, stays as it is. - If a pull fails, the agent tries again at the next read. If an update is refused while other maintenance runs, the agent tries again when that maintenance ends.
- If the swap or the new agent fails after an agent update’s pull, the node keeps the old agent. It
reports the failed update in
node_status. The agent then updates only to a newer release or rebuild, not to the build that failed. - If the helper crashes or runs out of memory, its rerun finishes or rolls back the swap. After a rollback, the agent tries the same build again at the next read.
- The agent reads any registry over HTTPS that allows anonymous reads, Docker Hub included.
ghcr.ioallows them for the official images.
Auto-update follows an image only when agent.yml names it by a release tag: X, X.Y, X.Y.Z
or latest. An image named by a digest, such as ghcr.io/ankorcs/ankor-cs2@sha256:…, or by any
other tag stays on its build. The other image and CS2 itself still update.
Update the CS2 image
The plugin, SwiftlyS2 and the server configuration ship together in the ankor-cs2 image.
update_cs2_image switches the node to another version. Only servers launched afterwards
use it. Images lists the version tags. The node also
follows image releases by itself.
Update the agent
update_agent replaces the agent’s container with another version of the agent image.
Running servers carry on. The node also
follows image releases by itself.
The agent cannot replace the container it runs in. Instead, it starts a helper container,
ankor-self-update-<id>, from the new image, and the helper makes the swap from outside. When the
old agent creates the helper’s container, it describes the swap in three environment variables. If
one is missing, the helper exits with an error before it touches a container:
| Variable | Holds |
|---|---|
ANKOR_SELF_UPDATE_PREVIOUS |
the id of the agent container that the helper replaces. |
ANKOR_SELF_UPDATE_SPEC |
the Docker create request for the new agent’s container as JSON, with the old container’s environment, labels, mounts and restart policy. |
ANKOR_SELF_UPDATE_FAILURE |
the path of agent-update-failed.json, in the data directory that the helper and both agents mount. |
The new agent receives the old container’s environment through ANKOR_SELF_UPDATE_SPEC. The
variables that the old image set are left out, because the new image sets its own. The helper
passes its result back through agent-update-failed.json to whichever agent runs after the swap.
Old agent
ankor-agent
Helper
ankor-self-update-<id>
New agent
ankor-agent
Starts the helperfrom the new image
The helper reads ANKOR_SELF_UPDATE_PREVIOUS, ANKOR_SELF_UPDATE_SPEC and ANKOR_SELF_UPDATE_FAILURE.
Renames it ankor-agent.previousafter stopping it
Starts it as ankor-agentfrom ANKOR_SELF_UPDATE_SPEC
The new agent connects and reports the update as under way in node_status, while the helper waits 10 seconds for it to stay up without a restart.
If the new agent held:
Removes ankor-agent.previous
The helper deletes agent-update-failed.json and exits 0. The new agent reports the update as done.
If a step failed, or the new agent exited or restarted:
Removes the new agent
Renames it back and starts it
The helper writes agent-update-failed.json and exits 0. The old agent connects again and reports the failed update.
-
The helper stops
ankor-agentand renames itankor-agent.previous, which frees the name. -
The helper creates the new agent’s container as
ankor-agent, with the old container’s settings and the new image, and starts it. The new agent connects to the controller while the helper waits, and reports the update as under way. -
If the new agent stays up for 10 seconds without a restart, the helper removes
ankor-agent.previous. It also deletesagent-update-failed.json, so an earlier failed update is no longer reported. The new agent reports the update as done. -
If a step fails, or the new agent exits or restarts within those 10 seconds, the helper undoes the steps that ran. It removes the new container, renames the old one back to
ankor-agentand starts it again. If the helper could not stop the old agent, nothing needs undoing. Either way, the helper writes the reason toagent-update-failed.jsonin the data directory. The old agent reports the failed update. -
If the helper crashes or runs out of memory, Docker restarts it, up to 3 times. The rerun checks the old agent:
- If the old agent is gone and the new agent runs, the swap held. The helper reports the update as done.
- If the old agent is gone and no new agent runs, the helper records the failure.
- If the old agent is stopped or renamed, the helper rolls the swap back as in step 4.
- If Docker refuses to show the old agent, the helper exits with an error, and Docker runs it again.
A helper stopped with
docker killstays stopped, because Docker counts it as a manual stop.
Every agent reports updates in node_status. After a rollback, the controller therefore
sees the new version send hello and disconnect, and then the old version connect again.
The helper’s container stays on the host after it exits, as the record of the swap. The next agent update removes it. Its log shows how the swap ended and what the helper rolled back:
docker ps -a --filter name=ankor-self-update-
docker logs ankor-self-update-<id>
The agent removes the images that an update replaced once no container uses them. It checks after each update and when it starts.
Update the agent from the host
If the agent cannot connect, or the controller rejects its protocol version, it cannot receive
update_agent. Update it on the host instead, with --update of the
setup script:
curl -fsSL https://ankor.sh/node-setup.sh | bash -s -- --update
Restart the agent
Restarting the agent is safe while servers run.
docker restart ankor-agent
The agent’s log lists the servers it took over and its reconnection to the controller. Logs shows how to read it.
A server whose slot has been removed from agent.yml is stopped at startup. Its demo still uploads.
One host per node
- ankor.sh connects one agent per node token at a time. It closes the socket
of a second agent with
node_connected. A second host with the sameagent.ymltherefore keeps retrying, and connects only once the first disconnects. A controller of your own should do the same, as Closing the socket describes. To move a node, remove it and set the new host up as a new node. - A host runs one agent. If an agent finds another agent container running on its host, it stops
before it changes anything. Its log names both containers. Remove the extra one with
docker rm -f <name>. Re-running setup does not help, because setup replaces only the container namedankor-agent.
Logs
The agent keeps its log and the recent output of each server. The controller reads them with
watch_logs. On the host:
docker logs --tail 200 ankor-agent
docker ps -a --filter label=ankor.slot --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'
docker logs --tail 200 CONTAINER_ID
A game server’s log is deleted with its container when the server exits.
Server console
console_command runs a console command on a running server. On ankor.sh it is under Node
options → Logs & console. Use it to inspect a server (status), not to steer a match.
Do not change the game during a match. The events record every round as it is played.
mp_restartgame, a map change, or cvars that change round rules, teams or money therefore leave
statistics that do not match the rounds played. The damage is permanent, because the events
themselves record the repeated rounds and changed rules. To play a match on from an earlier round,
restore it as Backups and restores describes.
Replace a slot’s token
When Steam refuses a slot’s Game Server Login Token, the slot receives no launches until it has a new login token. Steam refuses a token, for example, after Valve invalidated it. The controller replaces the token on an idle slot, because revoking the token of a running server drops that server.
ankor.sh replaces it automatically and on request. For a controller of your own, Replace a slot’s token has the steps. The agent logs the new token on immediately and reports whether Steam accepted it.
Demos
When a match records a demo, the agent uploads the finished file to the node’s demo_upload
target. It deletes the file once uploaded. Without a target, demos stay on the node and fill its
disk over time. Demo uploads covers the request, the answers and the retries.
An upload can fill the node’s uplink and make running servers lag. demo_upload.limit_mbit
caps the rate while any slot runs a server. Uploads return to full speed once the node is idle. To
size the cap, reserve about 10 Mbit/s of the uplink for each full server that the node runs at the
same time. Set the cap to the remainder.
Add SwiftlyS2 plugins
Drop the plugin’s folder into /srv/ankor/swiftlys2/plugins/. Servers launched afterwards load it
beside Ankor.
Paths in the game container are relative to /home/steam/cs2-dedicated/game/.
File layout shows the whole tree.
| On the node | Written by | In the game container | Access |
|---|---|---|---|
/srv/ankor/swiftlys2/ |
Setup | ||
├─ plugins/ |
Setup | ||
│ └─ MyPlugin/ |
You | csgo/addons/swiftlys2/plugins/MyPlugin/ |
Read-only |
│ └─ MyPlugin.dll |
You | csgo/addons/swiftlys2/plugins/MyPlugin/MyPlugin.dll |
Read-only |
└─ configs/plugins/ |
Setup | ||
└─ MyPlugin/ |
The agent | csgo/addons/swiftlys2/configs/plugins/MyPlugin/ |
Writable |
└─ config.jsonc |
The plugin, then you | csgo/addons/swiftlys2/configs/plugins/MyPlugin/config.jsonc |
Writable |
- The folder is the plugin as it ships, and its DLL has the folder’s name.
- The plugin writes its config at its first launch. Edit it there.
- A server loads its plugins and their configs when it starts, so changes to either apply from the next launch.
- A plugin must support the SwiftlyS2 major version of the node’s CS2 image.
- A folder named
Ankoris ignored. - Plugins run in matches too, and their changes to the game appear in the match’s events.
- Other plugins are unsupported. They must not interfere with the parts of the game that Ankor’s plugin controls, such as team assignment, the match flow and the match cvars. Report a conflict with Ankor’s plugin as an issue on GitHub.
Permissions
The agent and the game servers run as uid 1000. Setup creates the folders above owned by that user. After copying a plugin in, make it readable for the game servers:
chmod -R a+rX /srv/ankor/swiftlys2/plugins/MyPlugin
Add cfgs for servers
Your cfgs go in /srv/ankor/cfg/, which is read-only in every game container. A server sees them in
csgo/cfg/node/, and a launch runs one by listing node/<name> in
exec, without .cfg.
When the cfgs run shows when a server runs each cfg, and in
which order.
Ankor’s own cfgs sit apart from yours, in csgo/cfg/ankor/. They come from the CS2 image, and a
file that you add on the node never lands there. A node cfg that a launch runs overrides
ankor/base, but not warmup or the knife round.
| On the node | Written by | In the game container | A launch uses it with |
|---|---|---|---|
/srv/ankor/cfg/ |
Setup | csgo/cfg/node/ |
|
└─ retakes.cfg |
You | csgo/cfg/node/retakes.cfg |
exec: ["node/retakes"] |
- A server reads a cfg when it runs it, so an edit applies from the next run or launch.
- Make the cfgs readable to the game servers, as Permissions above shows.