diff --git a/configuration/default.nix b/configuration/default.nix
index 8f51f0b..cc51bac 100644
--- a/configuration/default.nix
+++ b/configuration/default.nix
@@ -1,9 +1,13 @@
 {
   pkgs,
   lib,
+  modulesPath,
   ...
 }: {
   imports = [
+    "${modulesPath}/profiles/headless.nix"
+    (import ../modules)
+
     ./services/gitea.nix
     ./services/nextcloud.nix
     ./services/webserver.nix
@@ -12,6 +16,14 @@
     ./sops.nix
   ];
 
+  nixpkgs.overlays = [
+    (final: prev: {
+      local = import ../pkgs {
+        pkgs = prev;
+      };
+    })
+  ];
+
   nix = {
     package = pkgs.nixFlakes;
     extraOptions = ''
diff --git a/configuration/linode.nix b/configuration/hardware-specific/linode/default.nix
similarity index 100%
rename from configuration/linode.nix
rename to configuration/hardware-specific/linode/default.nix
diff --git a/configuration/hardware-configuration.nix b/configuration/hardware-specific/linode/hardware-configuration.nix
similarity index 100%
rename from configuration/hardware-configuration.nix
rename to configuration/hardware-specific/linode/hardware-configuration.nix
diff --git a/configuration/hardware-specific/vm.nix b/configuration/hardware-specific/vm.nix
new file mode 100644
index 0000000..aed39e4
--- /dev/null
+++ b/configuration/hardware-specific/vm.nix
@@ -0,0 +1,17 @@
+{lib, ...}: {
+  users.users.tlater.password = "insecure";
+
+  # Disable graphical tty so -curses works
+  boot.kernelParams = ["nomodeset"];
+
+  # Sets the base domain for nginx to localhost so that we
+  # can easily test locally with the VM.
+  services.nginx.domain = lib.mkOverride 99 "localhost";
+
+  # Use the staging secrets
+  sops.defaultSopsFile = lib.mkOverride 99 ../../keys/staging.yaml;
+
+  # # Set up VM settings to match real VPS
+  # virtualisation.memorySize = 3941;
+  # virtualisation.cores = 2;
+}
diff --git a/flake.lock b/flake.lock
index 11b5c2c..c44d4a0 100644
--- a/flake.lock
+++ b/flake.lock
@@ -190,22 +190,6 @@
         "type": "indirect"
       }
     },
-    "nixos-hardware": {
-      "locked": {
-        "lastModified": 1665321371,
-        "narHash": "sha256-0SO6MTW0bX6lxZmz1AZW/Xmk+hnTd7/hp1vF7Tp7jg0=",
-        "owner": "nixos",
-        "repo": "nixos-hardware",
-        "rev": "236ba4df714131059945d7754c0aa3fbe9d2f74c",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nixos",
-        "ref": "master",
-        "repo": "nixos-hardware",
-        "type": "github"
-      }
-    },
     "nixpkgs": {
       "locked": {
         "lastModified": 1665466769,
@@ -299,7 +283,6 @@
     },
     "root": {
       "inputs": {
-        "nixos-hardware": "nixos-hardware",
         "nixpkgs": "nixpkgs",
         "sops-nix": "sops-nix",
         "tlaternet-webserver": "tlaternet-webserver"
diff --git a/flake.nix b/flake.nix
index 5603e08..a67b314 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,12 +3,10 @@
 
   inputs = {
     nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
-    nixos-hardware.url = "github:nixos/nixos-hardware/master";
     sops-nix = {
       url = "github:Mic92/sops-nix";
       inputs.nixpkgs.follows = "nixpkgs";
     };
-
     tlaternet-webserver = {
       url = "git+https://gitea.tlater.net/tlaternet/tlaternet.git";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -18,118 +16,97 @@
   outputs = {
     self,
     nixpkgs,
-    nixos-hardware,
     sops-nix,
     tlaternet-webserver,
   }: let
     system = "x86_64-linux";
-
-    overlays = [
-      (final: prev: {
-        local = import ./pkgs {
-          pkgs = prev;
-        };
-      })
-    ];
-
-    pkgs = import nixpkgs {inherit system overlays;};
-    sops-pkgs = sops-nix.packages.${system};
   in {
-    nixosConfigurations = {
-      tlaternet = nixpkgs.lib.nixosSystem {
-        inherit system;
-
-        modules = [
-          ({modulesPath, ...}: {
-            imports = [(modulesPath + "/profiles/headless.nix")];
-            nixpkgs.overlays = overlays;
-          })
-          (import ./modules)
-
-          (import ./configuration)
-          (import ./configuration/linode.nix)
-          (import ./configuration/hardware-configuration.nix)
+    ##################
+    # Configurations #
+    ##################
+    nixosConfigurations = let
+      # Modules that should be generic to all systems
+      genericModule = {...}: {
+        imports = [
+          # Inject flake dependencies
           sops-nix.nixosModules.sops
           tlaternet-webserver.nixosModules.default
+
+          # Import actual configuration
+          (import ./configuration)
+        ];
+      };
+    in {
+      # The actual system definition
+      tlaternet = nixpkgs.lib.nixosSystem {
+        inherit system;
+        modules = [
+          genericModule
+          (import ./configuration/hardware-specific/linode)
         ];
       };
 
+      # A qemu VM to test the above with
       vm = nixpkgs.lib.nixosSystem {
         inherit system;
-
         modules = [
-          ({modulesPath, ...}: {
-            imports = [(modulesPath + "/profiles/headless.nix")];
-            nixpkgs.overlays = overlays;
-          })
-          (import ./modules)
-
-          (import ./configuration)
-          sops-nix.nixosModules.sops
-          tlaternet-webserver.nixosModules.default
-          ({lib, ...}: {
-            users.users.tlater.password = "insecure";
-
-            # Disable graphical tty so -curses works
-            boot.kernelParams = ["nomodeset"];
-
-            # Sets the base domain for nginx to localhost so that we
-            # can easily test locally with the VM.
-            services.nginx.domain = lib.mkOverride 99 "localhost";
-
-            # Use the staging secrets
-            sops.defaultSopsFile = lib.mkOverride 99 ./keys/staging.yaml;
-
-            # # Set up VM settings to match real VPS
-            # virtualisation.memorySize = 3941;
-            # virtualisation.cores = 2;
-          })
+          genericModule
+          (import ./configuration/hardware-specific/vm.nix)
         ];
       };
     };
 
+    ####################
+    # Helper functions #
+    ####################
+    lib = import ./lib {lib = nixpkgs.lib;};
+
+    ####################
+    # VM launch script #
+    ####################
     apps.${system}.default = let
       inherit (self.nixosConfigurations.vm.config.system.build) vm;
       inherit (nixpkgs.legacyPackages.${system}) writeShellScript;
-      inherit (nixpkgs.lib.attrsets) mapAttrsToList;
-      inherit (nixpkgs.lib.strings) concatStringsSep;
-      ports = {
+      qemuNetOpts = self.lib.makeQemuNetOpts {
         "2222" = "2222";
         "3080" = "80";
         "3443" = "443";
-        "2221" = "2221";
         "21025" = "21025"; # Starbound
       };
-      QEMU_NET_OPTS =
-        concatStringsSep ","
-        (mapAttrsToList
-          (host: vm: "hostfwd=::${host}-:${vm}")
-          ports);
     in {
       type = "app";
       program = builtins.toString (writeShellScript "run-vm" ''
         export QEMU_OPTS="-m 3941 -smp 2 -curses"
-        export QEMU_NET_OPTS="${QEMU_NET_OPTS}"
+        export QEMU_NET_OPTS="${qemuNetOpts}"
         "${vm}/bin/run-tlaternet-vm"
       '');
     };
 
-    devShells.${system}.default = pkgs.mkShell {
-      sopsPGPKeyDirs = ["./keys/hosts/" "./keys/users/"];
-      nativeBuildInputs = [
-        sops-pkgs.sops-import-keys-hook
-      ];
-      buildInputs = with pkgs; [
-        nixfmt
-        git-lfs
-        sops-pkgs.sops-init-gpg-key
-      ];
+    ###########################
+    # Development environment #
+    ###########################
+    devShells.${system}.default = let
+      inherit (sops-nix.packages.${system}) sops-import-keys-hook sops-init-gpg-key;
+      deploy-rs-bin = deploy-rs.packages.${system}.default;
+      pkgs = nixpkgs.legacyPackages.${system};
+    in
+      nixpkgs.legacyPackages.${system}.mkShell {
+        sopsPGPKeyDirs = ["./keys/hosts/" "./keys/users/"];
+        nativeBuildInputs = [
+          sops-import-keys-hook
+        ];
+        packages = with pkgs; [
+          nixfmt
+          git-lfs
+          sops-init-gpg-key
+          deploy-rs-bin
+        ];
 
-      shellHook = ''
-        # Work around sudo requiring a full terminal when deploying to
-        # a remote host
-        export NIX_SSHOPTS="-t"
-      '';
-    };
+        shellHook = ''
+          # Work around sudo requiring a full terminal when deploying to
+          # a remote host
+          export NIX_SSHOPTS="-t"
+        '';
+      };
   };
 }
diff --git a/lib/default.nix b/lib/default.nix
new file mode 100644
index 0000000..20d874d
--- /dev/null
+++ b/lib/default.nix
@@ -0,0 +1,10 @@
+{lib}: let
+  inherit (lib.attrsets) mapAttrsToList;
+  inherit (lib.strings) concatStringsSep;
+in {
+  makeQemuNetOpts = portMapping:
+    concatStringsSep ","
+    (mapAttrsToList
+      (host: vm: "hostfwd=::${host}-:${vm}")
+      portMapping);
+}