1 { config, lib, pkgs, ... }:
3 cfg = config.services.factorio;
5 stateDir = "/var/lib/${cfg.stateDirName}";
6 mkSavePath = name: "${stateDir}/saves/${name}.zip";
7 configFile = pkgs.writeText "factorio.conf" ''
8 use-system-read-write-data-directories=true
10 read-data=${cfg.package}/share/factorio/data
11 write-data=${stateDir}
15 description = cfg.description;
20 username = cfg.username;
21 password = cfg.password;
23 game_password = cfg.game-password;
24 require_user_verification = cfg.requireUserVerification;
25 max_upload_in_kilobytes_per_second = 0;
26 minimum_latency_in_ticks = 0;
27 ignore_player_limit_for_returning_players = false;
28 allow_commands = "admins-only";
29 autosave_interval = cfg.autosave-interval;
31 afk_autokick_interval = 0;
33 only_admins_can_pause_the_game = true;
34 autosave_only_on_server = true;
35 non_blocking_saving = cfg.nonBlockingSaving;
36 } // cfg.extraSettings;
37 serverSettingsString = builtins.toJSON (lib.filterAttrsRecursive (n: v: v != null) serverSettings);
38 serverSettingsFile = pkgs.writeText "server-settings.json" serverSettingsString;
39 serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins);
40 modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods cfg.mods-dat;
45 enable = lib.mkEnableOption name;
47 type = lib.types.port;
50 The port to which the service should bind.
58 The address to which the service should bind.
62 admins = lib.mkOption {
63 type = lib.types.listOf lib.types.str;
65 example = [ "username" ];
67 List of player names which will be admin.
71 openFirewall = lib.mkOption {
72 type = lib.types.bool;
75 Whether to automatically open the specified UDP port in the firewall.
78 saveName = lib.mkOption {
82 The name of the savegame that will be used by the server.
84 When not present in /var/lib/''${config.services.factorio.stateDirName}/saves,
85 a new map with default settings will be generated before starting the service.
88 loadLatestSave = lib.mkOption {
89 type = lib.types.bool;
92 Load the latest savegame on startup. This overrides saveName, in that the latest
93 save will always be used even if a saved game of the given name exists. It still
94 controls the 'canonical' name of the savegame.
96 Set this to true to have the server automatically reload a recent autosave after
100 # TODO Add more individual settings as nixos-options?
101 # TODO XXX The server tries to copy a newly created config file over the old one
102 # on shutdown, but fails, because it's in the nix store. When is this needed?
103 # Can an admin set options in-game and expect to have them persisted?
104 configFile = lib.mkOption {
105 type = lib.types.path;
106 default = configFile;
107 defaultText = lib.literalExpression "configFile";
109 The server's configuration file.
111 The default file generated by this module contains lines essential to
112 the server's operation. Use its contents as a basis for any
116 extraSettingsFile = lib.mkOption {
117 type = lib.types.nullOr lib.types.path;
120 File, which is dynamically applied to server-settings.json before
123 This option should be used for credentials.
125 For example a settings file could contain:
128 "game-password": "hunter1"
133 stateDirName = lib.mkOption {
134 type = lib.types.str;
135 default = "factorio";
137 Name of the directory under /var/lib holding the server's data.
139 The configuration and map will be stored here.
142 mods = lib.mkOption {
143 type = lib.types.listOf lib.types.package;
146 Mods the server should install and activate.
148 The derivations in this list must "build" the mod by simply copying
149 the .zip, named correctly, into the output directory. Eventually,
150 there will be a way to pull in the most up-to-date list of
151 derivations via nixos-channel. Until then, this is for experts only.
154 mods-dat = lib.mkOption {
155 type = lib.types.nullOr lib.types.path;
158 Mods settings can be changed by specifying a dat file, in the [mod
160 format](https://wiki.factorio.com/Mod_settings_file_format).
163 game-name = lib.mkOption {
164 type = lib.types.nullOr lib.types.str;
165 default = "Factorio Game";
167 Name of the game as it will appear in the game listing.
170 description = lib.mkOption {
171 type = lib.types.nullOr lib.types.str;
174 Description of the game that will appear in the listing.
177 extraSettings = lib.mkOption {
178 type = lib.types.attrs;
180 example = { admins = [ "username" ];};
182 Extra game configuration that will go into server-settings.json
185 public = lib.mkOption {
186 type = lib.types.bool;
189 Game will be published on the official Factorio matching server.
193 type = lib.types.bool;
196 Game will be broadcast on LAN.
199 username = lib.mkOption {
200 type = lib.types.nullOr lib.types.str;
203 Your factorio.com login credentials. Required for games with visibility public.
205 This option is insecure. Use extraSettingsFile instead.
208 package = lib.mkPackageOption pkgs "factorio-headless" {
209 example = "factorio-headless-experimental";
211 password = lib.mkOption {
212 type = lib.types.nullOr lib.types.str;
215 Your factorio.com login credentials. Required for games with visibility public.
217 This option is insecure. Use extraSettingsFile instead.
220 token = lib.mkOption {
221 type = lib.types.nullOr lib.types.str;
224 Authentication token. May be used instead of 'password' above.
227 game-password = lib.mkOption {
228 type = lib.types.nullOr lib.types.str;
233 This option is insecure. Use extraSettingsFile instead.
236 requireUserVerification = lib.mkOption {
237 type = lib.types.bool;
240 When set to true, the server will only allow clients that have a valid factorio.com account.
243 autosave-interval = lib.mkOption {
244 type = lib.types.nullOr lib.types.int;
248 Autosave interval in minutes.
251 nonBlockingSaving = lib.mkOption {
252 type = lib.types.bool;
255 Highly experimental feature, enable only at your own risk of losing your saves.
256 On UNIX systems, server will fork itself to create an autosave.
257 Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
263 config = lib.mkIf cfg.enable {
264 systemd.services.factorio = {
265 description = "Factorio headless server";
266 wantedBy = [ "multi-user.target" ];
267 after = [ "network.target" ];
271 "test -e ${stateDir}/saves/${cfg.saveName}.zip"
273 "${cfg.package}/bin/factorio"
274 "--config=${cfg.configFile}"
275 "--create=${mkSavePath cfg.saveName}"
276 (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
278 + (lib.optionalString (cfg.extraSettingsFile != null) ("\necho ${lib.strings.escapeShellArg serverSettingsString}"
279 + " \"$(cat ${cfg.extraSettingsFile})\" | ${lib.getExe pkgs.jq} -s add"
280 + " > ${stateDir}/server-settings.json"));
284 KillSignal = "SIGINT";
286 StateDirectory = cfg.stateDirName;
288 ExecStart = toString [
289 "${cfg.package}/bin/factorio"
290 "--config=${cfg.configFile}"
291 "--port=${toString cfg.port}"
293 (lib.optionalString (!cfg.loadLatestSave) "--start-server=${mkSavePath cfg.saveName}")
294 "--server-settings=${
295 if (cfg.extraSettingsFile != null)
296 then "${stateDir}/server-settings.json"
297 else serverSettingsFile
299 (lib.optionalString cfg.loadLatestSave "--start-server-load-latest")
300 (lib.optionalString (cfg.mods != []) "--mod-directory=${modDir}")
301 (lib.optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
305 NoNewPrivileges = true;
307 PrivateDevices = true;
308 ProtectSystem = "strict";
310 ProtectControlGroups = true;
311 ProtectKernelModules = true;
312 ProtectKernelTunables = true;
313 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
314 RestrictRealtime = true;
315 RestrictNamespaces = true;
316 MemoryDenyWriteExecute = true;
320 networking.firewall.allowedUDPPorts = lib.optional cfg.openFirewall cfg.port;