Correct way to run a shell script with systemd service

Hi,

I am on NixOS 24.11.

I am trying to piece together the right way to run a shell script with systemd. For context, I am trying to run a Minecraft server using a systemd unit, but I can’t seem to find a clear answer on running user scripts in general.

On my Debian system, I can pretty easily set up a systemd unit that starts the server on startup and closes it on stop, pretty much just like this. Essentially what you do is write a shell script that cd’s into the server directory, and then runs the server .jar file.

#!/bin/bash
cd /some/server/directory
java -jar server.jar

Usually, I throw this script in /usr/local/bin and pass it to ExecStart in the systemd unit. I usually would have a second script in /usr/local/bin that sends a stop signal to the server, and pass that to ExecStop.

On my NixOS server, I tried to create an equivalent systemd service following this wiki entry (Extend NixOS). Because of this discourse, I thought that I should try just moving the scripts to a directory in my user home, so I decided to move them into the Minecraft server directory that contains the .jar file directly. I then pretty much just copied the unit described here into a dedicated service file that looked something like this:

# server.nix
{config, pkgs, lib, ...}:
 
 let
   cfg = config.services.my_minecraft_server;
 in
 
 with lib;
 
 {
   options = {
     services.my_minecraft_server= {
       enable = mkOption {
         default = false;
         type = with types; bool;
       };
     };
   };
 
   config = mkIf cfg.enable {
     systemd.services.my_minecraft_server= {
       wantedBy = [ "default.target" ]; 
       after = [ "network.target" ];
       description = "Start the minecraft server";
       serviceConfig = {
         Type = "simple";
         ExecStart = ''/home/user/minecraft_server/minecraft-server-start'';         
         ExecStop = ''/home/user/minecraft_server/minecraft-server-stop'';
         Restart="always"
       }; 
     };
     environment.systemPackages = [ mcrcon jdk ];
   };
 }

I added the service into my configuration.nix like

imports = [
  ./server.nix
];
...
services.my_minecraft_server.enable = true;
...

On rebuild, the service gets picked up properly, but it fails and exits. The journal says that systemd isn’t finding /home/user/minecraft_server/minecraft-server-start. I am not the biggest expert on systemd, or how it runs on NixOS as opposed to another distro, but I guessed that there’s a reason why you would put something in /usr/local/bin as opposed to anywhere else. I had to create /usr/local/bin and moved the startup scripts to those location. Here, the service failed due to permission denied, so I gave rwx to all users with chmod 777 minecraft-server-start. Here, the service failed because it couldnt’ find the java exectuable, even with both jdk and jre_minimal in the environment.systemPackages list in the unit file. Looking back at this discourse, it seems like the idiomatic thing to do is to write a nix derivation that evaluates? to the shell script, making it globally available on the path. However, I can’t find a straightforward way to do this and install it on my NixOS system. I tried following along with Nix Pill 5-8, but I am confused on how to take the derivation built there and install it on the system. I tried literally just copy pasting the External builder.sh script from this wiki (Shell Scripts), but nix-build fails and nix build seems to expect a flake (why these both exist, I am not sure). I was finally able to get a super simple derivation working, like this:

# default.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
  name = "hello-nix";
  src = ./src;
  buildPhase = "";
  installPhase = ''
    mkdir -p $out/bin
    cp $src/hello-nix.sh $out/bin
  '';
}
# ./src/hello-nix.sh
#!/bin/bash
echo "this is working"

If I nix-build in this directory, it correctly copies the shell script to result/bin. However, I can’t get even this to work on my system, either by declaring hello-drv = pkgs.callPackage ./hello-nix/efault.nix { inherit pkgs; } before my config and then having hello-drv in my systemPackages list, or by adding ./hello-derivation/default.nix to my imports, or any combination of the above.

I’m kind of at a loss here. I don’t know what the correct way to do what I’m trying to do is, and various documentation and discourse points me in contradictory directions. I did also take a look at the source for the actual minecraft_server service in nixpkgs, but all they do is run pkgs.minecraft_server on ExecStart, not call any kind of user script.

Any help here would be appreciated. I don’t know if my approach is wrong, but this seems like it should be a pretty trivial thing to do. Thanks.

Pills are useless, forget them.
Use something like writeShellApplication and ensure you provide full paths (via interpolation) to all binaries, because systemd services will ignore envvars by (default) design.

1 Like

Is there a resource like pills that are more modern or have the current way of doing things? I do really want to learn this stuff with more depth but it’s hard to figure out what I even need to know…

writeShellApplication looks like what I need, but where would this function actually run? Would this be an expression in it’s own file that gets imported into my configuration? If I wrapped my start and stop shell scripts in writeShellApplication, could I then just use them globally in the systemd unit, or is there a way to explicitly make the systemd unit build/depend on NixOS evaluating writeShellApplication on the scripts. Thanks for the quick response.

Closest thing is nix.dev.

systemd.services.FOO.serviceConfig.ExecStart =
  let
    BAR = pkgs.writeShellApplication { ... };
  in
    lib.getExe BAR;

(The let is just for readability.)

Rest of the config is up to your use case.

Thank you for this, worked like a charm. Not sure how I never saw the tutorials on nix.dev, I am going to start working through those.

Sorry for bumping a solved thread but as @waffle8946 mentioned, writeShell* is the way to go. Usually, I prefer writeShellScript since you don’t need to do anything extra. Here’s an example of how I do it: nixos-configuration/modules/services/custom-home-manager-upgrade.nix · 2cd622ccedbbb0e167fb769d0ee88d6e5a0d073a · Pratham Patel / prathams-nixos · GitLab

2 Likes