• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Naive Dockerized TFS 1.5

Giorox

Member
Joined
Jun 5, 2009
Messages
19
Reaction score
7

Introduction​

So, I've lurked in the community for over a decade at this point and I owe my current career and life to OpenTibia development (which is where I started developing/programming) and thought it was way past the point that I should give back to the community. In this sense, I started looking at TFS once again as a good base for developing OTs and was pleasantly surprised to see Docker being seriously discussed.

The "pleasantness" quickly faded when I realized that most discussions on Dockers were either shot-down as "too complicated for most users" or just "too hard to get right", while most attempts to share Docker instances didn't give readers enough context to UNDERSTAND why things were the way they were.

Although I want to make knowledge on Docker more acessible, it's out of scope of this post to teach you about Docker itself, some previous knowledge is necessary. What ISN'T out of scope is giving you the know-how to interpret the principal components in using Docker for OTs. This is also a sort of self-documentation for my own future reference but I figured it could come in handy for others aswell.

The code-base used in this was the TFS1.5 Downgrade by Nekiro for protocol 8.6, so if you're using that, it should workout fine. Both files in this post have to be at the root of the repository (where your config.lua is).

My Naive Image​

The reason it's naive is because there isn't a lot of computer-trickery going on (or even at all) and there is almost no optimization done to either the Dockerfile or the docker-compose.yaml files. The only thing I've done is bring the TFS Compilation Instructions for Ubuntu and transformed it into the Dockerfile that defines the IMAGE for the SERVER portion of our OT.

YAML:
FROM ubuntu:23.04 AS build
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
  apt-get --assume-yes install \
  cmake \
  build-essential \
  libluajit-5.1-dev \
  libmysqlclient-dev \
  libboost-system-dev \
  libboost-iostreams-dev \
  libpugixml-dev \
  libcrypto++-dev \
  libfmt-dev \
  libboost-filesystem-dev \
  libboost-date-time-dev

COPY cmake /usr/src/forgottenserver/cmake/
COPY src /usr/src/forgottenserver/src/
COPY CMakeLists.txt CMakePresets.json /usr/src/forgottenserver/
WORKDIR /usr/src/forgottenserver
RUN mkdir build
WORKDIR /usr/src/forgottenserver/build
RUN cmake .. && make

FROM ubuntu:23.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
  apt-get --assume-yes install \
  libluajit-5.1-dev \
  libmysqlclient-dev \
  libboost-system-dev \
  libboost-iostreams-dev \
  libpugixml-dev \
  libcrypto++-dev \
  libfmt-dev \
  libboost-filesystem-dev \
  libboost-date-time-dev

COPY --from=build /usr/src/forgottenserver/build/tfs /bin/tfs
COPY data /srv/data/
COPY config.lua LICENSE README.md *.sql key.pem /srv/

EXPOSE 7171 7172
WORKDIR /srv
VOLUME /srv
ENTRYPOINT ["/bin/tfs"]

Explanation​

This should be pretty simple, beside it being base on the compilation instructions I took some suggestions from the Dockerfile in the TFS repository in Github and split it into 2 portions which are denoted by the FROM ubuntu:23.04 statements. The top one defines our BUILD portion of the image where our source code will be built for us into the executable using CMake.

In both cases we are using ubuntu:23.04 because that is the only version of Ubuntu that has libfmt-dev at version 9.0.0 or over (which is a requirement lib for TFS). This image is pretty big and will result in a pretty long build step (takes me about 6 minutes to build the image) and gives us an image that is roughly 600Mb in disk size.

We set an environment variable that prevents the Ubuntu shell from waiting for user input and allows us to install all necessary packages, next we RUN a couple of daisy-chained commands. First we update the package repository by call apt-get update and chain it with double ampersand ($$) to the apt-get --assume-yes install command followed by all packages listed in the compilation procedure for TFS, this ensures our environment has everything to compile the sources.

Next we copy all necessary files from our local folder to the image (cmake folder, src folder, CMakeLists.txt CMakePresets.json) and set our working directory to the /usr/src/forgottenserver directory. In this directory we create a folder called build, once again change our working directory to this new folder and call cmake .. && make which does 2 things: First it ensures that we build our make files using the files in the parent folder and then calls the make command to build our code which markes the end of the build portion.

In the second portion of the file we do most of the same things except we install less packages (since we don't neeed build-essentials or cmake as they are only used to compile the code). After having all of our packages ready in this new step, we copy the resulting binaries from the build section into our /bin/tfs folder on the resulting image. We then copy all files necessary to run a server from our local machine: data folder, config.lua, LICENSE, README, our SQL files and our RSA key.

We finish the image by exposing ports 7171 and 7172 to connections, setting our working directory to the /srv folder, exporting as a volume so we can read changes to any of our srv files whenever the server is live and setting the entrypoint to calling the TFS binary compiled previously.

Why is it naive?​

Well, there are a LOT of layers in our Dockerfile, and each layer renders more disk size and complexity to the image, things we don't inherently want. WE explicitly copy our config.lua file instead of copying the config.lua.dist and letting TFS' configmanager handle the conversion and finally we use a pretty HUGE and non-LTS base image (Ubuntu:23.04) which is a weird choice when compared to things like alpine (Which is around 30x smaller but has some documented issues when compiling and executing C/Rust/GO code).

Finally, we assume you already have a TFS repository cloned to your machine, that this file (and the following docker-compose.yaml) are in the root and that you don't want to produce nightly build from the latest commit in the master branch of the TFS repository.

But hell, it works.

What now?​

Before proceeding, make sure you build the image, open a terminal, head into the root folder of your TFS in your local machine where this should be and run:
Bash:
docker build -t local/tfs-ubuntu:1.0.0 --no-cache .

This should produce a ton of verbose statements and it shouldn't error out at any point, once it's done you should have a local/tfs-ubuntu:1.0.0 image locally available.

"Dockerizing"​

Assuming you have succesfully done the previous part, all that's left is to take the server, add our database, and fire it up. This can (mostly) all be done by specifing a docker compose like the one below
YAML:
version: '3.8'
services:
  db:
    image: mysql
    container_name: "mysql_tfs"
    restart: unless-stopped
    command: --default-authentication-plugin=mysql_native_password --log_bin_trust_function_creators=1
    tty: true
    environment:
      - MYSQL_ROOT_HOST="%"
      - MYSQL_DATABASE=tfs
      - MYSQL_USER=tfs
      - MYSQL_PASSWORD=tfs
      - MYSQL_ALLOW_EMPTY_PASSWORD=true
    volumes:
      - ./db-volume:/var/lib/mysql
    ports:
      - 3306:3306
 
  tfs:
    image: local/tfs-ubuntu:1.0.0
    container_name: "server_tfs"
    restart: on-failure
    tty: true
    ports:
      - "7171:7171"
      - "7172:7172"
    depends_on:
      - db
    volumes:
      - .:/srv
      - "./config.lua:/srv/config.lua"
      - "./key.pem:/srv/key.pem"

Explanation​

We use Docker Compose's 3.8 version just because, no apparent reason, older versions might or might not support some things in the following descriptions though. WE define our services, which in our case is just the database (called db) and the server itself (called tfs).

The database service definition has it's own "naive" quirkiness aswell, we use the official mysql image instead of mariadb, it's, again, a much bigger image and probably overkill for most people running OTs, but it doesn't try and screw you over when trying to connect locally depending on how your config is set meaning this should be a plug-and-play solution.

We call this service's container "mysql_tfs" and set it to always restart unless we explicitly stop it. The command statement has some additional CLI commands (mostly to allow for connection using a mysql native password (again, pretty naive, don't do this in production) and sets it so we can create triggers the way TFS structured them. We set tty to true so we can see prints to console from stdout and stderr (basically for logging/monitoring purposes). We then set Host, Database, User and Password for our database, these values shoudl reflect in your config.lua except for HOST, which should be set to the service name db.

We then map the image's /var/lib/mysql folder (where data is persisted) to a db-volume folder so that we can persist data if we kill the container later. This is very important, and without this you will lose all your server progress at every container restart. Finally we just expose the 3306 port for any connections from the outside.

An important thing to note is that we don't automate the Schema import at all and as such, you still have to do it manually once the database is online.

The tfs service is pretty simple, for image remember to set it to the same name you gave it when you built it in the previous step, we call the container "server_tfs" in keeping with a pattern, set it to restart whenever it fails and again set tty to true so we can see WHY the server fails and any prints to the server log (like player logins and events).

We then expose the 7171 and 7172 ports so we can connect to the server, set it to depend on th db service (so if it crashes, the server goes offline aswell) and map some volumes. These volumes map our current ROOT (where the docker-compose.yaml is sitting on your computer) to the /srv folder in the container meaning that we can make changes to the files in that folder and have it reflect it on the server just as it would normally without having to rebuild the image every time. WE do the same things for the config and key files just for good measure.

What now?​

Now, you just have to start the server. again go to your folder in a terminal windows and run
Bash:
docker-compose -p tfs-server-ot up

And you should see the database and the serve come alive (with their logs being colored for good measure).

There are some thing you have to do, the server won't be able to start everytime you turn on the container because it's a lot faster than the mysql database and you probably will have to manually start the container after the database is up, everytime (this is something that can be fixed, but again, NAIVE implementation, room for improvement).

Something you'll have to deal with is, the serve will not turn on if there is no schema, so make sure to login to the database (with DBeaver for example) and import your schema.

One last thing, RSA key's are iffy. If you get any RSA key trouble (specially if it's footer/header related), make sure the line endings are Unix-style (LF) instead of Windows-style (CRLF).

Final words​

This is probably a lot more verbose than it HAS to be and it's might not even be of use for most users on here, but if it helps someone, hope that's enough :D

1687215484418.png
 
Cool, but I guess it would be the most valuable for a community not a naive implementation but production ready implementation. For example the most users with this docker setup would not know where to look for saved logs, saved crash dumps or add features like database backup on startup. I bet that majority of users uses this bash script.
To run their OTS in their production servers. If you would cover those basic questions/features. That would be very valuable. Anyways thank you for this article.
 
Cool, but I guess it would be the most valuable for a community not a naive implementation but production ready implementation. For example the most users with this docker setup would not know where to look for saved logs, saved crash dumps or add features like database backup on startup. I bet that majority of users uses this bash script.
To run their OTS in their production servers. If you would cover those basic questions/features. That would be very valuable. Anyways thank you for this article.
For sure, the goal wasn't to provide a batteries-included implementation for the community to just pick up and go with, more so to spread knowledge and try and improve the overall foundation around this technology for the community.

I should probably have disclaimer'd this better in the intro, but it's absolutely not production ready and there are MANY improvements including, but not limited to, the OTS Restart script cited.

This should serve as a sort of tutorial for the community and maybe giving Docker some love since the official Dockerfile (atleast the one in the Github repository) is very arcane and vague, there have been many discussions back and forth on if it's worth it or not and there is absolutely no documentation for it, most people run with that image but aren't really sure why.
 
Cool, but I guess it would be the most valuable for a community not a naive implementation but production ready implementation. For example the most users with this docker setup would not know where to look for saved logs, saved crash dumps or add features like database backup on startup. I bet that majority of users uses this bash script.
To run their OTS in their production servers. If you would cover those basic questions/features. That would be very valuable. Anyways thank you for this article.
dont run production in docker, this isnt web app
 
OT is currently a monolith right? Would be awesome if map server could be ran in separate processes and threads, that would make a great appeal for Docker in PROD

But atm, it's like @Giorox said himself, this is a great learning material and to me the best kind of documentation, the one that actually you run and see it happening.


Much appreciated sir, will possibly start looking into splitting the threads in the map server myself, could be a great match with Docker and Kubernetes :)
 
I have tried this docker on tfs nekiro 1.5 downgrade 7.772 and got this error:
Can someone help?

Bash:
[+] Building 25.3s (14/18)                                 docker:desktop-linux
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 87B                                           0.0s
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 1.18kB                                     0.0s
 => [internal] load metadata for docker.io/library/ubuntu:23.04            0.5s
 => [internal] load build context                                          0.1s
 => => transferring context: 240.71kB                                      0.1s
 => [build 1/9] FROM docker.io/library/ubuntu:23.04@sha256:51e70689b125fc  1.9s
 => => resolve docker.io/library/ubuntu:23.04@sha256:51e70689b125fcc2e800  0.0s
 => => sha256:89e6336dd9e04a0993754adca328bf88d988540bb 26.07MB / 26.07MB  1.0s
 => => sha256:51e70689b125fcc2e800f5efb7ba465dee85ede9da9 1.13kB / 1.13kB  0.0s
 => => sha256:aa2be6baa498ab5862770bbc08cf00a058154cb257e41dc 424B / 424B  0.0s
 => => sha256:4783be26912a96818aa1c9468ea8acb5eff2608697f 2.31kB / 2.31kB  0.0s
 => => extracting sha256:89e6336dd9e04a0993754adca328bf88d988540bb95cff28  0.7s
 => [build 2/9] RUN apt-get update &&   apt-get --assume-yes install   c  20.2s
 => [stage-1 2/6] RUN apt-get update &&   apt-get --assume-yes install    13.2s
 => [build 3/9] COPY cmake /usr/src/forgottenserver/cmake/                 0.0s
 => [build 4/9] COPY src /usr/src/forgottenserver/src/                     0.0s
 => [build 5/9] COPY CMakeLists.txt /usr/src/forgottenserver/              0.0s
 => [build 6/9] WORKDIR /usr/src/forgottenserver                           0.0s
 => [build 7/9] RUN mkdir build                                            0.1s
 => [build 8/9] WORKDIR /usr/src/forgottenserver/build                     0.0s
 => ERROR [build 9/9] RUN cmake .. && make                                 2.6s
------
 > [build 9/9] RUN cmake .. && make:
0.104 -- The C compiler identification is GNU 12.3.0
0.136 -- The CXX compiler identification is GNU 12.3.0
0.141 -- Detecting C compiler ABI info
0.173 -- Detecting C compiler ABI info - done
0.178 -- Check for working C compiler: /usr/bin/cc - skipped
0.178 -- Detecting C compile features
0.178 -- Detecting C compile features - done
0.180 -- Detecting CXX compiler ABI info
0.217 -- Detecting CXX compiler ABI info - done
0.222 -- Check for working CXX compiler: /usr/bin/c++ - skipped
0.222 -- Detecting CXX compile features
0.222 -- Detecting CXX compile features - done
0.225 -- Found Crypto++: /usr/include
0.227 -- MySQL Include dir: /usr/include/mysql  library dir: /usr/lib/aarch64-linux-gnu
0.227 -- MySQL client libraries: /usr/lib/aarch64-linux-gnu/libmysqlclient.so;-ldl
0.228 -- Performing Test CMAKE_HAVE_LIBC_PTHREAD
0.260 -- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
0.261 -- Found Threads: TRUE
0.262 -- Found PugiXML: /usr/include
0.263 -- Found LuaJIT: /usr/lib/aarch64-linux-gnu/libluajit-5.1.so;/usr/lib/aarch64-linux-gnu/libm.so (found version "2.1.0-beta3")
0.268 -- Found Boost: /usr/lib/aarch64-linux-gnu/cmake/Boost-1.74.0/BoostConfig.cmake (found suitable version "1.74.0", minimum required is "1.66.0") found components: date_time system filesystem iostreams
0.486 -- IPO / LTO enabled
0.487 -- Configuring done
0.494 -- Generating done
0.494 -- Build files have been written to: /usr/src/forgottenserver/build
0.514 [  1%] Building CXX object CMakeFiles/tfs.dir/cmake_pch.hxx.gch
1.088 In file included from /usr/include/boost/asio.hpp:23,
1.088                  from /usr/src/forgottenserver/src/otpch.h:42,
1.088                  from /usr/src/forgottenserver/build/CMakeFiles/tfs.dir/cmake_pch.hxx:5,
1.088                  from <command-line>:
1.088 /usr/include/boost/asio/awaitable.hpp: In constructor 'boost::asio::awaitable<T, Executor>::awaitable(boost::asio::awaitable<T, Executor>&&)':
1.088 /usr/include/boost/asio/awaitable.hpp:68:19: error: 'exchange' is not a member of 'std'; did you mean 'std::__atomic_impl::exchange'?
1.088    68 |     : frame_(std::exchange(other.frame_, nullptr))
1.088       |                   ^~~~~~~~
1.088 In file included from /usr/include/c++/12/bits/shared_ptr_atomic.h:33,
1.088                  from /usr/include/c++/12/memory:77,
1.088                  from /usr/src/forgottenserver/src/otpch.h:34:
1.088 /usr/include/c++/12/bits/atomic_base.h:976:7: note: 'std::__atomic_impl::exchange' declared here
1.088   976 |       exchange(_Tp* __ptr, _Val<_Tp> __desired, memory_order __m) noexcept
1.088       |       ^~~~~~~~
2.584 make[2]: *** [CMakeFiles/tfs.dir/build.make:77: CMakeFiles/tfs.dir/cmake_pch.hxx.gch] Error 1
2.585 make[1]: *** [CMakeFiles/Makefile2:100: CMakeFiles/tfs.dir/all] Error 2
2.585 make: *** [Makefile:91: all] Error 2
------
Dockerfile:23
--------------------
  21 |     RUN mkdir build
  22 |     WORKDIR /usr/src/forgottenserver/build
  23 | >>> RUN cmake .. && make
  24 |
  25 |     FROM ubuntu:23.04
 
Back
Top