Приветсвтую, в данной инструкции рассмотрим возможность сборки ява сервера в докер образ и последующем запуске в контейнере.
В данном примере будет вариант с использованием 3-х образов, логин-сервер, гейм-сервер и образ для инсталяции базы данных, запускать будем через docker-compose
Введение
Данная инструкция не предусматривает рассказ о том что такое контейнеризация, отличия от виртуализации, что такое docker и т.д. В интеренте достаточно информации на эту тему, с учетом того что docker с каждым днем продолжает набирать популярность. Так, что предлагаю перед прочтением этого мануала ознакомиться с общимим материалами на тему "docker и контенеризация приложений"
О докере можно почитать на официальном сайте
https://docs.docker.com/get-started/overview/
Окружение
Дев-машина: Windows 10, установленный docker desktop for windows, работает через hyper-v. С информацией по установке можно ознакомиться по ссылке https://docs.docker.com/docker-for-windows/install/
В этом мануале не будет рассматриваться какая-то конкретная сборка, их огромное количество со своими сходствами и отличиями, по этому прдеположим что у нас есть скомпилированый ява сервер с такой структурой
L2Server
├─── loginserver (файлы логин-сервера, конфиг файлы, файл запуска java процесса StartLoginServer.sh)
├─── gameserver (файлы гейм-сервера, конфиг файлы, файл запуска java процесса StartGameServer.sh)
├─── libs (необходимые библиотеки)
├─── install (файлы установки базы данных)
├─── login (sql файлы установки базы логин-сервера, файл запуска установки install-login.sh)
├─── game (sql файлы установки базы гейм-сервера, файл запуска установки install-game.sh)
Dockerfile'ы
Для того чтобы сбилдить докер-образ необходимо создать файл Dockerfile (название файла, без расширения), в котором прописываються необхходимые инструции билдера.
С синтаксисом можно ознакоиться здесь https://docs.docker.com/get-started/overview/
Один образ - один докерфайл, следовательно у нас будет 3 образа и 3 докерфайла.
Докерфайл для логин сервера разполагаем в папке loginserver, для гейм-сервера в папке gameserver, для установищка бд в папке install
В итоге получаем следующую структуру
L2Server
├─── loginserver
├─── Dockerfile
├─── gameserver
├─── Dockerfile
├─── libs
├─── install
├─── Dockerfile
Dockerfile для loginserver
#исходнй образ который взят за основу
FROM adoptopenjdk/openjdk8:jdk8u292-b10-centos
#рабочая папка внутри контейнера
WORKDIR /server
#копируем файлы логин-сервера в контейнер
COPY ["loginserver", "/server/loginserver"]
#копируем библиотеки
COPY ["libs", "/server/libs"]
#порты которые пробрасываються в контейнер
EXPOSE 2106 9014
#добавляем права на испольнение sh файла
RUN chmod +x /server/loginserver/StartLoginServer.sh
RUN chmod +x /server/loginserver/UpdateHosts.sh
#команда которая выполниться при запуске контейнера
CMD ["/bin/bash", "-c", "cd /server/loginserver/ && sh UpdateHosts.sh && sh StartLoginServer.sh"]
Примичание: что такое UpdateHosts.sh и зачем он нужен будет рассмотрено ниже.
Dockerfile для gameserver
#исходный образ который взят за основу
FROM adoptopenjdk/openjdk8:jdk8u292-b10-centos
#рабочая папка внутри контейнера
WORKDIR /server
#копируем файлы логин-сервера в контейнер
COPY ["gameserver", "/server/gameserver"]
#копируем библиотеки
COPY ["libs", "/server/libs"]
#порты которые пробрасываються в контейнер
EXPOSE 7777
#добавляем права на испольнение sh файла
RUN chmod +x /server/gameserver/StartGameServer.sh
RUN chmod +x /server/gameserver/UpdateHosts.sh
CMD ["/bin/bash", "-c", "cd /server/gameserver/ && sh UpdateHosts.sh && sh StartGameServer.sh"]
Примичание: что такое UpdateHosts.sh и зачем он нужен будет рассмотрено ниже.
Dockerfile для инсталлера базы данных
#исходные образ который взять за основу (alpine - легкий дестрибутив linux)
FROM alpine:latest
WORKDIR /db-install
#копируем файлы установки
COPY ["install/login", "/db-install/login"]
COPY ["install/game", "/db-install/game"]
#устаналиваем необходимые пакеты
#mysql client
RUN apk update && apk add --no-cache mysql-client && apk add --no-cache bash
#добавляем права на испольнение sh файлов
RUN chmod +x /db-install/login/install-login.sh
RUN chmod +x /db-install/game/install-game.sh
#команда которая выполниться при запуске контейнера
CMD ["/bin/bash", "-c", "cd /db-install/login && sh install-login.sh && cd /db-install/game && sh install-game.sh"]
Файл UpdateHosts.sh
Образ представляет собой готовый к запуску артефакт, т.е при сборке в него копируються файлы в том состоянии в котором они есть на момент сборки, следовательно для того чтобы изменить конфиг нужно каждый раз пересобирать образ. Одной из самых основных настроек логин и гейм сервера - настройки сети, а конкретнее хосты к которым биндиться сокет. Самым простым вариантом как управлять этими настройками без ребилда образа - передать нужные значения через переменные окружения (environment variables).
К примеру, в данном случае, команды заменяющие настройки в конфиге вынесены в отдельный файл для удобства
Для loginserver
#!/bin/bash
echo "Updating LoginserverHostname with $LOGINSERVER_HOST"
sed -ir "s/^[#]*\s*LoginserverHostname = .*/LoginserverHostname = $LOGINSERVER_HOST/" /server/logibserver/config/server.ini
Для gameserver
#!/bin/bash
echo "Updating ExternalHostname with $GAMESERVER_EXTERNAL_HOST"
sed -ir "s/^[#]*\s*ExternalHostname = .*/ExternalHostname = $GAMESERVER_EXTERNAL_HOST/" /server/gameserver/config/server.ini
echo "Updating InternalHostname with $GAMESERVER_INTERNAL_HOST"
sed -ir "s/^[#]*\s*InternalHostname = .*/InternalHostname = $GAMESERVER_INTERNAL_HOST/" /server/gameserver/config/server.ini
Таким образом можете вынести любые необходимые настройки, которые нужно часто менять "на лету"
К томуже переменные окружения удобно использовать для установки памяти требуемой для запуска в вашем sh файле, например StartLoginServer.sh
java -Xmx$JAVA_MAX_MEMORY l2p.loginserver.LoginServer 2>&1 | tee /server/logs/loginserver-stdout.log
В переменную можно передать $JAVA_MAX_MEMORY можно передать значение "256m", "1G" и тд
Сборка образов
Контейнер собираеться командой docker build (https://docs.docker.com/engine/reference/commandline/build/)
В данном случае выполняем команду из рут папки с сервером (L2Server) с указанием докерфайла, для того чтобы сохранить контекст, т.к по умолчанию билдер не имеет доступа к родительским деррикториям
Билд образа логинсервера
docker build -t server:loginserver -f ./loginserver/Dockerfile .
Билд образа геймсервера
docker build -t server:gameserver -f ./gameserver/Dockerfile .
Билд образа инсталятора
docker build -t server:db-install -f ./install/Dockerfile .
На данном этапе из этих образов уже можно запустить конйтенер с помощью команды docker run (https://docs.docker.com/engine/reference/run/) и получить работоспособные логин и гейм серверы, но мы идем дальше
Примичание: В данным момент нобходимо передать переменные окружения если они используються для обновления конфигурации.
База данных
Сервер базы данных так же можно запустить в отдельном контейнере, например запуск mariadb
docker run --name mariadbtest -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d docker.io/library/mariadb:10.3
Больше информации о работе с бд будет далее, когда мы будем обьеденять контейнеры в одно окружение.
Примичание: MYSQL_ROOT_PASSWORD - переменная окружения которую использует mariadb, подобно тому как выше было описан способ обновления конфигурации
docker-compose
По отдельности собраные выше образы и запущенные из них контейнеры мало чем полезны, запускать их по очереди не совсем удобно, по этому соберем стак контейнеров в одно окружение с помощью docker-compose https://docs.docker.com/compose/
docker-compose позволяет одновременно запускать и управлять несколькими контейнерами, настраивать внутренее изолированноее окружение, сеть между контейнерами, зависимости, порядки запуска и так далее.
docker-compose'у нужен свой файл с инструкциями, который парситься из YAML файла. По умолчания ожидаеться файл с одноименный названием
docker-compose.yml
Подробнее о синтаксисе: https://docs.docker.com/compose/compose-file/
Создаем файл, помещяем его корневую папку с нашим сервером, в итоге получаем следующую стркутуру
L2Server
├─── docker-compose.yml
├─── loginserver
├─── Dockerfile
├─── gameserver
├─── Dockerfile
├─── libs
├─── install
├─── Dockerfile
Содержимое docker-compose.yml
#версия парсера файла, влияет на доступность некоторых фич, более подробное описание на официальном сайта
version: "3.9"
#контейнеры, которые буду запускаться в docker-compose, их принято называть сервисами
services:
#сервис базы данных, будем использовать mariadb
mysql:
image: mariadb
restart: always
environment:
#рут пароль
MYSQL_ROOT_PASSWORD: root
#при необходимости при запуске конейтенра сразу будет создана база
MYSQL_DATABASE: lvldev
#при необходимости при запуске контейнера сразу будет создан допонительный пользователь
MYSQL_USER: localnetwork
MYSQL_PASSWORD: localnetwork
ports:
- 3306:3306
#внешняя дериктория для сохранения данных
volumes:
- D:\docker\mysql:/var/lib/mysql
healthcheck:
test: "/usr/bin/mysql --user=root --password=root --execute \"SHOW DATABASES;\""
interval: 10s
timeout: 20s
retries: 10
#инсталятор базы данных
db-install:
container_name: db-install
image: db-install
#сборка образа инсталятора из докерфайла
build:
context: .
dockerfile: ./install/Dockerfile
depends_on:
mysql:
condition: service_healthy
profiles:
- db-install
#сервис логинсервера
authserver:
container_name: authserver
image: loginserver
restart: on-failure
#сборка образа логинсервера из докерфайла
build:
context: .
dockerfile: ./loginserver/Dockerfile
ports:
- 2106:2106
environment:
#переменная окружения в которую передаем память выделяему для процесса логинсервера
JAVA_MAX_MEMORY: 256m
#переменная окружения для обновления конфига адреса биндинга для логинсервера
LOGINSERVER_HOST: "*"
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: "netstat -an | grep 9014 > /dev/null; if [ 0 == $$? ]; then echo 1; fi;"
interval: 10s
timeout: 1s
retries: 10
#внешняя дериктория для сохранения данных
volumes:
- D:\docker\logs:/server/logs
profiles:
- server
#сервис геймсервера
gameserver:
container_name: gameserver
image: gameserver
restart: on-failure
#сборка образа геймсервера из докерфайла
build:
context: .
dockerfile: ./gameserver/Dockerfile
ports:
- 7777:7777
environment:
#переменная окружения в которую передаем память выделяему для процесса логинсервера
JAVA_MAX_MEMORY: 5G
#переменная окружения для обновления конфига внешнего хоста геймсервера
GAMESERVER_EXTERNAL_HOST: "ВАШ ВНЕШНИЙ ИП ТУТ"
#переменная окружения для обновления конфига внутренего хоста геймсервера
GAMESERVER_INTERNAL_HOST: "127.0.0.1"
depends_on:
mysql:
condition: service_healthy
authserver:
condition: service_healthy
#внешняя дериктория для сохранения данных
volumes:
- D:\docker\logs:/server/logs
profiles:
- server
Теперь стоит вспомнить о конфигурационных файлах логин и гейм серверов, как написано выше - docker-compose строит внутреннюю сеть между контейнерами, в которой они могут между собой общаться с помощью именовоного хостнейма которое являеться именем сервиса в yml файле.
Например для того чтобы из java приложения в контейнере loginserver подключиться к базе данных в контейнере mysql можно использовать адресс mysql:3306, а чтобы из конейтенра gameserver подключиться к конйтенру loginserver можно использовать адресс loginserver:9104 и т.д (смысл думаю понятен)
Меняем настройки подключения к бд в конфигах логинсервера и геймсервера, например в моем случае получилось следующее
/gameserver/config/server.ini
/loginserver/config/server.ini
dataSource.url = jdbc:mariadb://mysql:3306/l2server?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
dataSource.user = localnetwork
dataSource.password = localnetwork
Также меняем настройки подключения геймсервера к логинсерверу
Например в моем случае
/gameserver/config/server.ini
LoginPort = 9014
#имя сервиса в docker-compose.yml
LoginHost = loginserver
Так же на забываем менять настройки в вашем инсталяторе базы данных
Обратите внимание на настройки в секции environment, здесь можно настроить параметры переменных окружения, которые будут переданны в контейнер при запуске, выше был описан момент обновления настроек с помощью енв-варов на лету.
Персистентность данных
Контейнер имеет своую виртуальную фаловую систему, при удалении контейнера так же удаляться все данные, в том числе и база данных сервера, соответсвенно база должна храниться гдето на хост машине.
Смотрим на настройки volumes, с помощью этой инструкции можно смонтировать папку на хост машине как виртуальную в контейнере.
ОСОБОЕ ВНИМАНИЕ пршу обратить на сервис mysql, в который смонтирована папка на хосте как /var/lib/mysql
Эта директория используеться ядром mysql для сохранения данных, следовательно все данные остануться на хост машине (в момем случае в D:\docker\mysql)
Так же, к примеру в логин и гейм серверы смонтирована папка на моем пк для записи логов D:\docker\logs как /server/logs
Запуск в docker-compose
Запустить стек контейнеров можно с помощью команды docker-compose up https://docs.docker.com/compose/reference/up/
С помощью настроек в docker-compose.yml файлы мы определили т.н профили запуска (с.м выще содержание файла, секцию profiles)
Это нужно для того, чтобы запускать определенный сервисы, при этом пропускать другие, например при запуске профиля db-install запуститься только сервис mysql и инсталятор базы данных (нам ведь нужно запустить его лишь один раз, не так ли?), а при запуске профиля server запустятся сервисы mysql, loginserver, authserver, минуя инсталятор
Запуск инсталятора
docker-compose --profile db-install up --build --force-recreate
Примичание: флаг --build используеться для того чтобы зафорсить ребилд контейнера, флаг опционален, удобно для дев окружения
Примичание 2: флаг --force-recreate ипользуеться для того чтобы пересоздать контейнер (если он ранее был создан), флаг оционален, удобно для дев окружения
Запуск логин и гейм сервера без инсталятора
docker-compose --profile server up --build --force-recreate
Так же с помощью docker-compose можно билдить образы без запуска, удобно
docker-compose build --no-cache db-install
docker-compose build --no-cache loginserver
docker-compose build --no-cache gameserver
Для того чтобы оставить все контейнеры используется команда docker-compose stop https://docs.docker.com/compose/reference/stop/
Для того чтобы остановить и удалить все контейнеры используется команда docker-compose- down https://docs.docker.com/compose/reference/down/
Продакшн
Получилось собрать и запустить все на локальной дев машине? Отлично! Что дальше?
У нас есть готовые образы, их нужно как-то передать на продакшн сервер, для этого используеться так называемый Docker реестр (Docker Registry).
Реестр может быть свой, (вопрос запуска собственного реестра это отдельная тема, которая требует отдельного манула) либо один из доступных платных или безплатных в олаке. У docker есть собственный реестр - Docker Hub https://hub.docker.com/ который предоставляет один безплатный приватный репозиторий (и неограниченное количествао публичных), его и будем использовать в рамках данного мануала
В докер хабе создаем рпозиторий, например его название будет "server". Для того чтобы отправить образы в свой репозиторий они должны называться по принцину
"ИМЯ_ПОЛЬЗОВАТЕЛЯ/ИМЯ_РЕПОЗИТОРИЯ". В моем случае образы должны именоваться "lvlkoo/server".
Можно ли загрузить несколько образом с одним именем? Нет. НО, на выручку приходят теги, у каждого образа может быть тег, и их может быть неограничение количество. Более подробнее о тегах можно почитать на официальном сайте, в основном они используеться для вариации похожих образов или версионирования
Тег образу проставляеться через двоеточие "ИМЯ_ОБРАЗА:ТЕГ". Сопоставив это с именованием образов для загрузки в репозиторий получаем следующие имена образов
Геймсервер: lvlkoo/server:gameserver
Логин-сервер: lvlkoo/server:loginserver
Инсталятор: lvlkoo/server:db-install
Подобный образом можно к примеру собирать несколько разных вариаций сборки, к примеру server:gameserver-x-1200, server:gameserver-x-100 и так далее
Перименовуем название образов в docker-compose.yml файлах (поле image). Например для инсталятора. Остальное по аналогии
image: lvlkoo/server:db-install
Ребилдим образы и пушим их в реестр с помощью команды
docker push https://docs.docker.com/engine/reference/commandline/push/
или с помощью
docker-compose push https://docs.docker.com/compose/reference/push/
Собираем docker-compose.yml файл для продакшн окружения.
В чем отличия?
Самое очевидное это то, что там будут передаваться различные переменные окружения с настройками (ип адресс хоста, итд)
Второй момент - это то, что на продакшне мы не будем билдить образы, а будем скачивать из докер реестра, если Вы посмотрите на текущий docker-compose.yml там будет секция build с указанием контекста и докерфайла, она не нужна.
Создаем файл docker-compose.prod.yml
version: "3.9"
services:
mysql:
image: mariadb
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: l2server
MYSQL_USER: localnetwork
MYSQL_PASSWORD: localnetwork
ports:
- 3306:3306
volumes:
- /home/server/mysql:/var/lib/mysql
healthcheck:
test: "/usr/bin/mysql --user=root --password=root --execute \"SHOW DATABASES;\""
interval: 10s
timeout: 20s
retries: 10
db-install:
container_name: db-install
image: lvlkoo/server:db-install
depends_on:
mysql:
condition: service_healthy
profiles:
- db-install
authserver:
container_name: loginserver
image: lvlkoo/server:loginserver
restart: on-failure
ports:
- 2106:2106
environment:
JAVA_MAX_MEMORY: 256m
LOGINSERVER_HOST: "*"
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: "netstat -an | grep 9014 > /dev/null; if [ 0 == $$? ]; then echo 1; fi;"
interval: 10s
timeout: 1s
retries: 10
volumes:
- /home/server/logs:/server/logs
profiles:
- server
gameserver:
container_name: gameserver
image: lvlkoo/server:gameserver
restart: on-failure
ports:
- 7777:7777
environment:
JAVA_MAX_MEMORY: 4G
GAMESERVER_EXTERNAL_HOST: "ИП ХОСТИНГ МАШИНИ ТУТ"
GAMESERVER_INTERNAL_HOST: "127.0.0.1"
depends_on:
mysql:
condition: service_healthy
authserver:
condition: service_healthy
volumes:
- /home/server/logs:/server/logs
profiles:
- server
Важный момент по поводу имени файла: все команды docker-compose изначально ожидают что файл будет называться docker-compose.yml, без лишних приставок, но можно дополнительно передать конкретный файл с помощью флага -f
например запуска инсталятора
docker-compose -f docker-compose.prod.yml --profile db-install up
В таком случае можно иметь по друкой несколько compose файлов, например docker-compose.dev.yml, docker-compose.prod.yml, docker-compose.prod-server=2.yml и т.д
Запуск на продакшене
Собственно кульминация истории и зачем все это нужно
Окружение: только-что купленная хостинг машина с centos на борту, без дополнительно установленного софта.
1. Устанавливаем docker https://docs.docker.com/engine/install/centos/
2. Устанвливаем docker-compose https://docs.docker.com/compose/install/
3. Создаем паки /home/server/logs и /home/server/mysql
4. Копируем файл docker-compose.prod.yml в /home/server (при этом сделав нужные изменения в переменных окружения)
5. Запускаем инсталятор docker-compose -f docker-compose.prod.yml --profile db-install up
6. Вырубаем инсталятор, запускаем сервер docker-compose -f docker-compose.prod.yml --profile server up
Несколько моментов о которых стоит упомянуть
- Для того, чтобы посмотреть список запущенных контейнеров используется команда docker ps https://docs.docker.com/engine/reference/commandline/ps/ либо
docker-compose ps https://docs.docker.com/compose/reference/ps/
- При запуске контенера (ов) вы окажитесь в т.н attached режие, в котором весь консольный ввод передаеться напрямую контейнеру, для запуска в deatached режиме нужно передать флаг -d
Чтобы приатачиться обратно можно использовать команду docker attach https://docs.docker.com/engine/reference/commandline/attach/
- Для того чтобы выполнить какую-нибудь команду в контейнер без аттача можно использовать команду
docker exec https://docs.docker.com/engine/reference/commandline/exec/ или docker-compose exec https://docs.docker.com/compose/reference/exec/
- Для просмотра стдаута приложения можно воспользоваться командой docker logs https://docs.docker.com/engine/reference/commandline/logs/
или docker-compose logs https://docs.docker.com/compose/reference/logs/
- После того как сделали какие-то изменения в файлах сервера контейнер нужно пересобрать (с.м выше про сборку образов), и перезалить в реестр (с.м выше про пуш образов) а дальше перезагрузить на продакшн сервере
Запулить нужный образ заного можно командой docker-compose pull https://docs.docker.com/compose/reference/pull/
Например
docker-compose -f docker-compose.prod.yml pull authserver
-При частой локальной пересборки будеи накапливаться мусор из старых образов. Очистить образы можно командой docker image prune https://docs.docker.com/engine/reference/commandline/image_prune/
- Тему можно развивать и настроить авматические билды и пуши образов из репозитория, с тегированием по номеру билда или ревизией (возможно будет полезно командам разработчиков)
Послеловие
Данная статья не претендует на научность или хау-ту, а лишь передает мой личный опыт. В основном я работаю с докером в другой сфере и в этом материале лишь описывается некоторый подход который возможно кому-то будет полезен. Спасибо всем кто дочитал до конца.