From 10fbd27bbaa34f72984dfe55b421b9b5b6e5600d Mon Sep 17 00:00:00 2001 From: TheAssassin Date: Thu, 13 Apr 2023 01:22:11 +0200 Subject: [PATCH] Add Docker build configuration for server The final image is around 150M in size, which is totally reasonable. Most of the size is added by the maps. --- .dockerignore | 16 +++ .gitignore | 2 + docker/Dockerfile | 68 +++++++++++ docker/docker-compose.yml.example | 25 ++++ docker/generate-servinit-template.py | 164 +++++++++++++++++++++++++++ docker/serverip.patch | 20 ++++ 6 files changed, 295 insertions(+) create mode 100644 .dockerignore create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml.example create mode 100644 docker/generate-servinit-template.py create mode 100644 docker/serverip.patch diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3703f3942 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# remember: the last(!) line that matches a specific file defines the behavior + +# ignore everything, then unblock one by one (note: . does not work "for historical reasons") +* + +#!.git/ +!README.md +!CMakeLists.txt +!config/ +!data/ +!doc/ +!docker/ +!src/ + +# ignore the Dockerfile so changes in it don't retrigger an entire build +!docker/Dockerfile diff --git a/.gitignore b/.gitignore index bef1e2cc1..2b5de6515 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ out *.core *.diff *.patch +!docker/serverip.patch *.out home/ cache/ @@ -54,3 +55,4 @@ GTAGS GRTAGS .idea/ *build*/ +*.*swp* diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..a4136fd4f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,68 @@ +# first, let's build the software +# this is going to take the most time +FROM alpine:3.17 AS builder + +RUN apk add gcc g++ binutils sdl2-dev zlib-dev perl git wget ca-certificates coreutils mesa-dev musl-dev glu-dev \ + sdl2_image-dev sdl2_mixer-dev psmisc cmake ninja + +RUN mkdir /blue-nebula +WORKDIR /blue-nebula + +COPY src/ /blue-nebula/src +COPY config/ /blue-nebula/config +COPY data/ /blue-nebula/data + +COPY CMakeLists.txt /blue-nebula/ +COPY README.md /blue-nebula/ +COPY docker/serverip.patch /serverip.patch + +RUN git apply < /serverip.patch && \ + ls -al && \ + mkdir build && \ + cd build && \ + # TODO: server-only build configuration + cmake .. -DCMAKE_BUILD_TYPE=Release -G Ninja -DCMAKE_INSTALL_PREFIX=/blue-nebula/install && \ + ninja -v && \ + ninja install + +# next, we convert the example server config to a template +# this is done in no time, so we do not have to benefit from Docker's caching a lot there +FROM python:3.10-alpine AS template + +COPY docker/generate-servinit-template.py doc/examples/servinit.cfg / + +RUN python3 /generate-servinit-template.py /servinit.cfg > /servinit.tmpl + + + +# finally, let's build the final runtime image (doesn't need all of the build dependencies) +FROM alpine:3.17 + +MAINTAINER "TheAssassin " + +RUN apk add tini && \ + apk add --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing dockerize + +RUN adduser -S -D -h /blue-nebula blue-nebula +WORKDIR /blue-nebula + +COPY --from=template /servinit.tmpl / +COPY --from=builder /blue-nebula/install /blue-nebula/install + +# keep the directory read-only but allow writing to the file +# this is needed since dockerize runs as blue-nebula and needs to modify the file +# we might want to run dockerize separately and then run the process as blue-nebula +RUN mkdir /blue-nebula/.blue-nebula && \ + install -o blue-nebula -m 0644 /dev/null /blue-nebula/.blue-nebula/servinit.cfg + +# install runtime dependencies +RUN apk add --no-cache libstdc++ libgcc + +# keep files copied from elsewhere read-only to prevent the process from writing files +USER blue-nebula + +EXPOSE 28799/udp 28800/udp 28801/udp 28802/udp + +ENTRYPOINT ["/sbin/tini", "--"] + +CMD dockerize -template /servinit.tmpl:/blue-nebula/.blue-nebula/servinit.cfg install/bin/blue-nebula_server_linux diff --git a/docker/docker-compose.yml.example b/docker/docker-compose.yml.example new file mode 100644 index 000000000..785399dd9 --- /dev/null +++ b/docker/docker-compose.yml.example @@ -0,0 +1,25 @@ +version: "3" + +services: + bn-server: + image: ghcr.io/blue-nebula/server:latest + restart: unless-stopped + + # adjust accordingly if you change the ports in the config + # note: the ports must be in sync! otherwise, the server will announce the wrong ports to the master + ports: + - "0.0.0.0:28801:28801/udp" + - "0.0.0.0:28802:28802/udp" + + # most-if-not-all environment variables are exposed as upper-case environment variables as shown below + environment: + ADMIN_PASS: "so-secret" + SERVER_TYPE: 1 + SERVER_PORT: 28801 + SV_SERVERCLIENTS: 16 + SV_SERVERDESC: "Example BN server running in Docker" + SV_SERVERMOTD: "Welcome to this example Blue Nebula server hosted in a Docker container!" + + # to add additional maps, you can mount a directory as shown below + volumes: + - ./custom-maps:/blue-nebula/.blue-nebula/maps/ diff --git a/docker/generate-servinit-template.py b/docker/generate-servinit-template.py new file mode 100644 index 000000000..b5df1b79e --- /dev/null +++ b/docker/generate-servinit-template.py @@ -0,0 +1,164 @@ +#! /usr/bin/env python3 + +import re +import sys +import textwrap + + +class ParsingException(Exception): + pass + + +def convert_variable(variable_name: str, default_value: str, comment: str, indentation: int = None): + if indentation is None: + indentation = 0 + + variable_name_upper = variable_name.upper() + env_var_name = ".Env.%s" % variable_name_upper + + lines = [] + lines.append("{{ if (contains .Env \"%s\") -}}" % variable_name_upper) + #lines.append(comment) + #lines.append("// default/example value: %s" % default_value) + lines.append("%s \"{{ %s }}\"" % (variable_name, env_var_name)) + lines.append("{{ else -}}") + lines.append("// %s %s %s" % (variable_name, default_value, comment)) + lines.append("{{ end -}}") + lines.append("") + + return textwrap.indent("\n".join(lines), " "*indentation) + + +def parse_and_convert_variable(pattern: str, line: str, indentation: int = None): + match = re.match(pattern, line) + + if not match: + return + + return convert_variable(*match.groups(), indentation) + + +def parse_regular_variable(line: str): + return parse_and_convert_variable(r"^//\s?((?:sv_|admin|server)\w+)\s+(.+)\s+(\/\/.*)$", line) + + +def parse_rehashing_variable(line: str): + return parse_and_convert_variable(r"^ //\s*((?!irc)\w+)\s+(.+)\s+(\/\/.*)$", line, 4) + + +def parse_add_variable(line: str): + match = re.match(r"^//\s? (add\w+)\s+(.+)\s*(|\/\/.*)$", line) + + if not match: + return + + variable_name, default_value, comment = match.groups() + + variable_name_upper = variable_name.upper() + env_var_name = ".Env.%s" % variable_name_upper + + lines = [] + lines.append("{{ if (contains .Env \"%s\") -}}" % variable_name_upper) + #lines.append(comment) + #lines.append("// default/example value: %s" % default_value) + lines.append("{{ range $e := ( split %s \";\" ) -}}" % env_var_name) + lines.append("%s {{ $e }}" % variable_name) + lines.append("{{ end -}}") + lines.append("{{ else -}}") + lines.append(line) + lines.append("{{ end -}}") + lines.append("") + + return "\n".join(lines) + + +def parse_and_convert_line(line: str) -> str: + for f in [parse_regular_variable, parse_rehashing_variable, parse_add_variable]: + parsed = f(line) + + if parsed is not None: + return parsed + + else: + return line + + +def make_irc_section(): + return textwrap.dedent( + """ + {{ if (contains .Env "ENABLE_IRC") -}} + // special single-server IRC configuration, suitable for our Docker deployment + // setting ENABLE_IRC to some value will be sufficient in most cases + if (= $rehashing 0) [ + ircfilter {{ default .Env.IRC_FILTER "1" }} // defines the way the colour-to-irc filter works; 0 = off, "1" = convert, 2 = strip + + ircaddrelay ircrelay {{ default .Env.IRC_RELAY_HOSTNAME "localhost" }} {{ default .Env.IRC_RELAY_PORT "6667" }} {{ default .Env.IRC_RELAY_NICK "re-server" }} + + {{ if (contains .Env "IRC_BIND_ADDRESS") -}} + ircbind ircrelay {{ .Env.IRC_BIND_ADDRESS }} // use this only if you need to bind to a specific address, eg. multihomed machines + {{ end -}} + + {{ if (contains .Env "IRC_SERVER_PASS" ) -}} + ircpass ircrelay {{ .Env.IRC_SERVER_PASS }} // some networks can use the PASS field to identify to nickserv + {{ end -}} + + {{ if (contains .Env "IRC_CHANNELS") -}} + {{ range $e := ( split .Env.IRC_CHANNELS "," ) -}} + ircaddchan ircrelay "{{ $e }}" + ircrelaychan ircrelay "{{ $e }}" 3 + {{ end -}} + {{ end -}} + + ircconnect ircrelay // and tell it to connect! + ] + {{ end -}} + """ + ) + + +def make_additional_vars_section(): + text = textwrap.dedent( + """ + {{ if (contains .Env "ADDITIONAL_VARS") -}} + // additional variables + {{ range $e := (split .Env.ADDITIONAL_VARS ";") -}} + {{ $a := (split $e "=") -}} + {{ index $a 0 }} {{ index $a 1 }} + {{ end -}} + {{ end -}} + + {{ if (contains .Env "SV_DUELMAXQUEUED") -}} + sv_duelmaxqueued "{{ .Env.SV_DUELMAXQUEUED }}" + {{ end -}} + """ + ) + + for i in ["duelmaxqueued", "teamneutralcolour"]: + pass + text += textwrap.dedent( + """ + {{{{ if (contains .Env "SV_{upper}") -}}}} + sv_{lower} "{{{{ .Env.SV_{upper} }}}}" + {{{{ end -}}}} + """.format(lower=i, upper=i.upper()) + ) + + return text + + +def main(): + with open(sys.argv[1]) as f: + lines = f.read().splitlines() + + for line in lines: + print(parse_and_convert_line(line)) + + print(make_irc_section()) + print(make_additional_vars_section()) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except BrokenPipeError: + pass diff --git a/docker/serverip.patch b/docker/serverip.patch new file mode 100644 index 000000000..c06542d00 --- /dev/null +++ b/docker/serverip.patch @@ -0,0 +1,20 @@ +diff --git a/src/engine/server.cpp b/src/engine/server.cpp +index a8530e43..89e63f4f 100644 +--- a/src/engine/server.cpp ++++ b/src/engine/server.cpp +@@ -1329,6 +1329,7 @@ int setupserversockets() + if(!servertype || (serverhost && pongsock != ENET_SOCKET_NULL)) return servertype; + + ENetAddress address = { ENET_HOST_ANY, enet_uint16(serverport) }; ++#if 0 + if(*serverip) + { + if(enet_address_set_host(&address, serverip) < 0) +@@ -1338,6 +1339,7 @@ int setupserversockets() + } + else serveraddress.host = address.host; + } ++#endif + + if(!serverhost) + {