panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 5232609719
2160 changed files with 300415 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# panel-native Factorio runtime image.
FROM debian:12-slim
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
locales \
tini \
xz-utils \
curl \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -g 1000 panel \
&& useradd -u 1000 -g 1000 -m -s /bin/bash panel \
&& mkdir -p /game /game-saves \
&& chown -R panel:panel /game /game-saves
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 34197/udp 27015/tcp
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# panel-native Factorio entrypoint.
set -euo pipefail
log() { printf '[panel-factorio] %s\n' "$*"; }
if [[ "$(id -u)" = "0" ]]; then
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
exec setpriv --reuid=panel --regid=panel --clear-groups \
--inh-caps=-all --bounding-set=-all \
env HOME=/game-saves PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
RCON_PASSWORD="${RCON_PASSWORD:-panel_rcon}" \
GAME_PORT="${GAME_PORT:-34197}" \
RCON_PORT="${RCON_PORT:-27015}" \
/entrypoint.sh "$@"
fi
GAME_DIR=/game
SAVE_DIR=/game-saves
# factorio.com's headless tarball extracts into a `factorio/` subdir. Our
# updater unpacks into /game, so we look for both.
BIN="${GAME_DIR}/factorio/bin/x64/factorio"
if [[ ! -x "${BIN}" ]]; then
BIN="${GAME_DIR}/bin/x64/factorio"
fi
SAVE="${SAVE_DIR}/saves/default.zip"
if [[ ! -x "${BIN}" ]]; then
log "ERROR: Factorio binary not found at /game/{factorio/,}bin/x64/factorio."
log " Run 'Update' in the panel to install Factorio (Direct provider)."
exit 78
fi
mkdir -p "${SAVE_DIR}/saves" "${SAVE_DIR}/mods" "${SAVE_DIR}/config" "${SAVE_DIR}/logs"
# Seed server-settings.json from Factorio's shipped example on first boot.
# Without this Factorio fails fast: "Hosting multiplayer game failed:
# filesystem error: file_size(p): unknown error: No such file or directory
# [/game-saves/config/server-settings.json]" — observed on smoke test.
# The example tree lives next to the binary at <bin>/../../data/.
DATA_DIR="$(dirname "${BIN}")/../../data"
if [[ ! -f "${SAVE_DIR}/config/server-settings.json" && -f "${DATA_DIR}/server-settings.example.json" ]]; then
log "seeding server-settings.json from example"
cp "${DATA_DIR}/server-settings.example.json" "${SAVE_DIR}/config/server-settings.json"
fi
# map-gen-settings + map-settings aren't required for boot, but having the
# examples in place means operators can edit them via the Files tab without
# having to dig into the binary tree.
for f in map-gen-settings.example.json map-settings.example.json; do
base="${f%.example.json}.json"
if [[ ! -f "${SAVE_DIR}/config/${base}" && -f "${DATA_DIR}/${f}" ]]; then
cp "${DATA_DIR}/${f}" "${SAVE_DIR}/config/${base}"
fi
done
# Create a default world on first boot — Factorio can't autostart from nothing.
if [[ ! -s "${SAVE}" ]]; then
log "no existing save — creating default map"
"${BIN}" --create "${SAVE}"
fi
cd "$(dirname "${BIN}")/../.."
GAME_PORT="${GAME_PORT:-34197}"
RCON_PORT="${RCON_PORT:-27015}"
log "starting Factorio headless server"
log " save: ${SAVE}"
log " settings: ${SAVE_DIR}/config/server-settings.json"
log " game port: ${GAME_PORT}/udp"
log " rcon port: ${RCON_PORT}/tcp"
exec stdbuf -oL -eL "${BIN}" \
--start-server "${SAVE}" \
--server-settings "${SAVE_DIR}/config/server-settings.json" \
--port "${GAME_PORT}" \
--rcon-port "${RCON_PORT}" \
--rcon-password "${RCON_PASSWORD}" \
--mod-directory "${SAVE_DIR}/mods" \
--console-log "${SAVE_DIR}/logs/factorio.log"
+125
View File
@@ -0,0 +1,125 @@
# Factorio — panel-native dedicated server module.
#
# Native Linux binary. Factorio has two install sources: SteamCMD app 427520
# (only if you own Factorio on Steam) OR direct download from factorio.com.
# We go direct: no Steam auth needed, and headless versions ship unzipped
# tar.xz archives. Users can swap to SteamCMD if they prefer.
#
# RCON: Factorio ships a source-compatible RCON server (same wire protocol
# as Valve's). Enabled via --rcon-port + --rcon-password flags.
#
# Reference: https://github.com/CubeCoders/AMPTemplates (Factorio.kvp)
id: factorio
name: "Factorio"
version: 0.1.0
authors:
- panel contributors
supported_modes:
- docker
runtime:
docker:
image: panel-factorio:latest
# Host networking -- container shares the host net namespace, so
# any port the game/mod binds is directly on the LAN. AMP-like
# model. Multi-instance safety comes from env-driven per-instance
# ports (panel allocator + env: mapping on each ports entry).
network_mode: host
browseable_root: /game-saves
browseable_roots:
- name: "Saves & Configs"
path: /game-saves
hint: "Save games + server-settings.json + mod-list.json"
- name: "Game Files"
path: /game
hint: "Binaries, mods/, data/"
env:
RCON_PASSWORD: "panel_rcon"
FACTORIO_VERSION: "stable" # "stable" or "latest" (experimental)
# Pre-declared so the agent's resolve.go filter lets the panel-allocated
# port through to the container env (only keys ALREADY in docker.env get
# overridden by config_values).
GAME_PORT: "34197"
RCON_PORT: "27015"
volumes:
- { type: volume, name: "panel-$INSTANCE_ID-saves", container: "/game-saves" }
- { type: volume, name: "panel-$INSTANCE_ID-game", container: "/game" }
- { target: "$DATA_PATH/logs", container: "/game-saves/logs" }
ports:
- { name: game, proto: udp, default: 34197, required: true, env: GAME_PORT }
- { name: rcon, proto: tcp, default: 27015, internal: true, env: RCON_PORT }
resources:
min_ram_mb: 1024
recommended_ram_mb: 4096 # mods + big bases
# Per-instance generated RCON secret. The agent generates a random
# 24-byte token at create time (pkg/module GenerateSecrets), which
# overrides the env default for RCON_PASSWORD below — so every instance
# gets a unique password. The weak env default only applies to
# containers launched outside the panel.
secrets:
- name: RCON_PASSWORD
description: "Factorio RCON password — generated per instance"
generated: true
rcon:
adapter: source_rcon
host_port: rcon
auth: source_rcon_login
password_secret: RCON_PASSWORD
# Legacy fallback only (pre-generated-secret instances). New instances
# always get the generated RCON_PASSWORD secret above.
password_literal: "panel_rcon"
commands:
list: "/players online"
save: "/server-save"
say: "{msg}" # Factorio treats non-/ input as chat to all players
kick: "/kick {player} {reason}"
ban: "/ban {player} {reason}"
state_sources:
- type: rcon
command: "/players online count"
every: 60s
parse:
kind: regex
pattern: 'Online players \((?P<count>\d+)\)'
fields:
players_online: count
events:
join:
pattern: "\\[JOIN\\] (?P<name>\\S+) joined the game"
kind: join
leave:
pattern: "\\[LEAVE\\] (?P<name>\\S+) left the game"
kind: leave
chat:
pattern: "\\[CHAT\\] (?P<name>\\S+): (?P<msg>.*)"
kind: chat
update_providers:
- id: stable
kind: direct
url: "https://factorio.com/get-download/stable/headless/linux64"
target_path: /game
- id: experimental
kind: direct
url: "https://factorio.com/get-download/latest/headless/linux64"
target_path: /game
# --- Manifest-driven UI metadata (WI-07) --------------------------
# Consumed by the CONTROLLER/dashboard only (the controller loads its
# own ./modules copy at startup); the agent ignores these at runtime.
# ready_pattern is a JavaScript regex (slash-delimited when it needs
# flags) moved verbatim from the dashboard's READY_PATTERNS map.
ready_pattern: '/Hosting game at IP ADDR|changing state from\(CreatingGame\) to\(InGame\)/i'
appearance:
emoji: "🏭"
grad: "linear-gradient(135deg,#8c6239,#d4a373)"
art: "/game-art/factorio.jpg"
steam: "427520"