From 48d87f667dcf1fd78b46b6cb90bc07d3cc0a589a Mon Sep 17 00:00:00 2001 From: Corey Sotiropoulos Date: Sun, 13 Apr 2025 22:28:40 -0400 Subject: [PATCH] Add Alpine build - Add build args for user/group. - Lots of cleanup and adjustments for safety and efficiency. - Update LSB submodule ref. --- .dockerignore | 15 +++-- .gitignore | 3 +- Dockerfile | 62 ------------------ README.md | 23 +++++-- config/.gitkeep | 0 docker-compose.alpine.yml | 18 ++++++ docker-compose.yml | 22 +++---- docker/alpine.Dockerfile | 133 ++++++++++++++++++++++++++++++++++++++ docker/ubuntu.Dockerfile | 110 +++++++++++++++++++++++++++++++ entrypoint.sh | 23 ++++--- mariadb-entrypoint.sh | 14 ++-- server | 2 +- 12 files changed, 323 insertions(+), 102 deletions(-) delete mode 100644 Dockerfile create mode 100644 config/.gitkeep create mode 100644 docker-compose.alpine.yml create mode 100644 docker/alpine.Dockerfile create mode 100644 docker/ubuntu.Dockerfile diff --git a/.dockerignore b/.dockerignore index 74c5ccd..fe9b90a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ +# Nothing tracked by LSB git should be in here. +# Keeps git hash clean during build. server/build -server/documentation -server/log -server/losmeshes -server/navmeshes -server/scripts -server/settings -server/sql +server/log/*.log +server/losmeshes/** +server/navmeshes/** +server/settings/*.lua +server/sql/backups/*.sql +server/tools/config.yaml diff --git a/.gitignore b/.gitignore index 30fa1ce..3c317d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -config \ No newline at end of file +config/** +!.gitkeep diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 638f3a9..0000000 --- a/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -############## -# Base stage # -############## -FROM ubuntu:latest AS base -ENV DEBIAN_FRONTEND=noninteractive -RUN rm -f /etc/apt/apt.conf.d/docker-clean && \ - echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ - > /etc/apt/apt.conf.d/keep-cache - -############### -# Build stage # -############### -FROM base AS build - -# Install build exclusive dependencies. -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt update && apt install -yqq --no-install-recommends \ - binutils-dev build-essential cmake git python3 python3-dev python3-pip \ - software-properties-common libluajit-5.1-dev libmariadb-dev-compat \ - libssl-dev libzmq3-dev zlib1g-dev - -# Install Python dependencies as user to facilitate copying to runtime image. -RUN --mount=type=bind,source=./server/tools/requirements.txt,target=/tmp/requirements.txt \ - --mount=type=cache,target=/root/.cache/pip \ - pip3 install --break-system-packages --user --requirement /tmp/requirements.txt - -# LSB requires Git for the build, and emits version.cpp along with the executables. -# The bind mounted server directory does not persist changes, so copy these from/to -# the cached build folder, preserving timestamps to avoid unnecessary rebuilds. -# Then make a copy of the executables that can be passed to the runtime stage. -RUN --mount=type=cache,target=/build \ - --mount=type=bind,source=./.git,target=/.git \ - --mount=type=bind,source=./server,target=/server,rw \ - cp -p /build/version.cpp /server/src/common 2> /dev/null || true && \ - cp -p /build/xi_* /server 2> /dev/null || true && \ - cmake -S /server -B /build -DCMAKE_BUILD_TYPE=Release && \ - cmake --build /build -j$(nproc) && \ - cp -p /server/src/common/version.cpp /build && \ - cp -p /server/xi_* /build && \ - cp /server/xi_* /root - -################# -# Runtime stage # -################# -FROM base AS service - -# Install runtime dependencies. -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt update && apt install -yqq --no-install-recommends \ - binutils git libluajit-5.1-2 libssl3t64 libzmq5 mariadb-client \ - python3 zlib1g \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /server -RUN git config --global --add safe.directory /server - -# Copy installed Python dependencies and built executables from build stage. -COPY --from=build /root/.local /root/.local -COPY --from=build /root/xi_* /server -COPY entrypoint.sh /entrypoint.sh diff --git a/README.md b/README.md index e8cfb71..fcbf73b 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ That's it! All the setup is handled for you. Check out the [official LSB documen - Use these to stop/start the server when rebuilding isn't necessary. - `docker compose build` - - Builds the image. With caching, this basically means rebuild the executables. + - Builds the image. With caching, this basically just rebuilds the executables if needed. Use `--no-cache` for a full clean build. - `docker compose down` - - Shuts down and deletes the containers. + - Shuts down and removes the containers. ---- @@ -30,10 +30,21 @@ That's it! All the setup is handled for you. Check out the [official LSB documen - `git submodule update --remote --merge` - This will pull the latest changes from upstream LSB. Run `docker compose up --build --detach` again to rebuild and start/restart the containers. + - You can also just change to the server directory and treat it as its own repo (change branch, fork, apply patches, etc.) + +## Alpine + +Alpine Linux is a minimal image that comes in at about half the size of the Ubuntu image (excluding bind mounted files/directories). The Alpine build is experimental and unsupported by LSB. It uses a newer version of GCC and the [musl libc](https://wiki.musl-libc.org/functional-differences-from-glibc.html). Use at your own risk. + +`docker compose -f ./docker-compose.yml -f ./docker-compose.alpine.yml up --build --detach` ## Notes +- The image is built with a default user and group named `xiadmin` with UID and GID `1000`. + - You can supply the build args `UNAME`, `UGROUP`, `UID`, `GID` to change these. + - `docker compose build --build-arg UID="$(id -u)" --build-arg GID="$(id -g)" --build-arg UNAME="$(whoami)"` - If using WSL with Windows, make sure [the project is stored in the WSL file system](https://learn.microsoft.com/en-us/windows/wsl/filesystems#file-storage-and-performance-across-file-systems) for best performance. + - You may experience issues related to the system clock. Using `sudo hwclock --hctosys` in WSL may help. - Secure database passwords and a default user and database name are generated into `config/.env` to simplify the setup process. - **Optional** - You can manually create this _before_ building the containers with these variables: ``` @@ -41,12 +52,10 @@ That's it! All the setup is handled for you. Check out the [official LSB documen MARIADB_USER= MARIADB_PASSWORD= MARIADB_ROOT_PASSWORD= - XI_NETWORK_ENABLE_HTTP= ``` - Otherwise, the default user is `xiadmin`, the default database is `xidb`, and both passwords are randomly generated. - - The LSB HTTP API is enabled by default and can be accessed at http://localhost:8088/. - - This can be disabled by setting `XI_NETWORK_ENABLE_HTTP=0` - - Because environment variables are used, the server settings `SQL_LOGIN`, `SQL_PASSWORD`, `SQL_DATABASE`, and `ENABLE_HTTP` (located in network.lua) are not used. + Otherwise, the default user is `xiadmin`, the default database is `xidb`, and the password is randomly generated. + - If `config/.env` is automatically generated, a random initial password for the root user will be generated and printed to stdout (check your build log!) + - Because environment variables are used, the server settings `SQL_LOGIN`, `SQL_PASSWORD`, and `SQL_DATABASE` (all located in network.lua) are not used. - Because LSB uses Git during the build and update process, changes to the Git metadata will trigger an image rebuild, but with caching this shouldn't be significant. ---- diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.alpine.yml b/docker-compose.alpine.yml new file mode 100644 index 0000000..790db21 --- /dev/null +++ b/docker-compose.alpine.yml @@ -0,0 +1,18 @@ +services: + dbtool: + image: lsd_service:alpine + build: + context: . + dockerfile: docker/alpine.Dockerfile + + connect: + image: lsd_service:alpine + + search: + image: lsd_service:alpine + + world: + image: lsd_service:alpine + + map: + image: lsd_service:alpine diff --git a/docker-compose.yml b/docker-compose.yml index a24f8b4..04ce21e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,18 @@ x-common: &common # Configs used by more than one service. - image: lsd_service + image: lsd_service:ubuntu pull_policy: never - entrypoint: ["/entrypoint.sh"] # Copies the MariaDB env vars into corresponding LSB settings. environment: XI_NETWORK_HTTP_HOST: 0.0.0.0 XI_NETWORK_ZMQ_IP: world XI_NETWORK_SQL_HOST: database - volumes: # YAML merge can't be used with sequences, so bind everything here. (SEE NOTE ABOUT WSL IN README!) + volumes: + - ./.git:/.git - ./config:/config - - ./.git:/.git # Since LSB is a submodule, we want to preserve - - ./server/.git:/server/.git # the directory structure inside the container. + - ./server/.git:/server/.git - ./server/log:/server/log - ./server/losmeshes:/server/losmeshes - ./server/modules:/server/modules - ./server/navmeshes:/server/navmeshes - - ./server/res:/server/res - ./server/scripts:/server/scripts - ./server/settings:/server/settings - ./server/sql:/server/sql @@ -24,22 +22,24 @@ services: database: image: mariadb entrypoint: ["/mariadb-entrypoint.sh"] + command: ["mariadbd"] restart: always volumes: - ./config:/config - ./mariadb-entrypoint.sh:/mariadb-entrypoint.sh # Custom entrypoint to generate .env. - - ./server/sql:/docker-entrypoint-initdb.d # Initializes the database. - database:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - start_period: 3m # First time initialization takes a while, so give it extra time. - interval: 15s # TODO: use dbtool setup, set lower healthcheck. + start_period: 10s + interval: 10s timeout: 5s - retries: 4 + retries: 3 dbtool: <<: *common # Import common configs. - build: . # This service builds the image shared by all LSB services. + build: + context: . + dockerfile: docker/ubuntu.Dockerfile # This service builds the image shared by all LSB services. command: ["python3", "/server/tools/dbtool.py", "update"] restart: no depends_on: diff --git a/docker/alpine.Dockerfile b/docker/alpine.Dockerfile new file mode 100644 index 0000000..3334d8f --- /dev/null +++ b/docker/alpine.Dockerfile @@ -0,0 +1,133 @@ +# syntax=docker/dockerfile:1-labs + +############## +# Base stage # +############## +FROM alpine:latest AS base + +ARG UNAME=xiadmin +ARG UGROUP=xiadmin +ARG UID=1000 +ARG GID=1000 + +RUN addgroup --gid $GID $UGROUP && \ + adduser --uid $UID $UNAME --ingroup $UGROUP --home /xiadmin --disabled-password + +# Install runtime dependencies at the base level. +RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ + apk --update-cache add \ + binutils \ + git \ + luajit \ + mariadb-client \ + mariadb-connector-c \ + openssl \ + python3 \ + zeromq \ + zlib + +RUN git config --system --add safe.directory /server +ENV PATH=/xiadmin/.local/bin:$PATH + +############### +# Build stage # +############### +FROM base AS build + +# Install build dependencies. +RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ + apk --update-cache add \ + binutils-dev \ + ccache \ + cmake \ + g++ \ + linux-headers \ + luajit-dev \ + make \ + mariadb-dev \ + openssl-dev \ + python3-dev \ + py3-pip \ + zeromq-dev \ + zlib-dev + +USER $UNAME +WORKDIR /server + +# Install Python dependencies here, copied into runtime stage. +RUN --mount=type=bind,source=server/tools/requirements.txt,target=/tmp/requirements.txt \ + --mount=type=cache,target=/xiadmin/.cache/pip,uid=$UID,gid=$GID \ + pip3 install --break-system-packages --user --ignore-installed --requirement /tmp/requirements.txt + +# Exclude changes to git metadata, scripts, and sql not needed for build. +# Excluded here instead of dockerignore so they can be bind mounted during build. +# Saves from copying everything whenever scripts/sql change. +# https://docs.docker.com/reference/dockerfile/#copy---exclude (docker/dockerfile:1.7-labs) +COPY --chown=$UNAME:$UGROUP --exclude=.git --exclude=scripts --exclude=sql server /server + +# --- PATCH LSB --- +RUN LSB_FILE="/server/cmake/FindMariaDBCPP.cmake" && \ + if [ -f "$LSB_FILE" ]; then \ + # Check if replacement is needed. + if grep -qF 'b09555de99ed4b1d054a88ff85acbae996bce1d1' "$LSB_FILE"; then \ + echo "Patching $LSB_FILE: Update MariaDB Connector/C++ to 1.0.5 (fixes compilation on Alpine)"; \ + sed -i 's/b09555de99ed4b1d054a88ff85acbae996bce1d1/a36ff95ac6a6236a2faaaa6ec710219c8aabe35d/g' "$LSB_FILE"; \ + fi; \ + else \ + echo "Warning: $LSB_FILE not found, skipping patch."; \ + fi; +# --- End Patch --- + +# Cache the build. Bind mounts to save copy time and keep clean git hash. +ENV CCACHE_DIR=/xiadmin/.ccache +RUN --mount=type=cache,target=/xiadmin/build,uid=$UID,gid=$GID \ + --mount=type=cache,target=/xiadmin/.ccache,uid=$UID,gid=$GID \ + --mount=type=bind,source=.git,target=/.git \ + --mount=type=bind,source=server/.git,target=/server/.git \ + --mount=type=bind,source=server/scripts,target=/server/scripts \ + --mount=type=bind,source=server/sql,target=/server/sql \ + # --- CACHE --- + cp -p /xiadmin/build/version.cpp /server/src/common/ 2> /dev/null; \ + cp -p /xiadmin/build/xi_* /server/ 2> /dev/null; \ + # --- End --- + cmake -S /server -B /xiadmin/build -DCMAKE_BUILD_TYPE=Release && \ + # --- PATCH efsw --- + EFSW_FILE="/xiadmin/build/_deps/efsw-src/src/efsw/FileWatcherInotify.cpp"; \ + if [ -f "$EFSW_FILE" ]; then \ + # Check if include is missing. + if ! grep -qF '#include ' "$EFSW_FILE"; then \ + echo "Patching $EFSW_FILE: Adding #include "; \ + sed -i '1i #include ' "$EFSW_FILE"; \ + fi; \ + # Check if replacement is needed. + if grep -qF 'u_int32_t' "$EFSW_FILE"; then \ + echo "Patching $EFSW_FILE: Replacing u_int32_t with uint32_t"; \ + sed -i 's/u_int32_t/uint32_t/g' "$EFSW_FILE"; \ + fi; \ + else \ + echo "Warning: $EFSW_SRC_FILE not found, skipping patch."; \ + fi; \ + # --- End Patch --- + cmake --build /xiadmin/build -j$(nproc) && \ + # --- CACHE --- + # Alpine seems to always re-link the executables. + cp -p /server/xi_* /xiadmin/build/; \ + cp -p /server/src/common/version.cpp /xiadmin/build/; + # --- End --- + +################# +# Runtime stage # +################# +FROM base AS service + +USER $UNAME +WORKDIR /server + +COPY server/res/compress.dat server/res/decompress.dat /server/res/ + +# Copy installed Python dependencies and built executables from build stage. +COPY --from=build /xiadmin/.local /xiadmin/.local +COPY --from=build /server/xi_* /server/ + +COPY --chmod=0755 entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/ubuntu.Dockerfile b/docker/ubuntu.Dockerfile new file mode 100644 index 0000000..6f8dbc6 --- /dev/null +++ b/docker/ubuntu.Dockerfile @@ -0,0 +1,110 @@ +# syntax=docker/dockerfile:1-labs + +############## +# Base stage # +############## +FROM ubuntu:latest AS base + +ARG UNAME=xiadmin +ARG UGROUP=xiadmin +ARG UID=1000 +ARG GID=1000 + +ARG DEBIAN_FRONTEND=noninteractive +RUN rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ + > /etc/apt/apt.conf.d/keep-cache + +RUN userdel --remove ubuntu && \ + groupadd --gid $GID $UNAME && \ + useradd --uid $UID $UNAME --gid $UGROUP --home-dir /xiadmin --create-home --skel /dev/null + +# Install runtime dependencies at the base level. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install --assume-yes --no-install-recommends --quiet \ + binutils \ + git \ + libluajit-5.1-2 \ + libssl3t64 \ + libzmq5 \ + mariadb-client \ + python3 \ + zlib1g + +RUN git config --system --add safe.directory /server +ENV PATH=/xiadmin/.local/bin:$PATH + +############### +# Build stage # +############### +FROM base AS build + +# Install build dependencies. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install --assume-yes --no-install-recommends --quiet \ + binutils-dev \ + build-essential \ + ccache \ + cmake \ + g++ \ + git \ + libluajit-5.1-dev \ + libmariadb-dev-compat \ + libssl-dev \ + libzmq3-dev \ + make \ + python3-dev \ + python3-pip \ + zlib1g-dev + +USER $UNAME +WORKDIR /server + +# Install Python dependencies here, copied into runtime stage. +RUN --mount=type=bind,source=server/tools/requirements.txt,target=/tmp/requirements.txt \ + --mount=type=cache,target=/xiadmin/.cache/pip,uid=$UID,gid=$GID \ + pip3 install --break-system-packages --user --ignore-installed --requirement /tmp/requirements.txt + +# Exclude changes to git metadata, scripts, and sql not needed for build. +# Excluded here instead of dockerignore so they can be bind mounted during build. +# Saves from copying everything whenever scripts/sql change. +# https://docs.docker.com/reference/dockerfile/#copy---exclude (docker/dockerfile:1.7-labs) +COPY --chown=$UNAME:$UGROUP --exclude=.git --exclude=scripts --exclude=sql server /server + +# Cache the build. Bind mounts to save copy time and keep clean git hash. +ENV CCACHE_DIR=/xiadmin/.ccache +RUN --mount=type=cache,target=/xiadmin/build,uid=$UID,gid=$GID \ + --mount=type=cache,target=/xiadmin/.ccache,uid=$UID,gid=$GID \ + --mount=type=bind,source=.git,target=/.git \ + --mount=type=bind,source=server/.git,target=/server/.git \ + --mount=type=bind,source=server/scripts,target=/server/scripts \ + --mount=type=bind,source=server/sql,target=/server/sql \ + # --- CACHE --- + cp -p /xiadmin/build/version.cpp /server/src/common/ 2> /dev/null; \ + cp -p /xiadmin/build/xi_* /server/ 2> /dev/null; \ + # --- End --- + cmake -S /server -B /xiadmin/build -DCMAKE_BUILD_TYPE=Release && \ + cmake --build /xiadmin/build -j$(nproc) && \ + # --- CACHE --- + cp -p /server/xi_* /xiadmin/build/; \ + cp -p /server/src/common/version.cpp /xiadmin/build/; + # --- End --- + +################# +# Runtime stage # +################# +FROM base AS service + +USER $UNAME +WORKDIR /server + +COPY server/res/compress.dat server/res/decompress.dat /server/res/ + +# Copy installed Python dependencies and built executables from build stage. +COPY --from=build /xiadmin/.local /xiadmin/.local +COPY --from=build /server/xi_* /server/ + +COPY --chmod=0755 entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index 6f7bfd4..d5851fa 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,11 +1,18 @@ -#!/bin/bash +#!/bin/sh +set -e -# Export XI_ variables from corresponding MARIADB_ variables. -set -a -source "/config/.env" -set +a -export XI_NETWORK_SQL_DATABASE=$MARIADB_DATABASE -export XI_NETWORK_SQL_LOGIN=$MARIADB_USER -export XI_NETWORK_SQL_PASSWORD="$MARIADB_PASSWORD" +echo "Loading configuration from /config/.env..." +if [ -f "/config/.env" ]; then + set -a + . "/config/.env" + set +a + unset MARIADB_ROOT_PASSWORD +else + echo "Warning: /config/.env not found." +fi + +export XI_NETWORK_SQL_PASSWORD="$MARIADB_PASSWORD" && unset MARIADB_PASSWORD +export XI_NETWORK_SQL_LOGIN="$MARIADB_USER" && unset MARIADB_USER +export XI_NETWORK_SQL_DATABASE="$MARIADB_DATABASE" && unset MARIADB_DATABASE exec "$@" diff --git a/mariadb-entrypoint.sh b/mariadb-entrypoint.sh index 015fdd9..7e927d4 100755 --- a/mariadb-entrypoint.sh +++ b/mariadb-entrypoint.sh @@ -1,34 +1,38 @@ #!/bin/bash +set -e ENV_FILE="/config/.env" +echo "Running custom entrypoint..." + # Set default values if the .env file doesn't exist. if [ ! -e "$ENV_FILE" ]; then + echo "$ENV_FILE not found, generating one..." MARIADB_DATABASE=xidb MARIADB_USER=xiadmin - XI_NETWORK_ENABLE_HTTP=1 # Generate random passwords. (MARIADB_RANDOM_ROOT_PASSWORD) # https://github.com/MariaDB/mariadb-docker/blob/87a043a031e8c56ba66a0ad06e633417ae75ee1e/docker-entrypoint.sh#L378-L382 MARIADB_PASSWORD=$(pwgen --numerals --capitalize --symbols --remove-chars="'\\" -1 32) - MARIADB_ROOT_PASSWORD=$(pwgen --numerals --capitalize --symbols --remove-chars="'\\" -1 32) + export MARIADB_RANDOM_ROOT_PASSWORD=1 # Write the variables to the .env file. cat <<-EOF > "$ENV_FILE" MARIADB_DATABASE=$MARIADB_DATABASE MARIADB_USER=$MARIADB_USER MARIADB_PASSWORD='$MARIADB_PASSWORD' - MARIADB_ROOT_PASSWORD='$MARIADB_ROOT_PASSWORD' - XI_NETWORK_ENABLE_HTTP=$XI_NETWORK_ENABLE_HTTP # Enables the LSB HTTP API EOF + chown mysql:mysql "$ENV_FILE" fi # Load environment variables. if [ -f "$ENV_FILE" ]; then + echo "Loading environment vairables from $ENV_FILE..." set -a # Automatically export all sourced variables. source "$ENV_FILE" set +a fi # Execute the original MariaDB entrypoint. -exec /usr/local/bin/docker-entrypoint.sh mariadbd "$@" +echo "Handing over to official MariaDB entrypoint..." +exec /usr/local/bin/docker-entrypoint.sh "$@" diff --git a/server b/server index 0985720..f283f72 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 0985720f78dfc8cfc5b93b55e2ea672baba4cd26 +Subproject commit f283f72fdbf83f718b250032411a0bb0430ec775