Factorio Field guide

How to set up a Factorio server on Ubuntu 24.04

Run a Factorio headless server on Ubuntu 24.04 with a save, private access, UDP networking, automatic startup and backups.

How to set up a Factorio server on Ubuntu 24.04

Install an exact Factorio release, create a base-game factory and keep it running through systemd. The important split is between replaceable game files in /opt/factorio and the saves, mods and settings in /srv/factorio.

Use a fresh Ubuntu 24.04 x86_64 VPS with a public IPv4 address, SSH access and sudo. If it already hosts Factorio, locate and back up the existing data before using these paths.

Pin the release before installing#

Read the complete version number in every player's client. Install the same release on the server. The free Linux headless download includes Space Age, so we will explicitly disable expansion content for this base-game world.

Run from your normal SSH account:

bash
sudo apt update
sudo apt install -y curl xz-utils python3 ufw nano
sudo useradd --system --user-group --home-dir /srv/factorio \
  --shell /usr/sbin/nologin factorio
sudo install -d -o factorio -g factorio -m 750 \
  /srv/factorio /srv/factorio/saves /srv/factorio/mods
read -r -p "Factorio version, for example 2.0.77: " factorio_release
curl --fail --location \
  "https://factorio.com/get-download/${factorio_release}/headless/linux64" \
  --output /tmp/factorio-headless.tar.xz
sudo tar --no-same-owner -xJf /tmp/factorio-headless.tar.xz -C /opt
/opt/factorio/bin/x64/factorio --version

The version prompt is a choice, not an instruction to use the example release. Check the reported version before continuing. Keep the installer and its SHA256 with your deployment notes; an automatic stable download during boot can change the game before players update.

Give the factory its own data directory#

Game updates replace /opt/factorio; the factory belongs in /srv/factorio, with a backup kept outside the VPS.

Create the config used by every invocation below:

bash
sudo tee /srv/factorio/config.ini >/dev/null <<'EOF'
[path]
read-data=/opt/factorio/data
write-data=/srv/factorio
EOF
sudo chown factorio:factorio /srv/factorio/config.ini
sudo chmod 640 /srv/factorio/config.ini

read-data selects packaged assets; write-data selects runtime data. These are distinct locations in Factorio's directory reference.

Disable the three expansion components before generating a base-game save:

bash
sudo tee /srv/factorio/mods/mod-list.json >/dev/null <<'EOF'
{"mods":[
  {"name":"base","enabled":true},
  {"name":"space-age","enabled":false},
  {"name":"quality","enabled":false},
  {"name":"elevated-rails","enabled":false}
]}
EOF
sudo chown factorio:factorio /srv/factorio/mods/mod-list.json
sudo -u factorio /opt/factorio/bin/x64/factorio \
  --config /srv/factorio/config.ini \
  --mod-directory /srv/factorio/mods \
  --create /srv/factorio/saves/factory.zip

For Space Age, enable its components before creating the world and ensure that every player owns the expansion. Space Age multiplayer requires it for all participants.

For custom resource settings, create a map in the client and transfer its ZIP to saves while the service is stopped. Keep its ownership as factorio:factorio. Replacing a save on disk does not switch the world already running in memory.

Keep access private#

Copy the settings template from the installed build, then edit it:

bash
sudo install -o factorio -g factorio -m 600 \
  /opt/factorio/data/server-settings.example.json \
  /srv/factorio/server-settings.json
sudo nano /srv/factorio/server-settings.json

Set name, set visibility.public and visibility.lan to false, choose a nonempty game_password, and retain require_user_verification: true. Leave username, password and token empty for this setup. Validate JSON:

bash
sudo -u factorio python3 -m json.tool \
  /srv/factorio/server-settings.json >/dev/null

In Wube's settings template, game_password protects entry; password is an account credential. Public listing needs account authentication. An unlisted server still accepts connections by address, so the game password matters.

Allow the game's UDP port#

Factorio multiplayer uses UDP 34197 by default. Permit it in the provider firewall and on Ubuntu.

Keep SSH reachable. Replace 22 with your actual SSH port before enabling UFW. Account for other services already using the machine, keep this session open and test a second SSH connection.

bash
sudo ufw allow 22/tcp
sudo ufw allow 34197/udp
sudo ufw enable
sudo ufw status verbose

Opening TCP 34197 does not create a UDP rule. Ubuntu's firewall guide covers UFW syntax.

Start the service and test persistence#

Open /etc/systemd/system/factorio.service:

bash
sudo nano /etc/systemd/system/factorio.service

Paste:

ini
[Unit]
Description=Factorio dedicated server
Wants=network-online.target
After=network-online.target

[Service]
User=factorio
Group=factorio
WorkingDirectory=/srv/factorio
ExecStart=/opt/factorio/bin/x64/factorio --config /srv/factorio/config.ini --mod-directory /srv/factorio/mods --start-server-load-latest --server-settings /srv/factorio/server-settings.json --port 34197
Restart=on-failure
RestartSec=5
KillSignal=SIGINT
TimeoutStopSec=300
UMask=0077
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

--start-server-load-latest selects the latest available save on restart; the command reference documents that option. Keep other worlds and backup ZIPs outside saves, where they cannot be selected as the active factory.

bash
sudo systemctl daemon-reload
sudo systemctl enable --now factorio
sudo systemctl status factorio --no-pager
sudo journalctl -u factorio -n 80 --no-pager
sudo ss -lunp 'sport = :34197'

Check the journal's loaded save and the local UDP listener. Then join PUBLIC_IP:34197 from a matching game client, place a recognisable object and wait for an autosave. Restart with sudo systemctl restart factorio and reconnect to check that the object survives. A listening socket alone does not verify this.

Back up before changing the build#

Stop normally and inspect the journal for a completed save before archiving:

bash
sudo systemctl stop factorio
sudo journalctl -u factorio -n 80 --no-pager
sudo install -d -o root -g root -m 700 /var/backups/factorio
sudo tar -czf /var/backups/factorio/factorio-backup.tar.gz -C /srv factorio
sudo chmod 600 /var/backups/factorio/factorio-backup.tar.gz
sudo systemctl start factorio
sudo install -o "$(id -un)" -g "$(id -gn)" -m 600 \
  /var/backups/factorio/factorio-backup.tar.gz "$HOME/factorio-download.tar.gz"

If shutdown failed or timed out, investigate before treating this archive as the latest factory. Download it from your computer, replacing the SSH username and VPS address:

bash
scp SSH_USER@VPS_IP:factorio-download.tar.gz .
ssh SSH_USER@VPS_IP 'rm "$HOME/factorio-download.tar.gz"'

For a custom SSH port use -P PORT with scp and -p PORT with ssh. The archive can contain account credentials: keep the downloaded copy private and outside the VPS. Record the binary release alongside it.

When maintaining several supported game instances, a panel for your own VPS is one way to organise them. Enrolling the machine and creating managed instances are separate steps from this manual installation.

If an update prevents players from joining, use the mod and save recovery guide before changing the working factory.