Skip to content

Instantly share code, notes, and snippets.

@glektarssza
Last active July 7, 2025 17:30
Show Gist options
  • Save glektarssza/c39b4d34ca893108fa1f29742c3e3e27 to your computer and use it in GitHub Desktop.
Save glektarssza/c39b4d34ca893108fa1f29742c3e3e27 to your computer and use it in GitHub Desktop.
Factorio Systemd Service
#-- Put your environment variable overrides here!
[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
[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
[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
[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
[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'
[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
#!/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 "")
#!/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