--- title: 'How to self host a Factorio headless server' description: 'Automated with ansible' date: '2024-09-25' tags: - ansible - Debian - Factorio --- ## Introduction With the upcoming v2.0 release next month, we decided to try a [seablock](https://mods.factorio.com/mod/SeaBlock) run with a friend and see how far we go in this time frame. Here is a the small ansible role I wrote to deploy this. It is for a Debian server but any Linux distribution with systemd will do. And if you ignore the service unit file, any Linux or even [FreeBSD](factorio-server-in-a-linux-jail.md) will do. ## Tasks This role has a single `tasks/main.yaml` file containing the following. ### User This is fairly standard: ``` yaml - name: 'Create factorio group' group: name: 'factorio' system: 'yes' - name: 'Create factorio user' user: name: 'factorio' group: 'factorio' shell: '/usr/bin/bash' home: '/srv/factorio' createhome: 'yes' system: 'yes' password: '*' ``` ### Factorio Factorio has an API endpoint that provides information about its latest releases, I query and then parse it with: ``` yaml - name: 'Retrieve factorio latest release number' shell: cmd: "curl -s https://factorio.com/api/latest-releases | jq -r '.stable.headless'" register: 'factorio_version_info' changed_when: False - set_fact: factorio_version: '{{ factorio_version_info.stdout_lines[0] }}' ``` Afterwards, it is just a question of downloading and extracting factorio: ``` yaml - name: 'Download factorio' get_url: url: "https://www.factorio.com/get-download/{{ factorio_version }}/headless/linux64" dest: '/srv/factorio/headless-{{ factorio_version }}.zip' mode: '0444' register: 'factorio_downloaded' - name: 'Extract new factorio version' ansible.builtin.unarchive: src: '/srv/factorio/headless-{{ factorio_version }}.zip' dest: '/srv/factorio' owner: 'factorio' group: 'factorio' remote_src: 'yes' notify: 'restart factorio' when: 'factorio_downloaded.changed' ``` I also create the saves directory with: ``` yaml - name: 'Make factorio saves directory' file: path: '/srv/factorio/factorio/saves' owner: 'factorio' group: 'factorio' mode: '0750' state: 'directory' ``` ### Configuration files There are two configuration files to copy from the `files` folder: ``` yaml - name: 'Deploy configuration files' copy: src: '{{ item.src }}' dest: '{{ item.dest }}' owner: 'factorio' group: 'factorio' mode: '0440' notify: - 'systemctl daemon-reload' - 'restart factorio' loop: - { src: 'factorio.service', dest: '/etc/systemd/system/' } - { src: 'server-adminlist.json', dest: '/srv/factorio/factorio/' } ``` The systemd service unit file contains: ``` ini [Unit] Descripion=Factorio Headless Server After=network.target After=systemd-user-sessions.service After=network-online.target [Service] Type=simple User=factorio ExecStart=/srv/factorio/factorio/bin/x64/factorio --start-server game.zip WorkingDirectory=/srv/factorio/factorio [Install] WantedBy=multi-user.target ``` The admin list is simply: ``` json ["adyxax"] ``` I generate the factorio game password with terraform/OpenTofu using a resource like: ``` hcl resource "random_password" "factorio" { length = 16 lifecycle { ignore_changes = [ length, lower, ] } } ``` This allows me to have it persist in the terraform state which is a good thing. For simplification, let's say that this state (which is a json file) is in a local file that I can load with: ``` yaml - name: 'Load the tofu state to read the factorio game password' include_vars: file: '../../../../adyxax.org/01-legacy/terraform.tfstate' name: 'tofu_state_legacy' ``` Given this template file: ``` json { "name": "Normalians", "description": "C'est sur ce serveur que jouent les beaux gosses", "tags": ["game", "tags"], "max_players": 0, "visibility": { "public": false, "lan": false }, "username": "", "password": "", "token": "", "game_password": "{{ factorio_game_password[0] }}", "require_user_verification": false, "max_upload_in_kilobytes_per_second": 0, "max_upload_slots": 5, "minimum_latency_in_ticks": 0, "max_heartbeats_per_second": 60, "ignore_player_limit_for_returning_players": false, "allow_commands": "admins-only", "autosave_interval": 10, "autosave_slots": 5, "afk_autokick_interval": 0, "auto_pause": true, "only_admins_can_pause_the_game": true, "autosave_only_on_server": true, "non_blocking_saving": true, "minimum_segment_size": 25, "minimum_segment_size_peer_count": 20, "maximum_segment_size": 100, "maximum_segment_size_peer_count": 10 } ``` Note the usage of `[0]` for the variable expansion: it is a disappointing trick that you have to remember when dealing with json query parsing using ansible's filters: these always return an array. The template invocation is: ``` yaml - name: 'Deploy configuration templates' template: src: 'server-settings.json' dest: '/srv/factorio/factorio/' owner: 'factorio' group: 'factorio' mode: '0440' notify: 'restart factorio' vars: factorio_game_password: "{{ tofu_state_legacy | json_query(\"resources[?type=='random_password'&&name=='factorio'].instances[0].attributes.result\") }}" ``` ### Service Finally I start and activate the factorio service on boot: ``` yaml - name: 'Start factorio and activate it on boot' service: name: 'factorio' enabled: 'yes' state: 'started' ``` ## Handlers I have these two handlers: ``` yaml --- - name: 'systemctl daemon-reload' shell: cmd: 'systemctl daemon-reload' - name: 'restart factorio' service: name: 'factorio' state: 'restarted' ``` ## Generating a map and starting the game If you just followed this guide factorio failed to start on the server because it does not have a map in its save folder. If that is not the case for you because you are coming back to this article after some time, remember to stop factorio with `systemctl stop factorio` before continuing. If you do not, when you later restart factorio will overwrite your newly uploaded save. Launch factorio locally, install any mod you want then go to single player and generate a new map with your chosen settings. Save the game then quit and go back to your terminal. Find the save file (if playing on steam it will be in `~/.factorio/saves/`) and upload it to `/srv/factorio/factorio/saves/game.zip`. If you are using mods, `rsync` the mods folder that leaves next to your saves directory to the server with: ``` shell rsync -r ~/.factorio/mods/ root@factorio.adyxax.org:/srv/factorio/factorio/mods/` ``` Then give these files to the factorio user on your server before restarting the game: ``` shell chown -R factorio:factorio /srv/factorio systemctl start factorio ``` ## Conclusion Good luck and have fun!