diff --git a/tv/3modules/default.nix b/tv/3modules/default.nix
index 397ee8e85..83dc212a6 100644
--- a/tv/3modules/default.nix
+++ b/tv/3modules/default.nix
@@ -6,5 +6,6 @@ _:
     ./ejabberd
     ./hosts.nix
     ./iptables.nix
+    ./x0vncserver.nix
   ];
 }
diff --git a/tv/3modules/x0vncserver.nix b/tv/3modules/x0vncserver.nix
new file mode 100644
index 000000000..44fed590d
--- /dev/null
+++ b/tv/3modules/x0vncserver.nix
@@ -0,0 +1,52 @@
+with import <stockholm/lib>;
+{ config, pkgs, ... }: let
+
+  cfg = config.tv.x0vncserver;
+
+in {
+  options.tv.x0vncserver = {
+    display = mkOption {
+      default = ":${toString config.services.xserver.display}";
+      type = types.str;
+    };
+    enable = mkEnableOption "tv.x0vncserver";
+    pwfile = mkOption {
+      default = {
+        owner = cfg.user;
+        path = "${cfg.user.home}/.vncpasswd";
+        source-path = toString <secrets> + "/vncpasswd";
+      };
+      description = ''
+        Use vncpasswd to edit pwfile.
+        See: nix-shell -p tigervnc --run 'man vncpasswd'
+      '';
+      type = types.secret-file;
+    };
+    rfbport = mkOption {
+      default = 5900;
+      type = types.int;
+    };
+    user = mkOption {
+      default = config.krebs.build.user;
+      type = types.user;
+    };
+  };
+  config = mkIf cfg.enable {
+    krebs.secret.files = {
+      x0vncserver-pwfile = cfg.pwfile;
+    };
+    systemd.services.x0vncserver = {
+      after = [ "graphical.target" "secret.service" ];
+      requires = [ "graphical.target" "secret.service" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.tigervnc}/bin/x0vncserver ${toString [
+          "-display ${cfg.display}"
+          "-passwordfile ${cfg.pwfile.path}"
+          "-rfbport ${toString cfg.rfbport}"
+        ]}";
+        User = cfg.user.name;
+      };
+    };
+    tv.iptables.input-retiolum-accept-tcp = singleton (toString cfg.rfbport);
+  };
+}