Last active
July 7, 2025 17:30
-
-
Save glektarssza/c39b4d34ca893108fa1f29742c3e3e27 to your computer and use it in GitHub Desktop.
Factorio Systemd Service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#-- Put your environment variable overrides here! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
#-- Unit metadata | |
Description=Factorio Server (%i) | |
#-- Dependencies | |
After=network-online.target | |
Wants=network-online.target | |
[Service] | |
#-- Service type | |
Type=simple | |
#-- Restart on failure | |
Restart=on-failure | |
#-- Wait 3 seconds before restarting | |
RestartSec=3s | |
#-- Limit to a maximum of 3 restarts per day | |
StartLimitInterval=1d | |
StartLimitBurst=3 | |
#-- User and group | |
User=factorio | |
Group=factorio | |
#-- Define I/O | |
Sockets[email protected] | |
StandardInput=socket | |
StandardOutput=journal | |
StandardError=journal | |
#-- Directories | |
RuntimeDirectory=factorio | |
RuntimeDirectoryMode=0775 | |
RuntimeDirectoryPreserve=no | |
WorkingDirectory=/mnt/factorio-data/ | |
#-- Protect various system parameters | |
ProtectSystem=full | |
ProtectHome=true | |
ProtectKernelTunables=true | |
ProtectKernelModules=true | |
ProtectControlGroups=true | |
#-- Timeout settings | |
TimeoutStopSec=30 | |
#-- Default environment variables | |
Environment=FACTORIO_STOP_BEFORE_SAVE_DELAY=10 | |
Environment=FACTORIO_STOP_AFTER_SAVE_DELAY=5 | |
Environment=FACTORIO_FINAL_STOP_DELAY=3 | |
#-- Environment file | |
EnvironmentFile=-/mnt/factorio-data/config/%i/.envrc | |
#-- Startup command(s) | |
ExecStart=/mnt/factorio-data/startFactorioServer.sh "%i" | |
#-- Stop command(s) | |
ExecStop=/bin/sh -c 'echo "Server is preparing to shut down!" > %t/factorio/%i.stdin' | |
ExecStop=/bin/sh -c 'echo "Saving the world in ${FACTORIO_STOP_BEFORE_SAVE_DELAY} seconds..." > %t/factorio/%i.stdin' | |
ExecStop=sleep ${FACTORIO_STOP_BEFORE_SAVE_DELAY} | |
ExecStop=/bin/sh -c 'echo "Saving the world!" > %t/factorio/%i.stdin' | |
ExecStop=/bin/sh -c 'echo "/save" > %t/factorio/%i.stdin' | |
ExecStop=/bin/sh -c 'echo "World saved! Shutting down in ${FACTORIO_STOP_AFTER_SAVE_DELAY} seconds..." > %t/factorio/%i.stdin' | |
ExecStop=sleep ${FACTORIO_STOP_AFTER_SAVE_DELAY} | |
ExecStop=/bin/sh -c 'echo "Shutting down! Good bye!" > %t/factorio/%i.stdin' | |
ExecStop=sleep ${FACTORIO_FINAL_STOP_DELAY} | |
ExecStop=/bin/sh -c 'echo "/quit" > %t/factorio/%i.stdin' | |
[Install] | |
WantedBy=multi-user.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
[email protected] | |
[Socket] | |
#-- User and group | |
SocketUser=factorio | |
SocketGroup=factorio | |
#-- The mode to create the FIFO node in | |
SocketMode=0665 | |
#-- Define standard input socket | |
ListenFIFO=%t/factorio/%i.stdin | |
#-- Remove when the parent service stops | |
RemoveOnStop=true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
#-- Unit metadata | |
Description=Restart the %i Factorio server on a set interval. | |
#-- Dependencies | |
Requisite=factorio@%i.service | |
[Service] | |
#-- Service type | |
Type=oneshot | |
#-- Timeout settings | |
TimeoutStartSec=30 | |
#-- Startup command(s) | |
ExecStart=/usr/bin/systemctl try-restart factorio@%i.service |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
#-- Unit metadata | |
Description=Restart the %i Factorio server on a set interval. | |
[Timer] | |
#-- The calendar time to restart at (optionally add a time zone) | |
OnCalendar=daily | |
#-- Do not persist next restart time to disk | |
Persistent=false | |
[Install] | |
WantedBy=timers.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
#-- Unit metadata | |
Description=Save the game running on the %i Factorio server on a set interval. | |
#-- Dependencies | |
Requisite=factorio@%i.service | |
[Service] | |
#-- Service type | |
Type=oneshot | |
#-- Startup command(s) | |
ExecStart=/bin/sh -c 'echo "/save" > %t/factorio/%i.stdin' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
#-- Unit metadata | |
Description=Save the game running on the %i Factorio server on a set interval. | |
[Timer] | |
#-- The calendar time to restart at (optionally add a time zone) | |
OnCalendar=hourly | |
#-- Do not persist next restart time to disk | |
Persistent=false | |
[Install] | |
WantedBy=timers.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
#-- Resolve script directory | |
SCRIPT_SOURCE="${BASH_SOURCE[0]}"; | |
while [[ -L "${SCRIPT_SOURCE}" ]]; do | |
SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_SOURCE}")" > /dev/null 2>&1 && pwd)"; | |
SCRIPT_SOURCE="$(readlink "${SCRIPT_SOURCE}")"; | |
[[ ${SCRIPT_SOURCE} != /* ]] && SOURCE="${SCRIPT_DIR}/${SCRIPT_SOURCE}"; | |
done | |
SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_SOURCE}")" > /dev/null 2>&1 && pwd)"; | |
if [[ -z "$1" ]]; then | |
printf "[\x1b[91mFATAL\x1b[0m] A save name/configuration to start is required!\n"; | |
exit 1; | |
fi | |
#-- Start server | |
exec "${SCRIPT_DIR}/bin/x64/factorio" --start-server "$1" --server-adminlist "${SCRIPT_DIR}/config/$1/server-adminlist.json" --server-settings "${SCRIPT_DIR}/config/$1/server-settings.json" $([[ -n "${FACTORIO_USE_WHITELIST}" ]] && echo --use-server-whitelist --server-whitelist "${SCRIPT_DIR}/config/$1/server-whitelist.json" || echo "") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
#-- Resolve script directory | |
SCRIPT_SOURCE="${BASH_SOURCE[0]}"; | |
while [[ -L "${SCRIPT_SOURCE}" ]]; do | |
SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_SOURCE}")" > /dev/null 2>&1 && pwd)"; | |
SCRIPT_SOURCE="$(readlink "${SCRIPT_SOURCE}")"; | |
[[ ${SCRIPT_SOURCE} != /* ]] && SCRIPT_SOURCE="${SCRIPT_DIR}/${SCRIPT_SOURCE}"; | |
done | |
SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_SOURCE}")" > /dev/null 2>&1 && pwd)"; | |
#-- Make sure we're in Factorio root directory | |
if ! pushd "${SCRIPT_DIR}" > /dev/null; then | |
printf "[\x1b[91mFATAL\x1b[0m] Failed to enter Factorio root directory\!"; | |
cleanupEnv; | |
exit 1; | |
fi | |
declare TRUE="true"; | |
declare FALSE="false"; | |
declare RESTART_SERVICES="${FALSE}"; | |
declare START_SERVICES="${FALSE}"; | |
declare FACTORIO_USER FACTORIO_GROUP CHANNEL SERVICE; | |
FACTORIO_USER="$(stat --format="%U" .)"; | |
FACTORIO_GROUP="$(stat --format="%G" .)"; | |
declare -a FACTORIO_SERVICES; | |
printHelp() { | |
printf "updateFactorioServer.sh [options] [arguments]\n"; | |
printf "\n"; | |
printf "=== Arguments ===\n"; | |
printf " channel: The channel to pull updates from [default: %s]\n" "${FACTORIO_CHANNEL:-stable}"; | |
printf " Valid options are: 'stable' or 'experimental'\n"; | |
printf "\n"; | |
printf "=== Options ===\n"; | |
printf " --help|-h: Show this help information an exit.\n"; | |
printf " --restart-services: Restart any running Factorio services. [default: false]\n"; | |
printf " Overrides '--no-restart-services' if placed after that option.\n"; | |
printf " Conflicts with '--start-services'.\n"; | |
printf " --no-restart-services: Do not restart any running Factorio services. [default: false]\n"; | |
printf " Overrides '--restart-services' if placed after that option.\n"; | |
printf " Conflicts with '--start-services'.\n"; | |
printf " --start-services: Start any stopped Factorio services. [default: false]\n"; | |
printf " Conflicts with '--restart-services'.\n"; | |
printf " --factorio-user: The user that owns the various Factorio files. [default: %s]\n" "${FACTORIO_USER}"; | |
printf " --factorio-group: The group that owns the various Factorio files. [default: %s]\n" "${FACTORIO_GROUP}"; | |
printf "\n" | |
} | |
restartServices() { | |
printf "[\x1b[94mINFO\x1b[0m] Preparing to restart previously running Factorio services...\n"; | |
for SERVICE in "${FACTORIO_SERVICES[@]}"; do | |
printf "[\x1b[94mINFO\x1b[0m] Restarting previously running Factorio service '\x1b[96m%s\x1b[0m'...\n" "${SERVICE}"; | |
#-- Start each service we stopped earlier | |
sudo systemctl start "${SERVICE}"; | |
done | |
printf "[\x1b[94mINFO\x1b[0m] Restarted previously running Factorio services\n"; | |
} | |
startServices() { | |
printf "[\x1b[94mINFO\x1b[0m] Discovering stopped Factorio services...\n"; | |
declare -a SERVICES_TO_START; | |
IFS=" " read -r -a FACTORIO_SERVICES <<< "$(systemctl list-units --all | awk '{if ($1 ~ /factorio@.*\.service/ && ! $1 ~ /restart/) {print $1}}' | xargs)"; | |
for SERVICE in "${FACTORIO_SERVICES[@]}"; do | |
if systemctl status "${SERVICE}" | grep -q inactive; then | |
SERVICES_TO_START+=( "${SERVICE}" ); | |
fi | |
done | |
printf "[\x1b[94mINFO\x1b[0m] Preparing to start any discovered Factorio services...\n"; | |
for SERVICE in "${SERVICES_TO_START[@]}"; do | |
printf "[\x1b[94mINFO\x1b[0m] Starting Factorio service '\x1b[96m%s\x1b[0m'...\n" "${SERVICE}"; | |
#-- Start the service | |
sudo systemctl start "${SERVICE}"; | |
done | |
printf "[\x1b[94mINFO\x1b[0m] Started all discovered Factorio services\n"; | |
} | |
cleanupEnv() { | |
#-- Remove temporary directory if it exists | |
if [[ -d "${SCRIPT_DIR}/tmp/" ]]; then | |
rm -r "${SCRIPT_DIR}/tmp/"; | |
fi | |
unset FACTORIO_SERVICES RESTART_SERVICES SCRIPT_DIR SCRIPT_SOURCE CHANNEL; | |
unset START_SERVICES SERVICE FACTORIO_USER FACTORIO_GROUP; | |
#-- Return to original directory | |
popd > /dev/null || return 1; | |
} | |
#-- Parse CLI arguments | |
while [[ -n "$1" ]]; do | |
case "$1" in | |
--help|-h) | |
printHelp; | |
cleanupEnv; | |
exit 0; | |
;; | |
--restart-services) | |
RESTART_SERVICES="${TRUE}"; | |
;; | |
--no-restart-services) | |
RESTART_SERVICES="${FALSE}"; | |
;; | |
--start-services) | |
START_SERVICES="${TRUE}"; | |
;; | |
--factorio-user) | |
shift; | |
FACTORIO_USER="$1"; | |
;; | |
--factorio-user=*) | |
FACTORIO_USER="$(echo "$1" | awk -F'=' '{a = $2; for (i = 3; i <= NF; i += 1) {a = (a "=" $i);} print a;}')"; | |
;; | |
--factorio-group) | |
shift; | |
FACTORIO_GROUP="$1"; | |
;; | |
--factorio-group=*) | |
FACTORIO_GROUP="$(echo "$1" | awk -F'=' '{a = $2; for (i = 3; i <= NF; i += 1) {a = (a "=" $i);} print a;}')"; | |
;; | |
*) | |
if [[ -z "${CHANNEL}" ]]; then | |
CHANNEL="$1" | |
else | |
printf "[\x1b[33mWARN\x1b[0m] Factorio channel already set to '\x1b[96m%s\x1b[0m', ignoring CLI argument '\x1b[95m$1\x1b[0m'!\n" "${CHANNEL}"; | |
fi | |
;; | |
esac | |
shift 1; | |
done | |
if [[ "${RESTART_SERVICES}" == "${TRUE}" && "${START_SERVICES}" == "${TRUE}" ]]; then | |
printf "[\x1b[91mFATAL\x1b[0m] '--restart-services' and '--start-services' conflict, specify only one!\n"; | |
exit 1; | |
fi | |
#-- Default to the stable update channel | |
if [[ -z "${CHANNEL}" ]]; then | |
printf "[\x1b[33mWARN\x1b[0m] No Factorio channel selected, assuming '\x1b[96m%s\x1b[0m'!\n" "${FACTORIO_CHANNEL:-stable}"; | |
CHANNEL="${FACTORIO_CHANNEL:-stable}"; | |
else | |
printf "[\x1b[94mINFO\x1b[0m] Factorio channel '\x1b[96m%s\x1b[0m' selected!\n" "${CHANNEL}"; | |
fi | |
#-- Check if the update channel is valid | |
if [[ "${CHANNEL}" != "stable" && "${CHANNEL}" != "experimental" ]]; then | |
printf "[\x1b[91mFATAL\x1b[0m] Unknown Factorio channel '\x1b[96m%s\x1b[0m' selected!\n" "${CHANNEL}"; | |
exit 1; | |
fi | |
printf "[\x1b[94mINFO\x1b[0m] We're about to do some admin operations!\n"; | |
printf "[\x1b[94mINFO\x1b[0m] For this purpose we're going to use 'sudo' to run some commands.\n"; | |
printf "[\x1b[94mINFO\x1b[0m] If you have not read this script's source code yet, \x1b[1mSTOP AND DO SO NOW\x1b[0m so you understand what's going on.\n"; | |
declare CONSENT; | |
while [[ $? -ne 0 || -z "${CONSENT}" ]]; do | |
read -r -n 1 -t 30 -p "Do you understand? [y/N] " CONSENT; | |
RES=$? | |
if [[ "${CONSENT}" == "n" || "${CONSENT}" == "N" || $RES -gt 128 ]]; then | |
printf "\n"; | |
printf "[\x1b[91mFATAL\x1b[0m] Consent to running admin operations not given!\n"; | |
cleanupEnv; | |
exit 1; | |
elif [[ "${CONSENT}" == "y" || "${CONSENT}" == "Y" ]]; then | |
break; | |
elif [[ $RES -ne 0 ]]; then | |
printf "\n"; | |
printf "[\x1b[91mERROR\x1b[0m] Failed to read input, please try again!\n"; | |
else | |
printf "\n"; | |
printf "[\x1b[91mERROR\x1b[0m] Unparsable input '%s', please try again!\n" "${CONSENT}"; | |
fi | |
done | |
unset CONSENT; | |
printf "\n"; | |
#-- Get sudo privileges so we don't need to ask again for a bit | |
sudo --validate; | |
IFS=" " read -r -a FACTORIO_SERVICES <<< "$(systemctl list-units | grep running | awk '{if ($1 ~ /factorio@.*\.service/) {print $1}}' | xargs)"; | |
if [[ "${RESTART_SERVICES}" == "${TRUE}" ]]; then | |
declare SOME_SERVICES_FAILED="${FALSE}" | |
declare -a UPDATED_FACTORIO_SERVICES; | |
printf "[\x1b[94mINFO\x1b[0m] Preparing to stop running Factorio services...\n"; | |
for SERVICE in "${FACTORIO_SERVICES[@]}"; do | |
printf "[\x1b[94mINFO\x1b[0m] Stopping running Factorio service '\x1b[96m%s\x1b[0m'...\n" "${SERVICE}"; | |
#-- Stop each service and reset their failure count (this avoids failed starts later on) | |
if ! sudo systemctl stop "${SERVICE}"; then | |
SOME_SERVICES_FAILED="${TRUE}"; | |
printf "[\x1b[33mWARN\x1b[0m] Failed to stop Factorio service '\x1b[96m%s\x1b[0m'!\n" "${SERVICE}"; | |
printf "[\x1b[33mWARN\x1b[0m] You will need to do it manually and/or figure out why it couldn't be stopped!\n"; | |
for NEW_SERVICE in "${FACTORIO_SERVICES[@]}"; do | |
if [[ "${NEW_SERVICE}" -ne "${SERVICE}" ]]; then | |
NEW_FACTORIO_SERVICES+=("${NEW_SERVICE}"); | |
fi | |
done | |
IFS=" " read -r -a UPDATED_FACTORIO_SERVICES <<< "${NEW_FACTORIO_SERVICES[@]}"; | |
fi | |
sudo systemctl reset-failed "${SERVICE}"; | |
printf "[\x1b[94mINFO\x1b[0m] Stopped Factorio service '\x1b[96m%s\x1b[0m'\n" "${SERVICE}"; | |
done | |
if [[ "${SOME_SERVICES_FAILED}" == "${TRUE}" ]]; then | |
IFS=" " read -r -a FACTORIO_SERVICES <<< "${UPDATED_FACTORIO_SERVICES[@]}"; | |
fi | |
printf "[\x1b[94mINFO\x1b[0m] Stopped running Factorio services\n"; | |
unset UPDATED_FACTORIO_SERVICES; | |
unset SOME_SERVICES_FAILED; | |
fi | |
#-- Create temporary directory | |
if ! mkdir -p "${SCRIPT_DIR}/tmp/"; then | |
printf "[\x1b[91mFATAL\x1b[0m] Failed to create temporary directory\!"; | |
if [[ "${RESTART_SERVICES}" == "${TRUE}" ]]; then | |
restartServices; | |
fi | |
cleanupEnv; | |
exit 1; | |
fi | |
#-- Enter temporary directory | |
if ! pushd "${SCRIPT_DIR}/tmp/" > /dev/null; then | |
printf "[\x1b[91mFATAL\x1b[0m] Failed to enter temporary directory\!"; | |
if [[ "${RESTART_SERVICES}" == "${TRUE}" ]]; then | |
restartServices; | |
fi | |
cleanupEnv; | |
exit 1; | |
fi | |
#-- Get latest update | |
wget -O "${SCRIPT_DIR}/tmp/factorio-headless-latest.tar.xz" "https://factorio.com/get-download/${CHANNEL}/headless/linux64"; | |
#-- Unpack latest | |
tar xvf "${SCRIPT_DIR}/tmp/factorio-headless-latest.tar.xz"; | |
#-- Return to Factorio root | |
if ! popd > /dev/null; then | |
printf "[\x1b[91mFATAL\x1b[0m] Failed to exit temporary directory\!"; | |
if [[ "${RESTART_SERVICES}" == "${TRUE}" ]]; then | |
restartServices; | |
fi | |
cleanupEnv; | |
exit 1; | |
fi | |
#-- Remove executable and data directories | |
if [[ -d "${SCRIPT_DIR}/bin/" ]]; then | |
sudo -u "${FACTORIO_USER}" -g "${FACTORIO_GROUP}" rm -r "${SCRIPT_DIR}/bin/"; | |
fi | |
if [[ -d "${SCRIPT_DIR}/data/" ]]; then | |
sudo -u "${FACTORIO_USER}" -g "${FACTORIO_GROUP}" rm -r "${SCRIPT_DIR}/data/"; | |
fi | |
#-- Apply update | |
sudo -u "${FACTORIO_USER}" -g "${FACTORIO_GROUP}" cp -r "${SCRIPT_DIR}/tmp/factorio/bin/" "${SCRIPT_DIR}/tmp/factorio/data/" "${SCRIPT_DIR}"; | |
if [[ "${START_SERVICES}" == "${TRUE}" ]]; then | |
startServices; | |
elif [[ "${RESTART_SERVICES}" == "${TRUE}" ]]; then | |
restartServices; | |
fi | |
cleanupEnv; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment