diff --git a/.sops.yaml b/.sops.yaml
new file mode 100644
index 0000000..ad56f8b
--- /dev/null
+++ b/.sops.yaml
@@ -0,0 +1,9 @@
+keys:
+  - &tlater 535B61015823443941C744DD12264F6BBDFABA89
+  - &server_tlaternet 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b
+
+creation_rules:
+  - key_groups:
+      - pgp:
+          - *tlater
+          - *server_tlaternet
diff --git a/configuration/default.nix b/configuration/default.nix
index 8600070..76a1bf7 100644
--- a/configuration/default.nix
+++ b/configuration/default.nix
@@ -6,6 +6,7 @@
     ./services/minecraft.nix
     ./services/nextcloud.nix
     ./services/webserver.nix
+    ./services/starbound.nix
     ./ids.nix
   ];
 
@@ -20,6 +21,14 @@
     trustedUsers = [ "@wheel" ];
   };
 
+  nixpkgs.config.allowUnfreePredicate = pkg:
+    builtins.elem (lib.getName pkg) ["forge-server" "steam-runtime" "steamcmd"];
+
+  sops = {
+    defaultSopsFile = ../keys/external.yaml;
+    secrets.steam = { };
+  };
+
   boot.kernelParams = [ "highres=off" "nohz=off" ];
 
   networking = {
@@ -29,7 +38,7 @@
     useDHCP = false;
     interfaces.eth0.useDHCP = true;
 
-    firewall.allowedTCPPorts = [ 80 443 2222 2221 25565 ];
+    firewall.allowedTCPPorts = [ 80 443 2222 2221 25565 21025 ];
   };
 
   time.timeZone = "Europe/London";
diff --git a/configuration/services/configs/starbound.json b/configuration/services/configs/starbound.json
new file mode 100644
index 0000000..d995fdf
--- /dev/null
+++ b/configuration/services/configs/starbound.json
@@ -0,0 +1,50 @@
+{
+  "allowAdminCommands" : true,
+  "allowAdminCommandsFromAnyone" : false,
+  "allowAnonymousConnections" : true,
+  "allowAssetsMismatch" : true,
+  "anonymousConnectionsAreAdmin" : false,
+  "bannedIPs" : [],
+  "bannedUuids" : [],
+  "checkAssetsDigest" : false,
+  "clearPlayerFiles" : false,
+  "clearUniverseFiles" : false,
+  "clientIPJoinable" : false,
+  "clientP2PJoinable" : true,
+  "configurationVersion" : {
+    "basic" : 2,
+    "server" : 4
+  },
+  "crafting" : {
+    "filterHaveMaterials" : false
+  },
+  "gameServerBind" : "::",
+  "gameServerPort" : 21025,
+  "interactiveHighlight" : true,
+  "inventory" : {
+    "pickupToActionBar" : true
+  },
+  "maxPlayers" : 8,
+  "maxTeamSize" : 4,
+  "monochromeLighting" : false,
+  "playerBackupFileCount" : 3,
+  "queryServerBind" : "::",
+  "queryServerPort" : 21025,
+  "rconServerBind" : "::",
+  "rconServerPassword" : "",
+  "rconServerPort" : 21026,
+  "rconServerTimeout" : 1000,
+  "runQueryServer" : false,
+  "runRconServer" : false,
+  "safeScripts" : true,
+  "scriptInstructionLimit" : 10000000,
+  "scriptInstructionMeasureInterval" : 10000,
+  "scriptProfilingEnabled" : false,
+  "scriptRecursionLimit" : 100,
+  "serverFidelity" : "automatic",
+  "serverName" : "tlater.net",
+  "serverOverrideAssetsDigest" : null,
+  "serverUsers" : {
+  },
+  "tutorialMessages" : true
+}
diff --git a/configuration/services/minecraft.nix b/configuration/services/minecraft.nix
index 23705ac..80b4ff5 100644
--- a/configuration/services/minecraft.nix
+++ b/configuration/services/minecraft.nix
@@ -51,9 +51,6 @@ let
   eula = pkgs.writeText "eula.txt" "eula=true";
 
 in {
-  nixpkgs.config.allowUnfreePredicate = pkg:
-    builtins.elem (lib.getName pkg) [ "forge-server" ];
-
   users = {
     extraUsers.minecraft = {
       uid = config.ids.uids.minecraft;
diff --git a/configuration/services/starbound.nix b/configuration/services/starbound.nix
new file mode 100644
index 0000000..e23028f
--- /dev/null
+++ b/configuration/services/starbound.nix
@@ -0,0 +1,110 @@
+{
+  pkgs,
+  lib,
+  ...
+}: let
+  inherit (lib) concatStringsSep;
+in {
+  systemd.services.starbound = {
+    description = "Starbound";
+    after = ["network.target"];
+
+    serviceConfig = {
+      ExecStart = "${pkgs.local.starbound}/bin/launch-starbound ${./configs/starbound.json}";
+
+      Type = "simple";
+
+      # Credential loading for steam auth (if necessary; prefer
+      # anonymous login wherever possible).
+      LoadCredential = "steam:/run/secrets/steam";
+
+      # Security settings
+      DynamicUser = true;
+
+      # This is where the StateDirectory ends up
+      WorkingDirectory = "/var/lib/starbound";
+      # Creates /var/lib/starbound (or rather, a symlink there to
+      # /var/lib/private/starbound), and sets it up to be writeable to
+      # by the dynamic user.
+      StateDirectory = "starbound";
+
+      # Note some settings below are basically tautologous with
+      # `NoNewPrivileges`, but they all work slightly differently so
+      # add additional layers in case of bugs.
+
+      ## THESE SETTINGS ARE A GOOD IDEA BUT THE STEAM CLIENT IS
+      ## REALLY, REALLY BAD, AND FOR SOME REASON I NEED TO USE IT TO
+      ## DOWNLOAD GAME SERVERS AS WELL:
+      ##
+      # To guarantee the above (only permits 64-bit syscalls, 32-bit
+      # syscalls can circumvent the above restrictions).
+      #
+      # Obviously, if running a 32 bit game server, change this.
+      # SystemCallArchitectures = "native";
+      # Game servers shouldn't need to create new namespaces ever.
+      #
+      # TODO: Since steam uses namespaces for things *entirely
+      # unrelated* to installing game servers, we need to allow
+      # namespace access. Ideally I'd instead do this in an
+      # ExecStartPre, but alas, this isn't possible because of
+      # https://github.com/systemd/systemd/issues/19604.
+      #
+      # RestrictNamespaces = true;
+
+      # Don't need to let the game server see other user accounts
+      PrivateUsers = true;
+      # *Probably* not harmful for game servers, which probably don't update dynamically
+      ProtectHostname = true;
+      # Yeah, if a game server tries to edit the hardware clock something's fishy
+      ProtectClock = true;
+      # Don't let game servers modify kernel settings, duh
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectKernelLogs = true;
+      # Game servers shouldn't use cgroups themselves either
+      ProtectControlGroups = true;
+      # Most game servers will never need other socket types
+      RestrictAddressFamilies = ["AF_UNIX AF_INET AF_INET6"];
+      # Also a no-brainer, no game server should ever need this
+      LockPersonality = true;
+      # Some game servers will probably try to set this, but they
+      # don't need it. It's only required for audio processing and
+      # such, which the server end doesn't need to do.
+      RestrictRealtime = true;
+      # Don't allow a variety of syscalls that gameservers have no
+      # business using anyway
+      SystemCallFilter =
+        "~"
+        + (concatStringsSep " " [
+          "@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@keyring"
+          "@memlock"
+          "@module"
+          # "@mount"  TODO: Consider adding when steamcmd is run in ExecStartPre
+          "@obsolete"
+          "@raw-io"
+          "@reboot"
+          "@resources"
+          "@setuid"
+          "@swap"
+        ]);
+      # Normally only "read-only", but steamcmd will puke if there is
+      # no home directory to write to (though the nix package will
+      # implicitly symlink to the path that we set in its override, so
+      # no actual files are created, besides a symlink).
+      ProtectHome = "tmpfs";
+
+      # Implied by DynamicUser anyway, but it doesn't hurt to add
+      # these explicitly, at least for reference.
+      RemoveIPC = true;
+      PrivateTmp = true;
+      PrivateDevices = true;
+      NoNewPrivileges = true;
+      RestrictSUIDSGID = true;
+      ProtectSystem = "strict";
+      # ProtectHome = "read-only"; # See further up
+    };
+  };
+}
diff --git a/flake.lock b/flake.lock
index 7a563d8..5c75d61 100644
--- a/flake.lock
+++ b/flake.lock
@@ -73,6 +73,7 @@
         "flake-utils": "flake-utils",
         "nixos-hardware": "nixos-hardware",
         "nixpkgs": "nixpkgs",
+        "sops-nix": "sops-nix",
         "tlaternet-templates": "tlaternet-templates",
         "tlaternet-webserver": "tlaternet-webserver"
       }
@@ -102,6 +103,26 @@
         "type": "github"
       }
     },
+    "sops-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1649756291,
+        "narHash": "sha256-KTll8bCINAzIUGaaMrbn9wb5nfhkXRLgmFrWGR/Dku0=",
+        "owner": "Mic92",
+        "repo": "sops-nix",
+        "rev": "c2614c4fe61943b3d280ac1892fcebe6e8eaf8c8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "Mic92",
+        "repo": "sops-nix",
+        "type": "github"
+      }
+    },
     "tlaternet-templates": {
       "inputs": {
         "flake-utils": [
diff --git a/flake.nix b/flake.nix
index 82ce5b7..772580b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -5,6 +5,11 @@
     nixpkgs.url = "github:nixos/nixpkgs/nixos-21.11";
     nixos-hardware.url = "github:nixos/nixos-hardware/master";
     flake-utils.url = "github:numtide/flake-utils";
+    sops-nix = {
+      url = "github:Mic92/sops-nix";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+
     tlaternet-webserver = {
       url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git";
       inputs = {
@@ -21,8 +26,8 @@
     };
   };
 
-  outputs = { self, nixpkgs, nixos-hardware, flake-utils, tlaternet-webserver
-    , tlaternet-templates, ... }@inputs:
+  outputs = { self, nixpkgs, nixos-hardware, flake-utils, sops-nix
+    , tlaternet-webserver, tlaternet-templates, ... }@inputs:
     let
       overlays = [
         (final: prev: {
@@ -53,6 +58,7 @@
             (import ./configuration)
             (import ./configuration/linode.nix)
             (import ./configuration/hardware-configuration.nix)
+            sops-nix.nixosModules.sops
           ];
         };
 
@@ -68,6 +74,7 @@
             (import ./modules)
 
             (import ./configuration)
+            sops-nix.nixosModules.sops
             ({ lib, ... }: {
               users.users.tlater.password = "insecure";
 
@@ -86,13 +93,20 @@
         };
       };
     } // flake-utils.lib.eachDefaultSystem (system:
-      let pkgs = import nixpkgs { inherit system overlays; };
+      let
+        pkgs = import nixpkgs { inherit system overlays; };
+        sops-pkgs = sops-nix.packages.${system};
       in {
-        devShell = with pkgs;
-          mkShell {
-            buildInputs = [
+        devShell =
+          pkgs.mkShell {
+            sopsPGPKeyDirs = ["./keys/hosts/" "./keys/users/"];
+            nativeBuildInputs = with sops-pkgs; [
+              sops-import-keys-hook
+            ];
+            buildInputs = with pkgs; with sops-pkgs; [
               nixfmt
               git-lfs
+              sops-init-gpg-key
 
               # For the minecraft mod update script
               (python3.withPackages (pypkgs:
@@ -116,9 +130,26 @@
                     ])
                 ]))
             ];
-            shellHook = ''
+            shellHook = let
+              inherit (pkgs.lib.attrsets) mapAttrsToList;
+              inherit (pkgs.lib.strings) concatStringsSep;
+              ports = {
+                "3022" = "2222";
+                "3080" = "80";
+                "3443" = "443";
+                "3021" = "2221";
+                "25565" = "25565";
+                "21025" = "21025"; # Starbound
+              };
+              QEMU_NET_OPTS =
+                concatStringsSep ","
+                  (mapAttrsToList
+                    (host: vm: "hostfwd=::${host}-:${vm}")
+                    ports);
+            in
+              ''
               export QEMU_OPTS="-m 3941 -smp 2 -curses"
-              export QEMU_NET_OPTS="hostfwd=::3022-:2222,hostfwd=::3080-:80,hostfwd=::3443-:443,hostfwd=::3021-:2221,hostfwd=::25565-:25565"
+              export QEMU_NET_OPTS="${QEMU_NET_OPTS}"
 
               # Work around sudo requiring a full terminal
               export NIX_SSHOPTS="-t"
diff --git a/keys/external.yaml b/keys/external.yaml
new file mode 100644
index 0000000..c51d957
--- /dev/null
+++ b/keys/external.yaml
@@ -0,0 +1,52 @@
+steam: ENC[AES256_GCM,data:Jhk91uP3Ixo7H4I9kXEWeA==,iv:s8BcwGNF1vG8KI41FmQXOBbqZl8SnMZ9+YP6GKwHdtY=,tag:dW462jNJCtG4HWrkeQTUzw==,type:str]
+sops:
+    kms: []
+    gcp_kms: []
+    azure_kv: []
+    hc_vault: []
+    age: []
+    lastmodified: "2022-04-23T07:07:21Z"
+    mac: ENC[AES256_GCM,data:E9z+h2kejAobo3wIRBQFGyMfMHoeVREE+pEF9XoVZF8cQM4xC4tHub+eENsBIDeWoIcPtugLE9Xwzn9odyg92Vri/SzcaxrEXzsAcvFj6Ox2cN27h17OrkQBMKeA/tnMVg+uJxQesWZbrfcMsmd99X1W1RH5SMUwNrqjCsNxZ7s=,iv:pfi/EXgacNapdVlKP0UEMKdxi7s4YicfFcSopvwOrNA=,tag:RmMPwBYFZx2M5FJCVyhcLg==,type:str]
+    pgp:
+        - created_at: "2022-04-23T07:07:08Z"
+          enc: |
+            -----BEGIN PGP MESSAGE-----
+
+            hQIMAzWu0p84AOApAQ/8DeQLvWBjQn3mNfmiPyH0NNj0d70FKbm546jFjBuVHW9h
+            P3KWVJF8pWdg17W5Hlu8xDPCCYmX5Rew3JznbEpyxIMUAUPS+HpwEWXvpKUMNhIj
+            VDcQ5cVkfiOc81gRKXLiWNmBP2lRKrjjmFBEbwZgHxW2Y9yzmzqsR97VWrBhkz9r
+            Rsif8Mi29LAbpG0lgVZSji7pzs/4EbclSQsfv6JSJMoA0bD3OdtyAmJh4dfUV31i
+            kzkOJ8WIAwYpvKXI4Jf3DuUS7njzdw4SRCgf5nuC8Ml1Kb4IvCwTsPEOaYRhCZIX
+            jUTmQ4DwiIZ+618Wzi6SHgdH1QZaS5e71n0rxPxsYY6UbCyDhrGNcXgn/p0DHP04
+            p+Hscl75IqJiMzlAnQobx111vw1f3oGgTuWYS78Tccpy/QgMtVf99CeVwp4fVkeU
+            iPGr5oy5KO3WF3EWvuK/A/eoiK208YRMcL+0hrFDuhTB7qnyCRBjTv+4SJdY3FiC
+            KA/syZ/+DioUVyEXNn8cttk0U9Wf5zub7s/9Ei7MQVVUgCvyZZDHFE+50d5UeVRm
+            WW5T38D3G+v6py8gkC4/noKndr6SzRgPBAVW/Ba9CZZtEulhA39U9M4Q9cCSyLCM
+            nAiu0ikOiDif4w+1fxeEA+BXp7uBbW9vz09jetfDkp+i4hvt41a9dwJpTmxWm57S
+            XgFQDuO+HdFs44Yw1gyJQKLK3YejSyoKIo5pN36yMGuYPw9B75Cx+MWJJihL5IHl
+            gTqNPiD3cIyFw9U+FYfvqdQz9Vo/dD8gF0G5Y5MVH0E9xNsSFuSWoA9H2Zl+Ops=
+            =W5PZ
+            -----END PGP MESSAGE-----
+          fp: 535B61015823443941C744DD12264F6BBDFABA89
+        - created_at: "2022-04-23T07:07:08Z"
+          enc: |
+            -----BEGIN PGP MESSAGE-----
+
+            hQIMA9ahl2ynTH87ARAAwKoe5UHRlt86d47oCSvXVV0JvRdw6K5VKDIyZr5FEAZD
+            VHDvZb7a0qlKuJsKiq5+3hZPkw9W9DLtpacxQdEoTUDQVAfVAi1pIKaJIYP7pSC3
+            LbcqNXucqJoKLKXHE0zCPAJdbTYOo804Z6iLRJRYwuLHfE17lVPMoJD9OjwZn1LS
+            ksJnki68YqQcFocRuUF/pq+bcd0ceV7YQiBIdO/89YyVPgviORXzlRZ5+hTpFjqd
+            rYFzgNBkHVRHUso8S9YNiFsU8dKVXBDS6LiykcKe9C6kEYDCe24VVHyrlj2s3isH
+            68PefnfQY5ZIBSDm2uwGmym1pSwhR015Ely/gqRx+T4qfCcAwva9ZPusXHzHu2+z
+            vwO83yeBXFbs4YFdHhh9GNbOfbBKCdPU0FAqQCuU0P7iVrCfjtTXdg622hn7x2YW
+            YwH5nlcm14/U39jOzWNketmyAiaOJ32ko3Ec63ELkGCb8+LR0eCPDNUNCQcnKGaU
+            yrTYnqUG6ODxdkhvq1JCsPKlZ/0pqiSQKBE1dGZBrcshZZ6SvdhO6hSimg3m+Mqn
+            wh5pcx/P5k3VXPovTRkRAU/U6RdYKs+qH1tXCBSMwjz4EU9VCYZbqLb1Bp/NfsPa
+            0CL50HA617ID0ofxOL8eT49xQJZZFFAArOMZmRGgsKrPAEYijnQ/593nci4DdozS
+            UAFrki95hH3EFygU3KkVdpULaOwc/0SWIPLZgmpFq2H5bKOqi9TL0IV3G00qEf5G
+            bOFb1q7pCAWMe5b6JH7MQRmSWwaoE4qhtmOkjHMOuqhA
+            =hTbV
+            -----END PGP MESSAGE-----
+          fp: 8a3737d48f1035fe6c3a0a8fd6a1976ca74c7f3b
+    unencrypted_suffix: _unencrypted
+    version: 3.7.2
diff --git a/keys/hosts/tlaternet.asc b/keys/hosts/tlaternet.asc
new file mode 100644
index 0000000..5b03dea
--- /dev/null
+++ b/keys/hosts/tlaternet.asc
@@ -0,0 +1,28 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsFNBAAAAAABEADkGxU+/uxtkVsX9+Wycp6ikG9177NvCsXqROeuIIJ5nDWD+jrt
+jlO1ngL9vvOfE4mGNY839r86cbSocK2WCirVnPdh8K5o9X+YptA7yHoe8RHn0fZC
+wM1eGSDTMmtGvP00IXoMpktzU+GcrO/1XMFIshj/2KboUGYqRpqhfiNQXPcYlz4J
+QRXsPzPnAphSn9ausYb6f8qYn+ivSo9jKDHZL4FJJ2OkYsoo3sX8lMWCE3ON48QF
+9K4UU0Ru86/1sBW55AT5IzVTF7TKWAjnHi8v9Lzd7lVJy8ZLe1bioP75wCl3aE1N
+owE/H4hPKfYTjKHdNkU4c4HjsATU1Ak9hJXj7zMeI/J7JF+wD7ztYo4RZWZTPPNP
+k1ELmCXMNQp3Dujy54kXLi+d5ii4FK/q/G1n1ZdMjjEhem7LEpZXq8tNsHnzfnlK
+QDazAQvGb3Gnv4OuMe4CvvMNBNRiooikCejsBYswW9QkOguyivQHDx2UMqUVU8L0
+HCBzVi9JQKy2Dn1gKP9SDvotYZmS/VVe++wx+EkIIty3ecl1HpsxtqQgNaUewJiM
+NnBBgnwzKigqCqCUDjg+8KKh6AENtAeGa+LFZWw2pZ6ZiwcqjDIu0zoQVesCuF/Z
+zktNVPjIMTH2Cg4PTKccHIROHRjFJjn1fIAmQPoh3QjDHrUMSbtaTbfWIQARAQAB
+zSlyb290IChJbXBvcnRlZCBmcm9tIFNTSCkgPHJvb3RAbG9jYWxob3N0PsLBYgQT
+AQgAFgUCAAAAAAkQ1qGXbKdMfzsCGw8CGQEAABDNEAAjEKhsm0ukNqHQZzmIlYRL
+6ybTmeGOYczKlTM0h5EDTMSvPn89WuWPZCL/oyIOE+TKqkjEsBc4ej2YmxdHSS/f
+4WLOLqWre6Krs1qZ/eTBLU0LOwEtYYt8x44WN5K1GyyVyZAkGdIYO5NaQPP86m/H
+QzyZ+YRQvcqlUgNUmRIsq2F+X7kRKl9X+6zZDsM72aAEvOlWss/sn3Dca2omxaAB
+/b32P0r1ppCPXLk0QRbZM+D38CUaZIKWhKKvaifKaRhY1eTPv1OG8uFGrkYcxB9m
+GkOZh4Kbz4+Es5uM5uYSxNWWmqU5FX/D8qEzsn7OB+WH1wFLwp75oR8shy8FQ3B0
++JVBwJEmg+kBoYZHPjVHxp/z2h4drTQ/h2uulplWUuCpH8dLjCy3dHukEH1tw803
+34fCUxN5RTyOG3uGC/8cXZ7Xj5RnX0dso6g4/0kmy42S2EAoxB+L1CmuI02SBxCK
+9go7oyP9sUBQ59BA8W5Ab8a5+CpB/kWthGErtMcq56bNqA7S9+UgyqOJze4mqBVs
+qBFjYK1/aYbKIarXuw6cOuCZfXQIRex7IDVdW1ikKs4h3g2h0wBgnY+xOaFNUQxa
+I7gG4e+sBnlvlCyoyzstE/JjAQr9QC9qj30UB3iankyDZD/F1MfEx4HRm7f+V/4P
+PoGnDfqmEEn9P7j7sOJ0iw==
+=z4j4
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/keys/users/tlater.asc b/keys/users/tlater.asc
new file mode 100644
index 0000000..d9c50dd
--- /dev/null
+++ b/keys/users/tlater.asc
@@ -0,0 +1,72 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+PhCB2Dz76iQ5nDY4MQ==
+=R7Pm
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 88a0112..dd334bc 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -13,4 +13,7 @@ in {
   # Minecraft modpacks
   voor-kia = callPackage ./minecraft/voor-kia.nix { };
   voor-kia-client = callPackage ./minecraft/voor-kia-client.nix { };
+
+  # Starbound
+  starbound = callPackage ./starbound { };
 }
diff --git a/pkgs/starbound/default.nix b/pkgs/starbound/default.nix
new file mode 100644
index 0000000..304f0f3
--- /dev/null
+++ b/pkgs/starbound/default.nix
@@ -0,0 +1,34 @@
+{
+  stdenv,
+  lib,
+  makeWrapper,
+  patchelf,
+  steamPackages,
+  replace-secret,
+}: let
+  # Use the directory in which starbound is installed so steamcmd
+  # doesn't have to be reinstalled constantly (we're using DynamicUser
+  # with StateDirectory to persist this).
+  steamcmd = steamPackages.steamcmd.override {
+    steamRoot = "/var/lib/starbound/.steamcmd";
+  };
+  wrapperPath = lib.makeBinPath [patchelf steamcmd replace-secret];
+in
+  stdenv.mkDerivation {
+    name = "starbound-update-script";
+    nativeBuildInputs = [makeWrapper];
+    dontUnpack = true;
+    patchPhase = ''
+      interpreter="$(cat $NIX_CC/nix-support/dynamic-linker)"
+      substitute ${./launch-starbound.sh} launch-starbound --subst-var interpreter
+    '';
+    installPhase = ''
+      mkdir -p $out/bin
+      cp launch-starbound $out/bin/launch-starbound
+      chmod +x $out/bin/launch-starbound
+    '';
+    postFixup = ''
+      wrapProgram $out/bin/launch-starbound \
+          --prefix PATH : "${wrapperPath}"
+    '';
+  }
diff --git a/pkgs/starbound/launch-starbound.sh b/pkgs/starbound/launch-starbound.sh
new file mode 100644
index 0000000..24d4db1
--- /dev/null
+++ b/pkgs/starbound/launch-starbound.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+set -eu
+
+if ! [[ -v STATE_DIRECTORY  &&  -v CREDENTIALS_DIRECTORY ]]; then
+    echo "Error: Runtime dir or credential not set"
+    exit 1
+fi
+
+# Update the server to the latest version
+echo "Updating/installing starbound"
+
+mkdir -p "${STATE_DIRECTORY}/.steamcmd"
+steamcmd <<EOF
+force_install_dir $STATE_DIRECTORY
+login tlater $(cat "$CREDENTIALS_DIRECTORY/steam")
+app_update 211820
+quit
+EOF
+
+echo "Updating config"
+if [ -f "$1" ]; then
+    mkdir -p ./storage
+    cp "$1" ./storage/starbound_server.config
+fi
+
+echo "Running starbound server"
+patchelf --set-interpreter '@interpreter@' ./linux/starbound_server
+# Must be run from the directory that the binary is in (why do game
+# devs do this?)
+cd linux
+./starbound_server