Ctrl K
ComunidadGuías…

docker/entrypoint.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Factorio headless server entrypoint.
#
# Responsibilities, in order:
#   1. normalise ownership of the data volume and drop root (if started as root)
#   2. lay out the volume on first run
#   3. render server-settings.json (and friends) from environment variables
#   4. update mods against the portal, if UPDATE_MODS=true
#   5. create a save if none exists
#   6. exec the server so it receives signals directly
set -euo pipefail

FACTORIO_DIR="${FACTORIO_DIR:-/factorio}"
FACTORIO_HOME="${FACTORIO_HOME:-/opt/factorio}"
BIN="${FACTORIO_HOME}/factorio"

log()  { printf '[entrypoint] %s\n' "$*"; }
warn() { printf '[entrypoint] WARNING: %s\n' "$*" >&2; }
die()  { printf '[entrypoint] ERROR: %s\n' "$*" >&2; exit 1; }

# --------------------------------------------------------------------------
# 1. privilege handling
# --------------------------------------------------------------------------
# Running as root is supported for convenience (it lets us fix up ownership of
# a freshly created bind mount) but we never *stay* root. If the image is
# started with --user, we skip all of this and run as whoever we were given.
if [ "$(id -u)" = "0" ]; then
  PUID="${PUID:-845}"
  PGID="${PGID:-845}"

  # The image creates this user, but the entrypoint is also usable against a
  # hand-rolled base, so create it rather than failing confusingly if missing.
  if ! getent group factorio >/dev/null 2>&1; then
    groupadd -o -g "${PGID}" factorio
  fi
  if ! id -u factorio >/dev/null 2>&1; then
    useradd -o -u "${PUID}" -g "${PGID}" -d "${FACTORIO_DIR}" -s /usr/sbin/nologin factorio
  fi

  if [ "$(id -u factorio)" != "${PUID}" ] || [ "$(id -g factorio)" != "${PGID}" ]; then
    log "remapping factorio user to ${PUID}:${PGID}"
    groupmod -o -g "${PGID}" factorio
    usermod  -o -u "${PUID}" -g "${PGID}" factorio
  fi

  # Only chown when it is actually wrong — recursively chowning a large saves
  # directory on every boot is slow, especially on an SD card.
  if [ "$(stat -c '%u:%g' "${FACTORIO_DIR}")" != "${PUID}:${PGID}" ]; then
    log "fixing ownership of ${FACTORIO_DIR} (this may take a moment on first run)"
    chown -R "${PUID}:${PGID}" "${FACTORIO_DIR}"
  fi

  log "dropping privileges to ${PUID}:${PGID}"
  exec gosu "${PUID}:${PGID}" "$0" "$@"
fi

# --------------------------------------------------------------------------
# 2. volume layout
# --------------------------------------------------------------------------
mkdir -p \
  "${FACTORIO_DIR}/saves" \
  "${FACTORIO_DIR}/mods" \
  "${FACTORIO_DIR}/config" \
  "${FACTORIO_DIR}/scenarios" \
  "${FACTORIO_DIR}/script-output"

SETTINGS_FILE="${FACTORIO_DIR}/config/server-settings.json"
MAP_GEN_FILE="${FACTORIO_DIR}/config/map-gen-settings.json"
MAP_SETTINGS_FILE="${FACTORIO_DIR}/config/map-settings.json"
ADMINLIST_FILE="${FACTORIO_DIR}/config/server-adminlist.json"
WHITELIST_FILE="${FACTORIO_DIR}/config/server-whitelist.json"
BANLIST_FILE="${FACTORIO_DIR}/config/server-banlist.json"
CONFIG_INI="${FACTORIO_DIR}/config/config.ini"

# Seed the map-generation files from upstream examples so they are easy to edit,
# but never overwrite what the operator has already put there.
[ -f "${MAP_GEN_FILE}" ]      || cp "${FACTORIO_HOME}/data/map-gen-settings.example.json" "${MAP_GEN_FILE}"
[ -f "${MAP_SETTINGS_FILE}" ] || cp "${FACTORIO_HOME}/data/map-settings.example.json"     "${MAP_SETTINGS_FILE}"

# Without this, Factorio's write-data path defaults to alongside its own binary
# (${FACTORIO_HOME}), not the volume — so mods/saves/script-output would resolve
# outside ${FACTORIO_DIR}, and --start-server-load-latest (which takes no path
# argument) would find nothing there since the Dockerfile deletes that directory.
[ -f "${CONFIG_INI}" ] || cat > "${CONFIG_INI}" <<EOF
[path]
read-data=${FACTORIO_HOME}/data
write-data=${FACTORIO_DIR}
EOF

# --------------------------------------------------------------------------
# 3. server-settings.json
# --------------------------------------------------------------------------
# Booleans arrive as strings from the environment; jq needs real JSON booleans.
jqbool() {
  case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
    1|true|yes|y|on)  echo true ;;
    *)                echo false ;;
  esac
}

# Split "a,b,c" into a JSON array, trimming whitespace and dropping empties.
# Built with `jq -n --arg` rather than piping on stdin: an empty string yields
# zero input lines, which makes `jq -R` emit nothing at all, and the empty
# result then fails --argjson in the caller.
jqlist() {
  jq -cn --arg s "${1:-}" \
    '$s | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'
}

if [ "$(jqbool "${UPDATE_CONFIG:-true}")" = "true" ] || [ ! -f "${SETTINGS_FILE}" ]; then
  log "rendering ${SETTINGS_FILE} from environment"

  # RCON without a password would expose an unauthenticated admin console, so
  # generate a strong one rather than silently starting without protection.
  if [ -z "${RCON_PASSWORD:-}" ]; then
    RCON_PASSWORD="$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32 || true)"
    warn "RCON_PASSWORD was not set; generated a random one for this container."
    warn "Set RCON_PASSWORD explicitly if you intend to use RCON."
  fi
  export RCON_PASSWORD

  jq -n \
    --arg     name          "${NAME:-Factorio}" \
    --arg     description   "${DESCRIPTION:-A Factorio server running in Docker}" \
    --argjson tags          "$(jqlist "${TAGS:-}")" \
    --argjson max_players   "${MAX_PLAYERS:-0}" \
    --argjson public        "$(jqbool "${VISIBILITY_PUBLIC:-false}")" \
    --argjson lan           "$(jqbool "${VISIBILITY_LAN:-true}")" \
    --arg     username      "${FACTORIO_USERNAME:-${USERNAME:-}}" \
    --arg     token         "${FACTORIO_TOKEN:-${TOKEN:-}}" \
    --arg     game_password "${GAME_PASSWORD:-}" \
    --argjson verify        "$(jqbool "${REQUIRE_USER_VERIFICATION:-true}")" \
    --argjson upload_max    "${MAX_UPLOAD_IN_KILOBYTES_PER_SECOND:-0}" \
    --argjson upload_slots  "${MAX_UPLOAD_SLOTS:-5}" \
    --argjson min_latency   "${MINIMUM_LATENCY_IN_TICKS:-0}" \
    --argjson pause_empty   "$(jqbool "${AUTO_PAUSE:-true}")" \
    --argjson pause_connect "$(jqbool "${AUTO_PAUSE_WHEN_PLAYERS_CONNECT:-false}")" \
    --argjson admins_pause  "$(jqbool "${ONLY_ADMINS_CAN_PAUSE_THE_GAME:-true}")" \
    --argjson autosave_only "$(jqbool "${AUTOSAVE_ONLY_ON_SERVER:-true}")" \
    --argjson nonblocking   "$(jqbool "${NON_BLOCKING_SAVING:-false}")" \
    --argjson autosave_int  "${AUTOSAVE_INTERVAL:-10}" \
    --argjson autosave_slot "${AUTOSAVE_SLOTS:-5}" \
    --argjson afk_kick      "${AFK_AUTOKICK_INTERVAL:-0}" \
    --argjson admins        "$(jqlist "${ADMINS:-}")" \
    '{
      name: $name,
      description: $description,
      tags: $tags,
      max_players: $max_players,
      visibility: { public: $public, lan: $lan },
      username: $username,
      token: $token,
      game_password: $game_password,
      require_user_verification: $verify,
      max_upload_in_kilobytes_per_second: $upload_max,
      max_upload_slots: $upload_slots,
      minimum_latency_in_ticks: $min_latency,
      ignore_player_limit_for_returning_players: false,
      allow_commands: "admins-only",
      autosave_interval: $autosave_int,
      autosave_slots: $autosave_slot,
      afk_autokick_interval: $afk_kick,
      auto_pause: $pause_empty,
      auto_pause_when_players_connect: $pause_connect,
      only_admins_can_pause_the_game: $admins_pause,
      autosave_only_on_server: $autosave_only,
      non_blocking_saving: $nonblocking,
      admins: $admins
    }' > "${SETTINGS_FILE}.tmp"

  mv "${SETTINGS_FILE}.tmp" "${SETTINGS_FILE}"
  chmod 600 "${SETTINGS_FILE}"   # it can hold a game password and an auth token
else
  log "keeping existing ${SETTINGS_FILE} (UPDATE_CONFIG=false)"
fi

# Optional player lists, rendered only when the corresponding variable is set so
# a hand-maintained file on the volume is never clobbered.
if [ -n "${ADMINS:-}" ]; then
  jqlist "${ADMINS}" > "${ADMINLIST_FILE}"
fi
if [ -n "${WHITELIST:-}" ]; then
  jqlist "${WHITELIST}" > "${WHITELIST_FILE}"
fi
if [ ! -f "${BANLIST_FILE}" ]; then
  echo '[]' > "${BANLIST_FILE}"
fi

# --------------------------------------------------------------------------
# 4. mods
# --------------------------------------------------------------------------
if [ "$(jqbool "${UPDATE_MODS:-false}")" = "true" ]; then
  log "UPDATE_MODS=true, checking mods against the portal"
  /usr/local/bin/update-mods.sh
fi

# --------------------------------------------------------------------------
# 5. save file
# --------------------------------------------------------------------------
SAVE_NAME="${SAVE_NAME:-default}"
SAVE_PATH="${FACTORIO_DIR}/saves/${SAVE_NAME}.zip"

shopt -s nullglob
existing_saves=("${FACTORIO_DIR}"/saves/*.zip)
shopt -u nullglob

if [ ${#existing_saves[@]} -eq 0 ]; then
  if [ "$(jqbool "${GENERATE_NEW_SAVE:-true}")" = "true" ]; then
    log "no save found — generating a new map at ${SAVE_PATH}"
    "${BIN}" \
      --create "${SAVE_PATH}" \
      --map-gen-settings "${MAP_GEN_FILE}" \
      --map-settings "${MAP_SETTINGS_FILE}" \
      --config "${CONFIG_INI}"
  else
    die "no save found in ${FACTORIO_DIR}/saves and GENERATE_NEW_SAVE=false"
  fi
else
  log "found ${#existing_saves[@]} save(s) in ${FACTORIO_DIR}/saves"
fi

# --------------------------------------------------------------------------
# 6. launch
# --------------------------------------------------------------------------
args=(
  --port "${PORT:-34197}"
  --server-settings "${SETTINGS_FILE}"
  --server-banlist "${BANLIST_FILE}"
  --server-id "${FACTORIO_DIR}/config/server-id.json"
  --config "${CONFIG_INI}"
)

# Prefer the newest save unless the operator pinned one by name.
if [ "$(jqbool "${LOAD_LATEST_SAVE:-true}")" = "true" ]; then
  args+=(--start-server-load-latest)
else
  args+=(--start-server "${SAVE_PATH}")
fi

if [ -f "${ADMINLIST_FILE}" ]; then
  args+=(--server-adminlist "${ADMINLIST_FILE}")
fi
if [ -f "${WHITELIST_FILE}" ]; then
  args+=(--server-whitelist "${WHITELIST_FILE}" --use-server-whitelist)
fi
if [ -n "${BIND:-}" ]; then
  args+=(--bind "${BIND}")
fi
if [ -n "${CONSOLE_LOG:-}" ]; then
  args+=(--console-log "${CONSOLE_LOG}")
fi

if [ -n "${RCON_PASSWORD:-}" ]; then
  args+=(--rcon-port "${RCON_PORT:-27015}" --rcon-password "${RCON_PASSWORD}")
fi

# Anything the operator wants to pass straight through.
if [ -n "${EXTRA_ARGS:-}" ]; then
  # shellcheck disable=SC2206  # deliberate word-splitting of an argument string
  args+=(${EXTRA_ARGS})
fi

log "starting: factorio ${args[*]//${RCON_PASSWORD:-__none__}/******}"
exec "${BIN}" "${args[@]}"