# syntax=docker/dockerfile:1 # # One image, one match. # # Downloads use BuildKit cache mounts, all sharing /tmp/deps. A cache mount is # not a layer, so each archive is fetched into the cache, checksummed, and # extracted in the same RUN; only the extracted tree becomes part of the image. # The cache survives between builds and between branches, so the ~1.1GB of # third-party artifacts is fetched once on a machine rather than once per build. # # apt uses the same mechanism: /var/cache/apt and /var/lib/apt are cache mounts, # and docker-clean is removed so apt keeps its .debs instead of deleting them. # Because neither directory is a layer, there is no need to rm the lists # afterwards - doing so would only empty the cache. # # sharing=locked because the stages below run in parallel and draw from the same # pool. Clear either with: # docker builder prune -f --filter type=exec.cachemount (downloads + apt) # docker builder prune -af (all build cache) # # Stages are per component - jre, megamek, suramadu, arena - so a failure names # the component, and the three that do not depend on each other build in # parallel. # # Requires BuildKit, which is the default in Docker 23+. ./scripts/build.sh sets # DOCKER_BUILDKIT=1 regardless. # # Licensing: this image redistributes MegaMek (GPL-3.0 code, CC BY-NC-SA 4.0 # data), Suramadu (AGPL-3.0) and a Temurin-derived runtime (GPLv2+CPE). Our # patches ship as source in /opt/arena/patches, and the licence texts travel with # the bundle. See LICENSING.md. ARG JDK_VERSION ARG JDK_URL_TAG ARG JDK_ARCHIVE ARG JDK_SHA256 ARG JDK_DIR ARG JRE_MODULES ARG MEGAMEK_VERSION ARG MEGAMEK_RELEASE_DIR ARG MEGAMEK_SRC_DIR ARG SURAMADU_VERSION ARG SURAMADU_WAR # Defaults, not blanks: scripts/build.sh does not pass these yet, and an empty # ARG would fetch a URL ending in a slash. The same four values live in # versions.env, where every other pin does; tests/shell/test-profiling.sh fails # if they drift apart. ARG ASYNC_PROFILER_VERSION=4.5 ARG ASYNC_PROFILER_ARCHIVE=async-profiler-4.5-linux-x64.tar.gz ARG ASYNC_PROFILER_SHA256=89546fbb9ee0fc5496c7edd4099b0709489bc78b0d8057ccbb4b801f6b032b62 ARG ASYNC_PROFILER_DIR=async-profiler-4.5-linux-x64 # --------------------------------------------------------------------------- # base: the tools every build stage needs # --------------------------------------------------------------------------- FROM debian:bookworm-slim AS base RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl tar unzip patch coreutils SHELL ["/bin/bash", "-o", "pipefail", "-c"] # --------------------------------------------------------------------------- # jre: Temurin JDK, then a jlink runtime # --------------------------------------------------------------------------- FROM base AS jre ARG JDK_URL_TAG ARG JDK_ARCHIVE ARG JDK_SHA256 ARG JDK_DIR ARG JRE_MODULES # Downloaded to .part and only renamed into place once it verifies, so an # interrupted curl can never leave a file that later builds treat as cached. A # cached file that fails its check is deleted and refetched rather than failing # forever - the cache has to be able to heal itself. RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ f="/tmp/deps/${JDK_ARCHIVE}"; \ if [ -f "$f" ] && ! echo "${JDK_SHA256} $f" | sha256sum -c - >/dev/null 2>&1; then \ echo "cached ${JDK_ARCHIVE} failed its checksum; refetching"; rm -f "$f"; \ fi; \ if [ ! -f "$f" ]; then \ echo "fetching ${JDK_ARCHIVE}"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$f.part" \ "https://github.com/adoptium/temurin21-binaries/releases/download/${JDK_URL_TAG}/${JDK_ARCHIVE}"; \ echo "${JDK_SHA256} $f.part" | sha256sum -c -; \ mv "$f.part" "$f"; \ else echo "using cached ${JDK_ARCHIVE}"; fi; \ mkdir -p /opt && tar xzf "$f" -C /opt; \ mv "/opt/${JDK_DIR}" /opt/jdk # --strip-debug shells out to objcopy to strip native debug symbols from the # JDK's .so files, and bookworm-slim has no binutils. Installed here rather than # in base because only this stage needs it, and it never reaches the runtime # image - only /opt/jre is copied forward. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update && apt-get install -y --no-install-recommends \ binutils # 59MB against 346MB for the full JDK, which is most of the image. If something # dies with NoClassDefFoundError on a java.* or jdk.* class, add the module to # JRE_MODULES in versions.env rather than shipping the whole JDK. # # The rm is not paranoia: jlink refuses to write into an existing directory and # reports it with the same exit code as an unresolvable module, so without it the # two failures look identical. RUN set -eu; \ : "${JRE_MODULES:?empty - pass it as a build arg, or use ./scripts/build.sh}"; \ echo "jlink modules: ${JRE_MODULES}"; \ rm -rf /opt/jre; \ /opt/jdk/bin/jlink \ --add-modules "${JRE_MODULES}" \ --strip-debug --no-man-pages --no-header-files --compress=zip-6 \ --output /opt/jre; \ /opt/jre/bin/java -version # --------------------------------------------------------------------------- # megamek: release + source, patched jar, primed units.cache # --------------------------------------------------------------------------- FROM base AS megamek ARG MEGAMEK_VERSION ARG MEGAMEK_RELEASE_DIR ARG MEGAMEK_SRC_DIR ARG MEGAMEK_RELEASE_SHA256 ARG MEGAMEK_SRC_SHA256 COPY --from=jre /opt/jdk /opt/jdk ENV JAVA_HOME=/opt/jdk ENV MM_HOME=/opt/megamek ENV MM_SRC=/opt/megamek-src # Both tarballs: the release supplies the jar and data/, the source is what the # patchset applies against. # # Upstream publishes no checksum for either, so the digests in versions.env # come from a verified fetch, and are enforced here the way the JDK's is: a # cached file that fails is deleted and refetched, a fresh one that fails # fails the build naming the mismatch. RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ fetch_tgz() { \ f="$1"; url="$2"; sha="$3"; \ if [ -f "$f" ] && ! echo "$sha $f" | sha256sum -c - >/dev/null 2>&1; then \ echo "cached $(basename "$f") failed its checksum; refetching"; rm -f "$f"; \ fi; \ if [ ! -f "$f" ]; then \ echo "fetching $(basename "$f")"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$f.part" "$url"; \ echo "$sha $f.part" | sha256sum -c -; \ mv "$f.part" "$f"; \ else echo "using cached $(basename "$f")"; fi; \ }; \ rel="/tmp/deps/MegaMek-${MEGAMEK_VERSION}.tar.gz"; \ src="/tmp/deps/megamek-src-${MEGAMEK_VERSION}.tar.gz"; \ fetch_tgz "$rel" "https://github.com/MegaMek/megamek/releases/download/v${MEGAMEK_VERSION}/MegaMek-${MEGAMEK_VERSION}.tar.gz" "${MEGAMEK_RELEASE_SHA256}"; \ fetch_tgz "$src" "https://github.com/MegaMek/megamek/archive/refs/tags/v${MEGAMEK_VERSION}.tar.gz" "${MEGAMEK_SRC_SHA256}"; \ mkdir -p /opt/extract; \ tar xzf "$rel" -C /opt/extract; \ tar xzf "$src" -C /opt/extract; \ mv "/opt/extract/${MEGAMEK_RELEASE_DIR}" "$MM_HOME"; \ mv "/opt/extract/${MEGAMEK_SRC_DIR}" "$MM_SRC"; \ rm -rf /opt/extract COPY megamek/ /src/megamek/ # A patch that no longer applies fails the build here rather than at runtime, # which is the signal that upstream moved under us. verify-patches.sh then # confirms the jar differs from stock in exactly those classes. RUN OUT_JAR=/opt/MegaMek-patched.jar /src/megamek/apply-patches.sh \ && PATCHED_JAR=/opt/MegaMek-patched.jar /src/megamek/verify-patches.sh RUN /src/megamek/prime-units-cache.sh # The lance.blue skin: the committed mmconf/skins/lanceBlueSkin.xml (derived # once from 0.51.0's "BW - Default.xml", the skin GUIPreferences defaults to) # plus its two generated backgrounds, and the window icons become the # lance.blue insignia. The script verifies every image the skin references # exists in the assembled tree, so a MegaMek bump that moves the Bloodwolf # border art fails the build here, naming the missing paths. # clientsettings.xml.template selects the skin. RUN /src/megamek/skin/install-skin.sh # Data no match can reach, removed here rather than in the runtime stage: this # tree is what `COPY --from=megamek` writes into the image, so what goes now is # never written into a layer. A delete in a later layer is a whiteout over bytes # that are still there and still pulled. See megamek/prune-data.sh - it is one # documented line per path, and it fails the build if a MegaMek bump moves one. # It runs after install-skin.sh so the skin's reference check sees the full tree. # The render benchmarks. Not scenarios anyone plays: they hold everything still # except unit count and board area, so two measurement runs differ only in the # thing being measured. Installed under their own directory so a MegaMek bump # cannot collide with them, and so `ls data/scenarios` still reads as upstream's # library plus ours. RUN mkdir -p "$MM_HOME/data/scenarios/lance-blue" \ && cp /src/megamek/scenarios/*.mms "$MM_HOME/data/scenarios/lance-blue/" RUN /src/megamek/prune-data.sh # The source tree and the platform launchers are build-time only. RUN rm -rf "$MM_SRC" \ "$MM_HOME"/logs/* "$MM_HOME"/savegames/* \ "$MM_HOME"/MegaMek.exe "$MM_HOME"/MegaMek.l4j.ini # --------------------------------------------------------------------------- # suramadu: the AGPL-3.0 fork by manticore-projects of Webswing v20.2.5 # --------------------------------------------------------------------------- FROM base AS suramadu ARG SURAMADU_VERSION ARG SURAMADU_SHA256 # The distribution ships no LICENSE file. We redistribute it, so the AGPL text # has to travel with it, and a failure to fetch that text fails the build. # The zip is pinned like every other archive; see versions.env. RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ zip="/tmp/deps/suramadu-${SURAMADU_VERSION}.zip"; \ if [ -f "$zip" ] && ! echo "${SURAMADU_SHA256} $zip" | sha256sum -c - >/dev/null 2>&1; then \ echo "cached suramadu zip failed its checksum; refetching"; rm -f "$zip"; \ fi; \ if [ ! -f "$zip" ]; then \ echo "fetching suramadu-${SURAMADU_VERSION}.zip"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$zip.part" \ "https://github.com/manticore-projects/suramadu/releases/download/${SURAMADU_VERSION}/suramadu-${SURAMADU_VERSION}.zip"; \ echo "${SURAMADU_SHA256} $zip.part" | sha256sum -c -; \ mv "$zip.part" "$zip"; \ else echo "using cached suramadu-${SURAMADU_VERSION}.zip"; fi; \ mkdir -p /opt/suramadu; \ unzip -q "$zip" -d /opt/suramadu; \ curl -fsSL -o /opt/suramadu/LICENSE.txt https://www.gnu.org/licenses/agpl-3.0.txt; \ rm -rf /opt/suramadu/logs/* /opt/suramadu/tmp/* # web/index.html is a copy of the one in the war. Checked here, where the war # actually is, so a Suramadu bump that renames a script or tightens the CSP # fails the build instead of serving a page that loads nothing. The whole # directory is copied because the check also wants the files ours adds on top # of upstream's - theme.css and perf.js - to be next to it. ARG SURAMADU_WAR COPY suramadu/web/ /src/suramadu/web/ COPY suramadu/verify-web.sh /src/suramadu/verify-web.sh RUN WAR="/opt/suramadu/${SURAMADU_WAR}" /src/suramadu/verify-web.sh # zip is needed to write the patched bundle back into the war, brotli to # regenerate the precompressed copy the browser actually gets. Installed here # rather than in base because only this stage needs them, and they never reach # the runtime image. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update && apt-get install -y --no-install-recommends \ zip brotli # The stock frontend parks keydown events behind a 100ms timer and only flushes # the input queue (including coalesced mousemove/wheel) on a 100ms interval, so # keyboard input lags by up to ~200ms and hover feedback by up to ~100ms. # patch-war.sh applies suramadu/patches/ to the bundle # inside the war - a patch that stops applying fails the build here, the same # signal as the MegaMek patchset - and verify-embed.sh then proves the served # bundle (and its precompressed copies) actually carry the change. COPY suramadu/patches/ /src/suramadu/patches/ COPY suramadu/patch-war.sh suramadu/verify-embed.sh /src/suramadu/ RUN WAR="/opt/suramadu/${SURAMADU_WAR}" /src/suramadu/patch-war.sh \ && WAR="/opt/suramadu/${SURAMADU_WAR}" /src/suramadu/verify-embed.sh # The war's bundle gets its .br and .gz back above; our webFolder never had # any, and Suramadu's resource handler serves a precompressed sibling only when # one is already next to the file - it compresses nothing on the fly. Done in # this stage because gzip and brotli are already here for patch-war.sh and # neither belongs in the shipped image. COPY suramadu/precompress-web.sh /src/suramadu/ RUN DIR=/src/suramadu/web /src/suramadu/precompress-web.sh # --------------------------------------------------------------------------- # profiler: async-profiler, staged but never loaded unless ARENA_PROFILE is set # --------------------------------------------------------------------------- # Same .part-then-verify dance as the JDK above, for the same reason. Upstream # publishes a sha256 per release asset, so this one is pinned properly. # # Only two files are kept. libasyncProfiler.so is the agent the JVMs load at # startup through -agentpath, which is why nothing here has to attach: attaching # wants a writable /tmp and a JVM whose cwd it can drop a handshake file into, # and both JVMs run out of the read-only image tree. asprof is the launcher for # the case where someone does want to attach to a match already in flight; it # costs 75KB. jfrconv (141KB) and include/ are dropped - we dump collapsed # stacks, never JFR, and nothing here compiles against the C API. FROM base AS profiler ARG ASYNC_PROFILER_VERSION ARG ASYNC_PROFILER_ARCHIVE ARG ASYNC_PROFILER_SHA256 ARG ASYNC_PROFILER_DIR RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ f="/tmp/deps/${ASYNC_PROFILER_ARCHIVE}"; \ if [ -f "$f" ] && ! echo "${ASYNC_PROFILER_SHA256} $f" | sha256sum -c - >/dev/null 2>&1; then \ echo "cached ${ASYNC_PROFILER_ARCHIVE} failed its checksum; refetching"; rm -f "$f"; \ fi; \ if [ ! -f "$f" ]; then \ echo "fetching ${ASYNC_PROFILER_ARCHIVE}"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$f.part" \ "https://github.com/async-profiler/async-profiler/releases/download/v${ASYNC_PROFILER_VERSION}/${ASYNC_PROFILER_ARCHIVE}"; \ echo "${ASYNC_PROFILER_SHA256} $f.part" | sha256sum -c -; \ mv "$f.part" "$f"; \ else echo "using cached ${ASYNC_PROFILER_ARCHIVE}"; fi; \ mkdir -p /opt/extract /opt/async-profiler/lib /opt/async-profiler/bin; \ tar xzf "$f" -C /opt/extract; \ d="/opt/extract/${ASYNC_PROFILER_DIR}"; \ cp "$d/lib/libasyncProfiler.so" /opt/async-profiler/lib/; \ cp "$d/bin/asprof" /opt/async-profiler/bin/; \ cp "$d/LICENSE" /opt/async-profiler/LICENSE; \ rm -rf /opt/extract # --------------------------------------------------------------------------- # arena: our own code # --------------------------------------------------------------------------- FROM base AS arena COPY --from=jre /opt/jdk /opt/jdk COPY --from=megamek /opt/megamek /opt/megamek COPY src/ /src/src/ RUN JAVA_HOME=/opt/jdk MM_HOME=/opt/megamek OUT_JAR=/opt/arena.jar \ /src/src/build-jar.sh # --------------------------------------------------------------------------- # lazy-png-agent: a javaagent that defers the PNG encode in Suramadu's # org.webswing.directdraw.model.ImageConst constructor to the first call of # toMessage() - see instrument/lazy-png-agent/. We have no source for # ImageConst, so this is a bytecode transform applied at classload rather # than a patch to Suramadu itself. # # The only stage here that needs a build tool, so it gets its own base image # rather than javac by hand: the ASM tree API this is written against is not # worth hand-verifying bytecode offsets for. mavenCentral() is reached over # the network during the build, the same as the JDK and MegaMek/async-profiler # archives above; the Gradle dependency cache is its own mount so a rebuild # does not refetch ASM every time. # --------------------------------------------------------------------------- FROM gradle:8.10.2-jdk21 AS lazy-png-agent ARG SURAMADU_WAR COPY instrument/lazy-png-agent/ /src/instrument/lazy-png-agent/ WORKDIR /src/instrument/lazy-png-agent RUN --mount=type=cache,target=/home/gradle/.gradle,sharing=locked \ gradle --no-daemon build # Run the transform against the real ImageConst before the jar is allowed into # the image. A javaagent that rewrites a third-party class at classload has no # business being on by default unless something proves the rewrite each build, # and until now nothing did: HarnessMain existed and was never run. # # It constructs the real DirectDraw and ImageConst out of the war's own jars and # asserts what the transform is for - that the constructor stashed the source # image and did *not* fill the byte field. A rewrite that silently stopped # applying, or applied and broke, fails the build here rather than showing up as # a blank hex in somebody's match. COPY --from=suramadu /opt/suramadu /opt/suramadu RUN set -eu; \ mkdir -p /tmp/dd; \ unzip -q -o "/opt/suramadu/${SURAMADU_WAR}" 'WEB-INF/lib/*.jar' -d /tmp/dd; \ CP="$(find /tmp/dd -name '*.jar' | tr '\n' ':')"; \ echo "$CP" | grep -q directdraw \ || { echo "ERROR: no directdraw jar in the war; did Suramadu move it?" >&2; exit 1; }; \ javac -d /tmp/harness test/HarnessMain.java; \ java -javaagent:build/libs/lazy-png-agent.jar \ -cp "/tmp/harness:$CP" HarnessMain # --------------------------------------------------------------------------- # test: everything that gates a build, run in the build environment # --------------------------------------------------------------------------- # git is here for tests/shell/test-deploy.sh, which builds throwaway # repositories to exercise the guards deploy.sh puts in front of a push. It # never reaches the runtime image - that is a separate FROM further down. FROM arena AS test RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update && apt-get install -y --no-install-recommends \ shellcheck jq python3 git brotli COPY --from=megamek /opt/MegaMek-patched.jar /opt/MegaMek-patched.jar COPY . /src/ RUN JAVA_HOME=/opt/jdk MM_HOME=/opt/megamek PATCHED_JAR=/opt/MegaMek-patched.jar \ /src/tests/run.sh # --------------------------------------------------------------------------- # runtime # --------------------------------------------------------------------------- FROM debian:bookworm-slim AS runtime # Only SURAMADU_WAR is declared here, because it feeds ENV below. The metadata # args - and the LABEL that consumes them - are at the very end of this stage on # purpose: BUILD_REF is the git SHA, so it changes on every commit, and anything # after an instruction that uses it is invalidated. With the LABEL up here, every # commit re-ran the apt install, every COPY and the fontconfig generation. ARG SURAMADU_WAR # fontconfig is required: MegaMek draws armor pips, heat markers and status icons # with symbol codepoints, and the Suramadu toolkit reads a legacy # fontconfig.properties that we generate below. DejaVu has the coverage Noto # Sans lacks; Noto Sans Symbols2 supplies the pips. # # The libx* packages are needed even though nothing renders to a display - # java.desktop links them regardless of the toolkit in use. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl jq coreutils procps \ fontconfig fonts-dejavu-core fonts-noto-core \ libfreetype6 libfontconfig1 \ libx11-6 libxext6 libxi6 libxrender1 libxtst6 libxau6 libxdmcp6 ENV ARENA_HOME=/opt/arena \ ARENA_RUN=/run/arena \ ARENA_PORT_WS=8080 \ ARENA_PORT_MM=8850 \ ARENA_SURAMADU_WAR=${SURAMADU_WAR} \ JAVA_HOME=/opt/arena/jre \ PATH=/opt/arena/jre/bin:$PATH COPY --from=jre /opt/jre ${ARENA_HOME}/jre COPY --from=megamek /opt/megamek ${ARENA_HOME}/megamek COPY --from=megamek /opt/MegaMek-patched.jar ${ARENA_HOME}/MegaMek-patched.jar COPY --from=suramadu /opt/suramadu ${ARENA_HOME}/suramadu COPY --from=arena /opt/arena.jar ${ARENA_HOME}/arena.jar # ~675KB unpacked, ~290KB of the pulled image. Dead weight in every match that # does not set ARENA_PROFILE, and worth it: the alternative is a second image # that is no longer the one production runs. COPY --from=profiler /opt/async-profiler ${ARENA_HOME}/async-profiler # Always loaded (see container/init/40-render-config.sh) - unlike the profiler, # this is not optional instrumentation. COPY --from=lazy-png-agent /src/instrument/lazy-png-agent/build/libs/lazy-png-agent.jar \ ${ARENA_HOME}/instrument/lazy-png-agent.jar COPY container/ ${ARENA_HOME}/container/ # The repo groups these by component; the image groups them by function, because # the container scripts want one config directory. # Served instead of the war's index.html, so the loading page is ours, plus the # two files it adds: theme.css and perf.js, the latency instrument that stays # inert unless a match is opened with ?perf=1. Verified against the war in the # suramadu stage. # From the suramadu stage rather than the build context: it is where the .gz # and .br siblings are written, and a sibling that is not there is not served. COPY --from=suramadu /src/suramadu/web/ ${ARENA_HOME}/web/ COPY megamek/clientsettings.xml.template ${ARENA_HOME}/config/ COPY megamek/gameoptions.xml.template ${ARENA_HOME}/config/ COPY megamek/log4j2-quiet.xml ${ARENA_HOME}/config/ COPY suramadu/suramadu.config.template ${ARENA_HOME}/config/ COPY suramadu/jetty.properties.template ${ARENA_HOME}/config/ COPY suramadu/log4j2-server.xml ${ARENA_HOME}/config/ # Shipped as source, not used at runtime: we distribute modified GPL-3.0 and # AGPL-3.0 code, so the modifications travel with the binaries that contain them. COPY megamek/patches/ ${ARENA_HOME}/patches/ COPY suramadu/patches/ ${ARENA_HOME}/patches/suramadu/ COPY LICENSING.md ${ARENA_HOME}/LICENSING.md COPY versions.env ${ARENA_HOME}/versions.env # Generated here rather than in a build stage: the file records absolute font # paths, which only exist in this image. FONTCONFIG_STRICT makes a missing font # fail the build instead of shipping boxed glyphs. COPY suramadu/gen-fontconfig.sh /tmp/gen-fontconfig.sh RUN chmod +x /tmp/gen-fontconfig.sh \ ${ARENA_HOME}/container/entrypoint.sh \ ${ARENA_HOME}/container/init/*.sh \ ${ARENA_HOME}/container/watch/*.sh \ ${ARENA_HOME}/container/exit/*.sh \ && MM_HOME=${ARENA_HOME}/megamek FONTCONFIG_STRICT=1 \ /tmp/gen-fontconfig.sh ${ARENA_HOME}/fontconfig.properties \ && rm /tmp/gen-fontconfig.sh # Unprivileged. MegaMek and Suramadu write inside their own trees, so those # are the only writable paths; everything else stays owned by root. # --home-dir is the tmpfs, not ARENA_HOME. The JVM takes user.home from the # passwd entry rather than $HOME, and /opt/arena is root-owned, so pointing it # there made every JVM fail to create $user.home/.java/.userPrefs and log # "Couldn't create user preferences directory" twice at startup. # container/entrypoint.sh creates the directory before anything starts. RUN useradd --system --uid 10001 --home-dir ${ARENA_RUN}/home --shell /usr/sbin/nologin arena \ && mkdir -p ${ARENA_RUN} \ ${ARENA_HOME}/megamek/mmconf ${ARENA_HOME}/megamek/logs \ ${ARENA_HOME}/megamek/savegames \ ${ARENA_HOME}/suramadu/logs ${ARENA_HOME}/suramadu/tmp \ && chown -R arena:arena ${ARENA_RUN} \ ${ARENA_HOME}/megamek/mmconf ${ARENA_HOME}/megamek/logs \ ${ARENA_HOME}/megamek/savegames \ ${ARENA_HOME}/suramadu/logs ${ARENA_HOME}/suramadu/tmp # MegaMek merges userdata/data over its own data/ for files opened through # MegaMekFile, and resolves "userdata" against its working directory. Pointing # it at the tmpfs keeps anything written there out of every image layer. The # target is created by container/init/30-assets.sh, so the symlink dangles # until launch. # # Not the camo path. That merge does not cover camo - see the note in # 30-assets.sh - and per-match camo is staged on the tmpfs directly. RUN rm -rf ${ARENA_HOME}/megamek/userdata \ && ln -s ${ARENA_RUN}/userdata ${ARENA_HOME}/megamek/userdata # MegaMek writes crew-portrait tooltips to data/images/temp at runtime, and # the image tree is read-only to the arena user. Point it at the tmpfs too; # container/init/30-assets.sh creates the target. RUN rm -rf ${ARENA_HOME}/megamek/data/images/temp \ && ln -s ${ARENA_RUN}/images-temp ${ARENA_HOME}/megamek/data/images/temp USER arena WORKDIR ${ARENA_HOME} EXPOSE 8080 # Last, so a new commit rebuilds only this metadata layer. See the note at the # top of this stage. ARG MEGAMEK_VERSION ARG SURAMADU_VERSION ARG BUILD_REF=unknown LABEL org.opencontainers.image.title="lance.blue arena" \ org.opencontainers.image.description="One MegaMek match, played in a browser via Suramadu" \ org.opencontainers.image.licenses="GPL-3.0-or-later AND AGPL-3.0-or-later AND CC-BY-NC-SA-4.0" \ org.opencontainers.image.revision="${BUILD_REF}" \ blue.lance.megamek.version="${MEGAMEK_VERSION}" \ blue.lance.suramadu.version="${SURAMADU_VERSION}" ENTRYPOINT ["/opt/arena/container/entrypoint.sh"]