У меня есть собственные Rust сервера на арендованной удаленной машине. Онлайн пока что крайне мал (в основном - никого, хотя бывает и 1-3 игроков), но мне нравится настройка и администрирование, поэтому в первую очередь мой сервер мне служит в образовательных целях.

Начинал я с малого: пытался писать небольшие плагины для OxideMod с помощью ChatGPT и, организовав git репозиторий прямо в папке oxide/plugins, сделал процесс обновления плагинов максимально удобным. А недавно мне досталась задача посложнее: в свете недавнего обновления RustDedicated Server (которое стало отправной точкой) я решил наконец по максимуму автоматизировать имеющиеся задачи - об этом далее в статье.


Все началось с того, что разработчики Rust Dedicated Server починили сломали функцию restart. Она и ранее не работала, поскольку выключала сервер вместо того, чтобы перезагружать его. А теперь она стала работать согласно названию, но параллельно с ней перезагружать почему-то начала и функция quit. Очевидно, что это два названия одного и того же, но разработчики не определились, чего же именно.

Как бы то ни было - данный фикс создал мне проблему: теперь я мог выключить запущенный сервер только нативными средствами Linux:

pkill RustDedicated

и это работало исправно, пока сервер был всего один. Но серверов стало два. И выключение всех серверов каждый раз, когда я выключаю один из них - меня не устраивало. Вот тут-то и появился он.

Docker

Давно уже посматривал на эту технологию, но как-то не было повода ее применить. А теперь, когда мне потребовалось запускать каждый сервер в изолированной среде - docker оказался вполне подходящим инструментом (плюс появился хороший повод наконец изучить его на практике).

Сама по себе папка с сервером представляет собой развернутое с помощью steamcmd приложение, куда дополнительно устанавливается oxidemod. Когда я начинал использовать docker, я так же добавил в эту папку Dockerfile с настройками.

Типичная структура сервера примерно такая:

Rust_Server
├── Bundles (~ 6.5 ГБ)
├── HarmonyMods
├── RustDedicated_data (~ 700 МБ)
│   ├── Managed
│   ├── Mono
│   ├── MonoBleedingEdge
│   ├── Plugins
│   ├── Resources
│   └── StreamingAssets
├── oxide
├── scripts
├── server
│   └── melee.mayhem
│       ├── cfg
│       ├── scripts
│       └── serveremoji
├── steamapps
└── Dockerfile

Когда мне понадобилось больше серверов, я просто копировал всю папку Rust_Server каждый раз, когда хотел добавить еще один. Очевидно, что подход избыточный, особенно в условиях ограниченного места на диске, ведь папка сама по себе весит более 7 ГБ, а вдобавок еще и каждый docker-образ занимает столько же.

Поэтому в конце-концов структура эволюционировала в единственную папку, в которой для каждого нового сервера (./server/$IDENTITY) я просто добавлял локальную подпапку oxide с отдельным набором плагинов и конфиг-файлами, что значительно экономило место на диске. Для запуска/сборки всех серверов было решено использовать docker-compose.

В итоге, преобразования произошли только в подпапке server, остальная структура осталась без изменений:

Rust_Server
├── Bundles
├── HarmonyMods
├── RustDedicated_data
│   ├── Managed
│   ├── Mono
│   ├── MonoBleedingEdge
│   ├── Plugins
│   ├── Resources
│   └── StreamingAssets
├── oxide
├── scripts
├── server
│   ├── melee.mayhem
│   │   ├── cfg
│   │   ├── oxide
│   │   │   ├── config
│   │   │   ├── data
│   │   │   └── plugins
│   │   ├── scripts
│   │   └── serveremoji
│   └── oxid.vanguard
│       ├── cfg
│       ├── oxide
│       │   ├── config
│       │   ├── data
│       │   └── plugins
│       ├── scripts
│       └── serveremoji
├── steamapps
├── Dockerfile
└── docker-compose.yml

На этом этапе возникла проблема - корневая папка oxide (строка 11 выше) оказалась общей для всех контейнеров (поскольку она, как и остальные папки, примонтирована в volumes), тогда как мне было нужно, чтобы в каждом контейнере такая папка была изолированной (поскольку наборы плагинов разные).

И хотя в wiki Facepunch я не нашел, как я могу указать кастомный путь к папке oxide, решение, к счастью, было найдено.

TMPFS

В процессе изучения документации докера я наткнулся на этот тип монтирования, при котором создается временная папка, доступная только в рамках текущего контейнера и удаляемая при его остановке.

Итоговый конфиг docker-compose.yml при этом стал таким:

version: "3.8"
services:
  main_server:
    build:
      context: .
      args:
        - SERVER_ROOT=main_server
    environment:
      - IDENTITY=oxid.vanguard
      - HOSTNAME=ZGR | Zombie Got Rust | PVP - Solo.Duo.Trio.Quad
      - SERVER_URL=zgr.ddns.net
      - SERVER_TAGS=monthly,vanilla,EU
      - SERVER_PORT=28015
      - QUERY_PORT=28016
      - WORLDSIZE=3000
      - MAXPLAYERS=100
    container_name: main_server
    ports:
      - "28015:28015/udp"
      - "28016:28016/udp"
      - "25888:25888"
      - "80:80"
      - "443:443"
    volumes:
      - .:/main_server
    tmpfs:
      - /main_server/oxide
    command: ./run-server.sh
  mayhem_server:
    build:
      context: .
      args:
        - SERVER_ROOT=mayhem_server
    environment:
      - IDENTITY=melee.mayhem
      - HOSTNAME=ZGR | Melee Mayhem
      - SERVER_URL=zgr.ddns.net
      - SERVER_TAGS=weekly,vanilla,EU
      - SERVER_PORT=28017
      - QUERY_PORT=28018
      - WORLDSIZE=1200
      - MAXPLAYERS=150
    container_name: mayhem_server
    ports:
      - "28017:28017/udp"
      - "28018:28018/udp"
      - "25889:25888"
    volumes:
      - .:/mayhem_server
    tmpfs:
      - /mayhem_server/oxide
    command: ./run-server.sh
Мысли насчет конфига

В конфиге выше меня смущает только необходимость дублировать SERVER_ROOT (см args, container_name, volumes, tmpfs), поэтому если не найду другого решения - просто создам bash-script для генерации docker-compose.yml, а дублирующееся значение перенесу в переменную.

А в файле для запуске сервера run-server.sh я просто выполняю копирование из текущей подпапки oxide в изолированную корневую tmpfs папку oxide:

cp -R ./server/$IDENTITY/oxide/* ./oxide

Однако, данный подход забрал у меня одну важную фичу - live reload папки oxide. Папка копируется всего один раз на старте и больше не обновляется. Т.е. если я залью новый плагин в эту папку (./server/$IDENTITY/oxide), мне придется подключаться к контейнеру:

docker exec -it mayhem_server /bin/bash

и выполнять вот это действие вручную:

cp -R ./server/$IDENTITY/oxide/* ./oxide

Поэтому было решено использовать watch на каждой папке ./server/$IDENTITY/oxide и выполнять обновление tmpfs oxide при изменениях. В самом docker уже имеется такая фича, но она помечена как experimental, что лично меня отпугивает, поэтому решил пока воспользоваться проверенными средствами линукс - inotify-tools.

Я сделал bash-скрипт watch-oxide-dir.sh, который будет отслеживать изменения в папках oxide каждого сервера и при необходимости обновлять содержимое tmpfs oxide:

#!/bin/bash

while inotifywait -r ./server/$IDENTITY/oxide -e modify,create,delete; do
    cp ./server/$IDENTITY/oxide/discord.config.json ./oxide
    cp ./server/$IDENTITY/oxide/oxide.config.json ./oxide

    rsync -a --delete ./server/$IDENTITY/oxide/config ./oxide
    rsync -a --delete ./server/$IDENTITY/oxide/plugins ./oxide
    rsync -a --delete ./server/$IDENTITY/oxide/data ./oxide
    rsync -a --delete ./server/$IDENTITY/oxide/lang ./oxide
    rsync -a --delete ./server/$IDENTITY/oxide/logs ./oxide
done

Итоговый run-server.sh стал таким:

#!/bin/bash

SEED=$(cat ./server/$IDENTITY/seed.txt)

cp -R ./server/$IDENTITY/oxide/* ./oxide

"./set-permissions.sh" &
"./watch-oxide-dir.sh" &

./RustDedicated -batchmode \
    +server.hostname "$HOSTNAME" \
    +server.identity "$IDENTITY" \
    +server.maxplayers $MAXPLAYERS \
    +server.worldsize $WORLDSIZE \
    +server.seed "$SEED" \
    +server.tags "$SERVER_TAGS" \
    +server.url "$SERVER_URL" \
    +server.port $SERVER_PORT \
    +server.queryport $QUERY_PORT;

Здесь добавление амперсанда (&) на строке 7 и 8 делает скрипт выполняемым в фоне.

Итоговый Dockerfile с необходимыми зависимостями, устанавливаемыми при сборке образа:

# syntax=docker/dockerfile:1
FROM ubuntu:22.04

RUN apt-get update && \
    apt-get install -y openssl iproute2 ca-certificates inotify-tools rsync && \
    apt-get clean

ARG SERVER_ROOT

WORKDIR /${SERVER_ROOT}

EXPOSE 28015 28016 28017 28018 25888 80 443

COPY . .

Далее все отлично запускается командой docker-compose up -d

В качестве бонуса буду рад поделиться ссылкой на свой консольный rcon-client для Rust серверов, который написан на языке Rust. Предельно прост в использовании.

А вы администрировали подобные сервера ?

Комментарии (16)


  1. Nnnnoooo
    13.08.2023 21:02
    +1

    докер это конечно хорошо.
    Но вместо того чтобы создать нормальный примитивный сервис (как раз systemd с этим очень хорошо справляется), пляски с бубном с докером


    1. ivanuzzo Автор
      13.08.2023 21:02
      -1

      в случае с OxideMod, установленном поверх RustDedicated, предполагается, что папка oxide должна быть в корне. Сервис, который вы предлагаете создать, позволяет создать изолированную папку oxide, которая не будет перезаписана другим таким же сервисом ?

      как это примерно будет выглядеть, можете, пожалуйста, показать ?


      1. Revertis
        13.08.2023 21:02
        +3

        Можно создать два сервиса, и каждому прописать свою WorkingDirectory.


      1. 13werwolf13
        13.08.2023 21:02
        +2

        [werwolf@power] ~  
        ❯ systemctl cat valheim-server@.service 
        # /usr/lib/systemd/system/valheim-server@.service
        [Unit]
        Description=Valheim dedicated server
        Wants=network-online.target
        After=multi-user.target network.target network-online.target
        
        [Service]
        Type=simple
        User=valheim
        Group=valheim
        Environment="LD_LIBRARY_PATH=/var/lib/valheim/%i/linux64"
        EnvironmentFile=/etc/valheim-%i.conf
        WorkingDirectory=/var/lib/valheim/%i
        ExecStartPre=+/usr/bin/steamcmd +login $STEAM_LOGIN +force_install_dir /var/lib/valheim/%i +app_update 896660 validate +exit
        ExecStartPre=+/usr/bin/chown -R valheim:valheim /var/lib/valheim
        ExecStart=/var/lib/valheim/%i/valheim_server.x86_64 -name $SERVER_NAME -port $PORT -world $WORLD_NAME -password $PASSWORD
        Restart=always
        RestartSec=30
        TimeoutStartSec=300
        NoNewPrivileges=yes
        PrivateTmp=yes
        PrivateDevices=yes
        ProtectKernelTunables=yes
        ProtectKernelModules=yes
        ProtectControlGroups=yes
        ProtectSystem=strict
        ProtectHome=read-only
        SystemCallFilter=~@mount
        ReadWritePaths=/var/lib/valheim/%i
        
        [Install]
        WantedBy=multi-user.target

        примерно вот так

        systemctl start valheim-server@XXX.service вызовет запуск юнита в котором все %i заменятся на XXX, тоесть будет создана директория /var/lib/valheim/XXX в неё будет установлен valheim dedicated server, в ней же он будет запущен, и только в неё же он сможет писать, а все необходимые переменные будут взяты из файла /etc/valheim-XXX.conf.

        писал на память, может чего-то и опечатался, или упустил, но думаю общий смысл понятен.

        systemd решает ВСЕ ваши проблемы с запуском сервисов, совершенно незачем для этого использовать docker. локально docker в первую очередь это инструмент тестирования. он больше имеет смысл в составе кубового кластера где нужно быстренько скалировать, где нужно чтобы работали life и health метрики (кстати, аналог health есть и у systemd, гуглить sd_notify). в общем не в ту сторону вы копнули.


        1. ivanuzzo Автор
          13.08.2023 21:02

          спасибо за пример, смысл понятен. А можете, пожалуйста, подсказать, чем подход с докером хуже, чем ваш ?


          1. 13werwolf13
            13.08.2023 21:02
            +2

            1) зачем тащить на сервер дополнительный софт?
            2) зачем тащить на сервер дополнительный рантайм пусть и в образе контейнера?
            3) зачем нужны дополнительные абстракции?
            4) зачем переусложнять конфигурацию сервиса и добавлять дополнительные точки отказа?

            по большому счёту разворачивание сервисов в докере это НЕ плохо, просто зачастую это оверинженеринг без которого можно было обойтись. вроде бы не страшно что вы заняли на сервере пару лишних мегобайт дискового пространства и пару мегобайт ОЗУ, но из таких вот "да пусть будет" складываются привычки, а плохие привычки имеют свойство в будущем стрелять в ногу. конечно же иногда есть весомые причины развернуть что либо именно в контейнере, но в вашем кейсе я таких не вижу.


            1. ivanuzzo Автор
              13.08.2023 21:02

              а как в вашем случае быть с обновлениями нескольких серверов ? и отдельно интересует, как происходит автоматизация через крон - вы прописываете для каждой директории /var/lib/valheim/XXX отдельное правило в crontab или это можно как-то обобщить (по типу перебора всех подпапок в bash-скрипте, вызова там обновлений и уже этот скрипт вызывать в кроне?)

              с моим подходом я устанавливаю обновления всего раз в месяц (force wipe) в одну единственную папку и эти изменения автоматически затрагивают все имеющиеся инстансы серверов.


              1. 13werwolf13
                13.08.2023 21:02
                +1

                если присмотреться в содержимое сервисфайла то можно увидеть что один из ExecStartPre существует как для начальной установки так и для последующего обновления при перезапуске. так что для обновления конкретного инстанса я просто перезапускаю его (к тому же у меня в cron есть скрипт для бекапа всех инстансов который первым делом останавливает службы а предпоследним запускает их, поэтому автообновление раз в сутки гарантировано.

                не берусь утверждать что это лучшее и/или самое правильное решение, но лучше я ничего не придумал за те 5 минут что потратил на эту задачу (самое обидное что играть то я не играю, некогда, для друзей держу несколько серверов)

                почему было сделано именно так: одна из задач которую я решал делая этот сервис было опакетить его покласть в репозитории дистрибутива сервер для игры, поскольку нет явного разрешения класть серверную часть игры в репозиторий (и уж тем более исходников для сборки) это был самый простой и ничего не нарушающий путь.

                правда версия с несколькими инстансами в репозитории не попала, я всё ещё ленюсь допроверить все возможные подводные камни, но одноинстансовый вариант уже давно там и его можно установить как пакет родным пакетным менеджером opensuse - zypper, возможно в будущем я так же опакечу для красношляпых и для deb, но пока на это времени просто нет. зато этот service легко модифицировать под любой сервер ставящийся силами steamcmd.


            1. Nnnnoooo
              13.08.2023 21:02

              спасибо за развернутый ответ. Именно это я имел ввиду когда писал что докер излишен в данном случае


  1. saboteur_kiev
    13.08.2023 21:02
    +4

    и это работало исправно, пока сервер был всего один. Но серверов стало два. И выключение всех серверов каждый раз, когда я выключаю один из них - меня не устраивало. Вот тут-то и появился он.

    То есть вместо того, чтобы заменить pkill на kill, и вырубать нужный сервер по его process ID, вы замутили целый докер, а не разобравшись как пробросить каталог внутрь, замутили синхронизацию с рамдиском.

    Давайте я решу все ваши проблемы, поправив run-server.sh

    #!/bin/bash
    WORKDIR=$(dirname $0)
    PIDFILE=${WORKDIR}/server.pid
    
    if [ "$1" == "start" ]; then
      [ -e ${PIDFILE} ] && echo "Server is already running" && exit 1
    
    SEED=$(cat ${WORKDIR}/server/$IDENTITY/seed.txt)
      cp -R ${WORKDIR}/server/$IDENTITY/oxide/* ${WORKDIR}/oxide
      ./set-permissions.sh &
      ./watch-oxide-dir.sh &
    
      ${WORKDIR}/RustDedicated -batchmode \
        +server.hostname "$HOSTNAME" \
        +server.identity "$IDENTITY" \
        +server.maxplayers $MAXPLAYERS \
        +server.worldsize $WORLDSIZE \
        +server.seed "$SEED" \
        +server.tags "$SERVER_TAGS" \
        +server.url "$SERVER_URL" \
        +server.port $SERVER_PORT \
        +server.queryport $QUERY_PORT &
      echo $! > ${PIDFILE}
    elif [ "$1" == "stop" ]; then
      [ ! -e "${PIDFILE}" ] && echo "Server is not running" && exit 0
      read PID<${PIDFILE}
      if ps $PID >/dev/null 2>&1; then
        kill $PID
      else
        echo "Server is not running and previous shutdown was not graceful"
      fi
    else
      echo "Usage: $0 start|stop"
    fi
    


    1. ivanuzzo Автор
      13.08.2023 21:02

      если честно, я с этого начинал - пытался запоминать pid, чтобы потом вырубать через kill, только потом у меня в чем-то случилась загвоздка и я не смог этот вариант довести до конца.

      замутили целый докер

      а можете подсказать, почему использование докера будет избыточно ? кроме того, что нужно устанавливать новый софт.


      1. saboteur_kiev
        13.08.2023 21:02

        Ну тем что вам пришлось
        1. устанавливать докер
        2. докеризировать сервер, что включает в себя и сеть и проброс дисков
        3. вместо проброса дисков, вы нашли возможность использовать рамдрайв, но его надо синхронизировать - сделали еще лишний скрипт
        4. усложнили поддержку своего сервера.


        И вместо всего этого - можно просто удалить процесс по PID.


        1. ivanuzzo Автор
          13.08.2023 21:02

          но корневая папка oxide-то все еще общая в вашем случае. Т.е. если я запущу второй сервер - он перезапишет эту папку и будет конфликт. И в итоге придется вернуться к изначальному подходу - т.е. копировать папку для каждого нового сервера.

          Пожалуйста, исправьте меня, если я ошибаюсь.

          проброс дисков

          можете уточнить, что вы называете "пробросом дисков" ? монтирование (volumes) ? Если да, то несовсем понятен ваш следующий пункт

          вместо проброса дисков, вы нашли возможность использовать рамдрайв

          я и пробрасываю диск (вся папка Rust_Server) и использую рамдрайв (исключительно для одной подпапки, которая должна быть локальной для каждого запущенного сервера).

          Т.е. не "вместо", а "вместе с" в данном случае будет корретнее.

          усложнили поддержку своего сервера

          можете развить эту мысль ? в чем сложность поддержки ?


          1. saboteur_kiev
            13.08.2023 21:02

            Простите, а как вы раньше запускали два сервера?

            Если вы хотели сделать какие-то папки общими для всех серверов - сделайте линками. Просто я не знаком с архитектурой серверов, что там можно обобщать, что нельзя. Но я уверен что Докер - это явно не тот инструмент, если вы хотели сэкономить место на дисках, и ln тут будет и быстрее и проще.


  1. unwrecker
    13.08.2023 21:02
    +1

    Хотите больше игроков? Отключите EAC. Я знаю всего один сервер сейчас где можно играть из-под Линукса, остальные не пускают из-за того что EAC не пашет в wine.


  1. bororo
    13.08.2023 21:02

    люто