Factorio Field guide

Factorio headless saves: backup and restore

Back up Factorio data and mods, keep an off-host copy and verify a selected save in an isolated environment.

Factorio headless saves: backup and restore

A recoverable Factorio backup needs more than a recent ZIP. Keep the factory, its mod environment, the exact game release and enough configuration to start the intended world again. Then prove that the external copy can load without touching the running factory.

This guide continues the Ubuntu headless setup: service and account factorio, executable /opt/factorio/bin/x64/factorio, runtime directory /srv/factorio. It assumes Bash, sudo, GNU tar, Python 3 and an existing working Factorio 2.x installation. Adjust these paths to your installation before copying commands. The procedure deliberately includes downtime.

Decide what the copy must recover#

The runtime directory contains more than saves: keep config.ini, server settings, mods and their settings, access lists, and any custom scenarios. The directory reference explains the separation between program files and user data. If your config or startup arguments reference files elsewhere, include those separately and document their paths.

Keep the exact headless installer alongside the backup. A version number identifies what you need, but does not guarantee that you will still be able to download it later. For a modded factory, preserve the installed mod archives rather than relying on today's releases. This is a backup workflow; use the compatibility guide when changing game or mod versions.

Autosave slots rotate. They help recover recent progress, but files on the same disk disappear with that disk. Configure the desired interval and number of slots in the installed release's server-settings.json; the Wube template documents these fields. Do not enable experimental non-blocking saving as a shortcut for backup consistency.

An archive is a recovery candidate. Loading a selected save from a separate extracted copy establishes whether it contains a usable factory.

Stop cleanly before taking the archive#

Announce the interruption, then stop through the existing service:

bash
sudo systemctl stop factorio
sudo systemctl show factorio -p ActiveState -p SubState -p Result
sudo journalctl -u factorio -n 100 --no-pager

Check that Factorio exited and that the journal records the completed save. The headless documentation describes saving on exit; a forced kill or a stop timeout cannot provide the same assurance. If the service reports a failure, the disk is full, or saving failed, preserve the available files but investigate before calling them the latest factory. Do not archive while another supervisor is restarting the process.

Run this block in the same Bash session. It creates a new private directory; mktemp avoids overwriting an earlier backup:

bash
(
set -e
sudo install -d -o root -g root -m 700 /var/backups/factorio
backup_dir=$(sudo mktemp -d /var/backups/factorio/backup-XXXXXXXX)
sudo sh -c '/opt/factorio/bin/x64/factorio --version > "$1/version.txt"' sh "$backup_dir"
sudo tar -czf "$backup_dir/runtime.tar.gz" -C /srv factorio
sudo chmod 600 "$backup_dir/runtime.tar.gz" "$backup_dir/version.txt"
sudo tar -tzf "$backup_dir/runtime.tar.gz" >/dev/null
sudo sha256sum "$backup_dir/runtime.tar.gz"
printf '%s\n' "$backup_dir"
)

Continue only if each command succeeds. Confirm that the archive contains the expected saves and settings, record which save you intend to restore, and retain the checksum. A successful archive listing checks readability, not game compatibility. Start production again with sudo systemctl start factorio and check its journal; the remaining work uses the captured copy.

Keep a verified copy outside the VPS#

Copy runtime.tar.gz, version.txt and the matching installer to private storage on another machine. An encrypted backup destination or SSH transfer is appropriate: server settings can contain credentials. For an SSH transfer, first create an operator-owned mode-600 copy of the root-only archive, download it, compare SHA-256 on both ends and remove the temporary download copy. Never make /var/backups/factorio world-readable to simplify the transfer.

Keep several dated generations, including one from before a game update. Set retention according to storage and acceptable lost progress. Deleting the only known-good copy because a new upload completed confuses transfer success with recovery success.

Restore into a fresh directory#

Use your own trusted archive. Set archive to the absolute path of the external copy after transferring it back. This recipe requires regular files and directories: the preflight rejects symbolic links, archive hard links and special files before extraction or configuration writes. If it refuses your custom layout, prepare an independent regular-file copy with its external data first. It also refuses to reuse an existing restore directory:

bash
(
set -e
read -r -p "Absolute archive path: " archive
sudo test -f "$archive"
sudo python3 - "$archive" <<'PYARCHIVE'
import sys
import tarfile
from pathlib import PurePosixPath
with tarfile.open(sys.argv[1], 'r:gz') as archive:
    for member in archive.getmembers():
        path = PurePosixPath(member.name)
        if path.is_absolute() or '..' in path.parts:
            raise SystemExit('Unsafe archive path')
        if not (member.isfile() or member.isdir()):
            raise SystemExit('This restore example requires regular files and directories')
PYARCHIVE
sudo mkdir -m 750 /srv/factorio-restore
sudo tar --no-same-owner -xzf "$archive" -C /srv/factorio-restore
sudo chown -R factorio:factorio /srv/factorio-restore
sudo -u factorio tee /srv/factorio-restore/factorio/config.ini >/dev/null <<'CONFIG'
[path]
read-data=/opt/factorio/data
write-data=/srv/factorio-restore/factorio
CONFIG
)

Use a matching executable at /opt/factorio; if production has moved to another release, use a separate preserved installation and adjust read-data too. Inspect extracted files and change any other absolute paths pointing to production. Create private test settings without public or LAN advertising:

bash
sudo -u factorio python3 - <<'PY'
import json
from pathlib import Path
root = Path('/srv/factorio-restore/factorio')
settings = json.loads((root / 'server-settings.json').read_text())
settings['visibility'] = {'public': False, 'lan': False}
for key in ('username', 'password', 'token'):
    settings[key] = ''
(root / 'restore-settings.json').write_text(json.dumps(settings, indent=2))
PY

Select a real ZIP from the restored saves directory. Replace CHOSEN.zip; do not use latest-save selection for this check:

bash
sudo -u factorio /opt/factorio/bin/x64/factorio \
  --config /srv/factorio-restore/factorio/config.ini \
  --mod-directory /srv/factorio-restore/factorio/mods \
  --start-server /srv/factorio-restore/factorio/saves/CHOSEN.zip \
  --server-settings /srv/factorio-restore/factorio/restore-settings.json \
  --bind 127.0.0.1 --port 34198

Check that port 34198 is unused first. This foreground instance is local-only; do not open a firewall rule for it. Review loading errors and stop it with Ctrl+C. A successful load is useful evidence, but still inspect the factory in a matching local client using a separate save copy: check the expected location, research and recognizable recent construction.

For production recovery, stop the production service, preserve its current state and deliberately choose the restored ZIP using the save-selection guide. Rejoin and check persistence after a normal restart. Keep the external recovery copy unchanged until that check passes.

With several instances, a workspace for your own VPS can help organize operations. Keep the external backup destination and restore checks in your administration procedure.