factoriotools / factorio-docker

Factorio headless server in a Docker container
https://hub.docker.com/r/factoriotools/factorio/
MIT License
1.08k stars 243 forks source link

Auto-update if no one is online #543

Open codegain opened 3 weeks ago

codegain commented 3 weeks ago

Hi,

I'm looking for a way to upgrade the server to the latest "stable" tag when no player is online. My server is configured to pause the game if no player is online. If I'm not at home, my friends sometimes have to downgrade their factorio version to an older version because the server isn't upgraded yet.

I have an .sh file to automatically backup the save, shut down the container, upgrade it and start it again. I haven't found a way to fully automate this to upgrade the server if no one is online.

I could use a cronjob, but this would disrupt the gameplay if a player is online at that time.

Is there a way to read periodically if some player is online and if not, upgrade the server to a new version like once per hour maybe (or to start my custom script)?

tripplet commented 3 weeks ago

You could use the the rcon protocol to check for that, I wrote an update script a long time ago, which also included the manually building the container (this is no longer necessary): auto-update The rcon client used in the script is this one: rcon But any other rcon client should also work

codegain commented 3 weeks ago

@tripplet Thanks! I will give that a try and report back with my script (if anyone else wants this too)

DennisGHUA commented 3 weeks ago

Yes, an "Auto-update if no one is online" feature would be a great addition. Currently, stable updates are being released rapidly. While this pace might slow down in the future, another DLC or major update could bring back the same problem. To future-proof this project, it would be ideal to implement this feature.

At the moment, I have a Watchtower instance that updates the server at 4:00 AM, assuming no players are online. I also have a Watchtower container that runs only once when I boot it, allowing for a quick instant update. However, having a feature that checks if all players have left the server before updating would be ideal. Optionally, it could include a maximum delay, like 24 hours, before forcibly kicking players to update the server.

For now, please share the scripts you have so I can set up a better system on my machine.

Fank commented 2 weeks ago

I would like to have something like this. The only question I would have is how this could be added, because it should not be part of the image, but something like a sidecar. The simples option would be something that controls the https://github.com/factoriotools/factorio-docker/blob/master/docker/docker-compose.yml deployment. But that would be quite static for docker compose only users, but at least a good start.

jessielw commented 2 weeks ago

The container already contains an update script that runs when starting https://github.com/factoriotools/factorio-docker/blob/master/update.sh. A couple ways this could be done, some sort of cron script that is ran every X time. Supervisord etc.

I'd be interested in this as well, there is constant patches so it has to be updated all the time.

Fank commented 2 weeks ago

@jessielw This script is not part of the container. It is only used for the updater which checks for new versions then build new docker images based on it.

jessielw commented 2 weeks ago

Ah I see that now. Certainly this could be resolved with Supervisord/SteamCMD or cron with basic scripts.

codegain commented 2 weeks ago

Some progress from my side:

I found https://github.com/gorcon/rcon-cli which can be installed via docker and executed via docker run as running this factorio server already presumes that you have docker installed (but there would also be a binary available).

To get the currently online players I use this command:

docker run -it --rm outdead/rcon ./rcon -a 172.17.0.2:27015 -p $(cat /opt/factorio/config/rconpw) /players | grep -i online
if [ $? -eq 0 ]; then
    echo "  Found active players. Exit"
    exit
else
    echo "  No active players"
fi

There are multiple requirements for this to work:

  1. The factorio server has to run with IP 172.17.0.2 (I currently have no idea how to read it from the factorio docker container)
  2. You run the factorio image with the rcon-port exposed (-p 27015:27015/tcp)
  3. The rcon password is set via the file /opt/factorio/config/rconpw which is the factorio installation directory + /config/rconpw

The command (without | grep -i online) will give you a list of all players:

Players (2):
  PlayerA
  PlayerB

If there is a player online:

Players (2):
  PlayerA
  PlayerB (online)

the grep only exists with an exit code of 0 if online could be found and would not run the rest of you upgrade script. (Thanks @tripplet for your script above)

Some help would be appricated:

  1. Read ip address of factorio docker container from docker somehow SOLVED
  2. Read/configure factorio installation directory (maybe read from the docker volumes?) for the rconpw SOLVED
  3. Before running any of this, check if there is a newer version of factoriotools/factorio:stable available - because we don't want to shutdown the server every couple of hours just because there is no one online. I tried with docker inspect to get the digest and curl to fetch infos from Docker Hub - but apparently the digest is the index digest and Docker Hub only published image digests in their /tags endpoint.

EDIT:

Requires jq to be installed and the name of the container to be factorio

  1. can be done with docker inspect factorio | jq -r '.[].NetworkSettings.Networks.bridge.IPAddress' and RCON PORT with docker inspect factorio | jq -r '.[].Config.Env[] | select(. | contains ("RCON_PORT"))' | awk -F= '{print $2}'
  2. can be done with docker inspect factorio | jq -r '.[].Mounts.[].Source'
codegain commented 2 weeks ago

This is currently what I came up with:

#!/bin/sh

# configure
IMAGE_NAME="factoriotools/factorio"
TAG_NAME="stable"
CONTAINER_NAME="factorio"
# END configure 

echo "Updating factorio..."

LOCAL_DIGEST=$(docker images --digests --no-trunc | grep $IMAGE_NAME | grep $TAG_NAME | awk '{print $3}')
REMOTE_DIGEST=$(curl -s "https://hub.docker.com/v2/repositories/$IMAGE_NAME/tags/$TAG_NAME/" | jq -r '.digest')

if [ "$LOCAL_DIGEST" = "$REMOTE_DIGEST" ]; then
    echo " No update available for $IMAGE_NAME:$TAG_NAME ($LOCAL_DIGEST == $REMOTE_DIGEST)"
    echo " Exit"
    exit
else
    echo " Update for $TAG_NAME available!"
fi

echo "Checking if any player is online..."

IP_ADDRESS=$(docker inspect $CONTAINER_NAME | jq -r '.[].NetworkSettings.Networks.bridge.IPAddress')
RCON_PORT=$(docker inspect $CONTAINER_NAME | jq -r '.[].Config.Env[] | select(. | contains ("RCON_PORT"))' | awk -F= '{print $2}')
MOUNT=$(docker inspect $CONTAINER_NAME | jq -r '.[].Mounts.[].Source')

echo " for container $CONTAINER_NAME (rcon $IP_ADDRESS:$RCON_PORT) at $MOUNT"

docker run -it --rm outdead/rcon ./rcon -a $IP_ADDRESS:$RCON_PORT -p $(cat $MOUNT/config/rconpw) /players | grep -i "(online)"
if [ $? -eq 0 ]; then
    echo "  Found online players. Exit"
    exit
else
    echo "  No online players"
fi

echo "Continue to upgrade..."

# server is ready for update - run you update logic here
# f.e.
# docker stop factorio
# docker rm factorio
# docker pull factoriotools/factorio
# sudo docker run -d -p 34197:34197/udp -p 27015:27015/tcp -v /opt/factorio:/factorio --name factorio --restart=unless-stopped factoriotools/factorio:stable

The script in this state does only check and not update the container (if you haven't commented out the last lines) just to test if it works for your given setup.

This could be executed by a cron on the host to periodically check if there is an update available and update the server if no player is online.

tripplet commented 2 weeks ago

Arg I also finally found some time today 😄

My idea was to add an rcon client to all factorio images. This is a very minimal c implementation, just 200 lines C code without any dependencies (the result binary is only ~34kB).

The rcon implementation is tailored for this purpose, no option to set a host, port or password. All required values pulled from the factorio images environment variables.

It is only there to send commands to the running factorio server inside the same container and read the response.

To use it simply run docker exec running-factorio-container rcon ARGUMENTS

For example: docker exec factorio rcon /players or docker exec factorio rcon /promote SomePlayer

I think this is a nice addition in general, outside of the use for updating as a build-in way to talk to the factorio server, for example in scripts.

I also added a simple script which checks for the active player count. Using that it should be possible to leverage the pre-update hook of watchtower: https://containrrr.dev/watchtower/lifecycle-hooks/

I have not tested that part yet, but wanted to post it now to combine our efforts.

See the following pull request

codegain commented 2 weeks ago

@tripplet Awesome! That would massively simplify a manual updating script to

#!/bin/sh

# configure
IMAGE_NAME="factoriotools/factorio"
TAG_NAME="stable"
CONTAINER_NAME="factorio"
# END configure 

echo "Updating factorio..."

LOCAL_DIGEST=$(docker images --digests --no-trunc | grep $IMAGE_NAME | grep $TAG_NAME | awk '{print $3}')
REMOTE_DIGEST=$(curl -s "https://hub.docker.com/v2/repositories/$IMAGE_NAME/tags/$TAG_NAME/" | jq -r '.digest')

if [ "$LOCAL_DIGEST" = "$REMOTE_DIGEST" ]; then
    echo " No update available for $IMAGE_NAME:$TAG_NAME ($LOCAL_DIGEST == $REMOTE_DIGEST)"
    echo " Exit"
    exit
else
    echo " Update for $TAG_NAME available!"
fi

echo "Checking if any player is online..."
docker exec $CONTAINER_NAME rcon /players | grep -i "(online)"
if [ $? -eq 0 ]; then
    echo "  Found online players. Exit"
    exit
else
    echo "  No online players"
fi

echo "Continue to upgrade..."

# server is ready for update - run you update logic here
# f.e.
# docker stop factorio
# docker rm factorio
# docker pull factoriotools/factorio
# sudo docker run -d -p 34197:34197/udp -p 27015:27015/tcp -v /opt/factorio:/factorio --name factorio --restart=unless-stopped factoriotools/factorio:stable

and possibly remove the need to expose the RCON port as this is all done within the container, right? Or just using watchtower and let it handle the upgrade with a pre-update hook.

tripplet commented 2 weeks ago

Nice script. I think both are valid approaches maybe someone does not want to have a watchtower container running the whole time just to check for updates and just have a simple cron job script which runs when the want it to or trigger an update manually using this script.