Last active
October 22, 2025 20:17
-
-
Save Doosty/2dab302d7445b4fc3b9239958a9fccb0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # `nixos-module-dockurr-windows.nix` | |
| # my current module, might or might not work, have not used it in a while | |
| { | |
| config, | |
| lib, | |
| pkgs, | |
| ... | |
| }: let | |
| app = "dockurr-windows"; | |
| cfg = config.my-nix-lib.${app}; | |
| inherit (lib) types mkOption mkEnableOption optionalAttrs mkIf mkMerge strings optionals; | |
| in { | |
| imports = [ | |
| # ./winapps # doesnt work on nixos properly, is very dynamically written script...TODO: try using winboat | |
| ]; | |
| options.my-nix-lib.${app} = { | |
| enable = mkOption { | |
| type = types.bool; | |
| default = false; | |
| }; | |
| address = mkOption { | |
| type = types.str; | |
| default = "127.0.0.1"; | |
| description = "Listen on this host address"; | |
| }; | |
| ports = { | |
| rdp = mkOption { | |
| type = types.nullOr types.port; | |
| default = 3389; | |
| description = "Rdp remote access port, use `null` if you dont want it exposed on the host address"; | |
| }; | |
| http = mkOption { | |
| type = types.nullOr types.port; | |
| default = 8006; | |
| description = "Http remote access port, use `null` if you dont want it exposed on the host address"; | |
| }; | |
| }; | |
| stateDir = mkOption { | |
| type = types.path; | |
| default = "/var/lib/${app}"; | |
| description = "Location for persistent state data"; | |
| }; | |
| version = mkOption { | |
| type = types.str; | |
| default = "latest"; | |
| example = "4.09"; | |
| description = "Version of the dockurr/windows container. Find the version tags at `https://hub.docker.com/r/dockurr/windows/tags`"; | |
| }; | |
| shareDir = mkOption { | |
| type = types.path; | |
| default = "/var/lib/${app}/shared"; | |
| description = "Location for data that will be shared between the host and guest system. Inside the guest the location is `\\\\host.lan\\Data` or `Computer -> Network (must enable discovery) -> host.lan`"; | |
| }; | |
| passthrough-usb-devices = mkOption { | |
| type = types.listOf types.str; | |
| default = []; | |
| example = ["012a:023b" "0123:0456"]; | |
| description = '' | |
| Identifiers of *USB devices* that will be passed through into the Windows container. Use `nix shell nixpkgs#usbutils --command lsusb` to identify your devices | |
| ''; | |
| }; | |
| privileged = mkOption { | |
| type = types.bool; | |
| default = false; | |
| }; | |
| manual-networking = { | |
| enable = mkEnableOption "Create and use a specific container network"; | |
| address = mkOption { | |
| type = types.nullOr types.str; | |
| example = "10.89.73.101"; | |
| default = | |
| if cfg.manual-networking.enable | |
| then (throw "Must set `manual-networking.address`") | |
| else null; | |
| }; | |
| network = mkOption { | |
| type = types.nullOr types.str; | |
| example = "10.89.73.0/24"; | |
| description = "Container network subnet in CIDR format"; | |
| default = | |
| if cfg.manual-networking.enable | |
| then (throw "Must set `manual-networking.network`") | |
| else null; | |
| }; | |
| }; | |
| envConfig = mkOption { | |
| default = {}; | |
| type = types.submodule { | |
| freeformType = types.str; | |
| options = { | |
| VERSION = mkOption { | |
| type = types.str; | |
| default = "11"; | |
| }; | |
| RAM_SIZE = mkOption { | |
| type = types.str; | |
| default = "8G"; | |
| }; | |
| CPU_CORES = mkOption { | |
| type = types.str; | |
| default = "12"; | |
| }; | |
| DISK_SIZE = mkOption { | |
| type = types.str; | |
| default = "50G"; | |
| }; | |
| NETWORK = mkOption { | |
| type = types.str; | |
| default = "Y"; | |
| }; | |
| ARGUMENTS = mkOption { | |
| type = types.str; | |
| default = ""; | |
| }; | |
| USERNAME = mkOption { | |
| type = types.str; | |
| default = "docker"; | |
| }; | |
| PASSWORD = mkOption { | |
| type = types.str; | |
| default = ""; | |
| }; | |
| # GPU: "Y" | |
| }; | |
| }; | |
| }; | |
| }; | |
| config = (mkIf cfg.enable) (mkMerge [ | |
| { | |
| virtualisation.oci-containers.containers.${app} = { | |
| image = "docker.io/dockurr/windows:${cfg.version}"; # <https://hub.docker.com/r/dockurr/windows/tags> | |
| user = "root:root"; | |
| autoStart = false; | |
| # puts `cfg.passthrough-usb-devices` at the end of `ARGUMENTS` env var, in the correct format <https://github.com/dockur/windows?tab=readme-ov-file#how-do-i-pass-through-a-usb-device> | |
| environment = | |
| lib.mapAttrs ( | |
| name: value: | |
| if name == "ARGUMENTS" | |
| then | |
| toString value | |
| + (lib.concatStringsSep "" ( | |
| map ( | |
| device: let | |
| parts = lib.splitString ":" device; | |
| vendorId = lib.elemAt parts 0; | |
| productId = lib.elemAt parts 1; | |
| in " -device usb-host,vendorid=0x${vendorId},productid=0x${productId}" | |
| ) | |
| cfg.passthrough-usb-devices | |
| )) | |
| else toString value | |
| ) | |
| cfg.envConfig; | |
| ports = | |
| (optionals (cfg.ports.http != null) [ | |
| "${cfg.address}:${builtins.toString cfg.ports.http}:8006" | |
| ]) | |
| ++ (optionals (cfg.ports.rdp != null) [ | |
| "${cfg.address}:${builtins.toString cfg.ports.rdp}:3389/tcp" | |
| "${cfg.address}:${builtins.toString cfg.ports.rdp}:3389/udp" | |
| ]); | |
| extraOptions = | |
| [ | |
| "--device=/dev/kvm" | |
| "--device=/dev/net/tun" | |
| "--cap-add=NET_ADMIN" | |
| "--cap-add=NET_RAW" | |
| ] | |
| ++ (optionals (cfg.privileged) | |
| [ | |
| "--privileged" # gives everything | |
| # "--cap-add=SYS_ADMIN,MKNOD" | |
| # "--device=/dev/fuse" | |
| # "--security-opt" "label=disable" # selinux | |
| ]) | |
| ++ (optionals (cfg.passthrough-usb-devices != []) | |
| ["--device=/dev/bus/usb"]); | |
| volumes = [ | |
| "${cfg.stateDir}:/storage" | |
| "${cfg.shareDir}:/data" | |
| ]; | |
| }; | |
| environment = { | |
| systemPackages = | |
| [ | |
| pkgs.freerdp | |
| # cli | |
| (pkgs.writeShellScriptBin "${app}-service-start" ''sudo systemctl start podman-${app}.service'') | |
| (pkgs.writeShellScriptBin "${app}-service-status" ''sudo systemctl status podman-${app}.service'') | |
| (pkgs.writeShellScriptBin "${app}-service-stop" ''sudo systemctl stop podman-${app}.service'') | |
| (pkgs.writeShellScriptBin "${app}-service-logs" ''sudo journalctl --unit podman-${app}.service --follow'') | |
| ] | |
| ++ (optionals (cfg.ports.http != null) | |
| [ | |
| (pkgs.writeShellApplication { | |
| name = "${app}-access-web"; | |
| text = '' | |
| systemctl is-active --quiet podman-${app}.service || { echo "Error: ${app} not running"; exit 1; } \ | |
| && xdg-open http://${cfg.address}:${builtins.toString cfg.ports.http}/ | |
| ''; | |
| }) | |
| ]) | |
| ++ (optionals (cfg.ports.rdp != null) | |
| [ | |
| (pkgs.writeShellApplication { | |
| name = "${app}-access-rdp-wl"; | |
| runtimeInputs = [pkgs.freerdp]; | |
| text = '' | |
| systemctl is-active --quiet podman-${app}.service || { echo "Error: ${app} not running"; exit 1; } \ | |
| && wlfreerdp /u:${cfg.envConfig.USERNAME} /p:${cfg.envConfig.PASSWORD} /cert:ignore /v:${cfg.address}:${builtins.toString cfg.ports.rdp} /scale:100 +auto-reconnect +home-drive -wallpaper +dynamic-resolution | |
| ''; | |
| }) | |
| (pkgs.writeShellApplication { | |
| name = "${app}-access-rdp-x11"; | |
| runtimeInputs = [pkgs.rdesktop]; | |
| text = '' | |
| systemctl is-active --quiet podman-${app}.service || { echo "Error: ${app} not running"; exit 1; } \ | |
| && rdesktop -u ${cfg.envConfig.USERNAME} ${cfg.address}:3389 | |
| ''; | |
| }) | |
| # gui with yad | |
| (pkgs.writeShellApplication { | |
| name = "${app}-start-and-access-with-freerdp"; | |
| runtimeInputs = [pkgs.netcat-gnu pkgs.freerdp pkgs.yad]; | |
| text = '' | |
| TIMEOUT=15 | |
| # Function to show notifications | |
| show_notification() { | |
| local message="$1" | |
| local title="$2" | |
| local icon="''${3:-info}" | |
| yad --notification --image="gtk-$icon" \ | |
| --text="$message" \ | |
| --command="" \ | |
| --timeout=3 & | |
| # Also show as a popup notification | |
| yad --notification-popup \ | |
| --image="gtk-$icon" \ | |
| --text="$message" \ | |
| --timeout=3 \ | |
| --title="$title" & | |
| } | |
| check_port() { | |
| nc -z -w 2 "${cfg.address}" "${builtins.toString cfg.ports.rdp}" >/dev/null 2>&1 | |
| return $? | |
| } | |
| start_service() { | |
| # show_notification "Starting podman-dockurr-windows service..." "Service Start" "info" | |
| sudo systemctl start podman-dockurr-windows.service | |
| # show_notification "Waiting for RDP port to become available..." "Port Check" "info" | |
| # Create a progress dialog | |
| ( | |
| for i in $(seq 1 $TIMEOUT); do | |
| if check_port; then | |
| echo "100" | |
| echo "# RDP port is now open" | |
| break | |
| fi | |
| # Calculate percentage | |
| percentage=$((i * 100 / TIMEOUT)) | |
| echo "$percentage" | |
| echo "# Waiting for port... ($i/$TIMEOUT)" | |
| sleep 1 | |
| done | |
| ) | yad --progress \ | |
| --title="Waiting for RDP Port" \ | |
| --text="Checking port availability..." \ | |
| --percentage=0 \ | |
| --auto-close \ | |
| --auto-kill \ | |
| --width=300 | |
| # Check final status | |
| if ! check_port; then | |
| return 1 | |
| fi | |
| } | |
| if ! check_port; then | |
| # show_notification "RDP port is not open. Starting service..." "Service Status" "info" | |
| start_service || { | |
| # show_notification "Failed to start service and open RDP port" "Error" "error" | |
| exit 1 | |
| } | |
| fi | |
| # show_notification "Connecting to remote machine..." "RDP Connection" "info" | |
| base_args=( | |
| /u:${cfg.envConfig.USERNAME} | |
| /p:${cfg.envConfig.PASSWORD} | |
| /cert:ignore | |
| /v:${cfg.address}:${builtins.toString cfg.ports.rdp} | |
| /scale:100 | |
| +auto-reconnect | |
| +home-drive | |
| -wallpaper | |
| +dynamic-resolution | |
| ) | |
| if [ -d "/run/media" ]; then | |
| base_args+=(/drive:'/run/media,RemovableDevices') | |
| fi | |
| if ! wlfreerdp "''${base_args[@]}"; then | |
| show_notification "Failed to connect using wlfreerdp" "Connection Error" "error" | |
| exit 1 | |
| fi | |
| ''; | |
| }) | |
| # "${app}-access-rdp-wayland-2" = execute-if-running "${pkgs.freerdp3}/bin/wlfreerdp /u:docker /p: /v:${address-only}:3389 /w:${builtins.toString cfg.resolution-of-rdp-session.width} /h:${builtins.toString cfg.resolution-of-rdp-session.height} /cert:ignore"; | |
| # "${app}-access-rdp-wayland-3" = execute-if-running "${pkgs.freerdp3}/bin/sdl-freerdp /u:docker /p: /v:${address-only}:3389 /w:${builtins.toString cfg.resolution-of-rdp-session.width} /h:${builtins.toString cfg.resolution-of-rdp-session.height} /cert:ignore"; | |
| # "${app}-access-rdp-wayland" = execute-if-running "${pkgs.freerdp}/bin/wlfreerdp /u:${cfg.extraEnvConfig.USERNAME} /p:${cfg.extraEnvConfig.PASSWORD} /v:${address-only}:3389 /w:${builtins.toString cfg.resolution-of-rdp-session.width} /h:${builtins.toString cfg.resolution-of-rdp-session.height} /cert:ignore"; | |
| ]); | |
| }; | |
| assertions = [ | |
| { | |
| assertion = config.networking.nftables.enable == false; | |
| message = "ERROR: dockur windows container requires iptables, but you are using nftables"; | |
| } | |
| ]; | |
| systemd.tmpfiles.rules = [ | |
| "d '${cfg.stateDir}' 0700 root root - -" | |
| # "d '${cfg.shareDir}' 0770 root users - -" | |
| ]; | |
| # environment.persistence."/persistent".directories = [ | |
| # { | |
| # directory = config.my-nix-lib.microsoft.${app}.stateDir; | |
| # mode = "u=rwx,g=,o="; | |
| # } | |
| # ]; # NOTE: for `nix-community/impermanence` | |
| } | |
| (mkIf (cfg.manual-networking.enable) { | |
| # WARNING: if address is only changed it will create errors | |
| # TODO: create a proper systemd service here that recreates the network if addresses change? | |
| virtualisation.oci-containers.containers.${app} = { | |
| extraOptions = [ | |
| "--network=manual-${app}" | |
| "--ip=${cfg.manual-networking.address}" | |
| ]; | |
| }; | |
| systemd.services."podman-${app}".preStart = '' | |
| ${pkgs.podman}/bin/podman network exists manual-${app} || \ | |
| ${pkgs.podman}/bin/podman network create manual-${app} \ | |
| --subnet=${cfg.manual-networking.network} | |
| ''; | |
| networking.firewall = { | |
| extraCommands = mkIf (!config.networking.nftables.enable) '' | |
| iptables -A INPUT -p tcp --destination-port 53 -s ${cfg.manual-networking.address} -j ACCEPT | |
| iptables -A INPUT -p udp --destination-port 53 -s ${cfg.manual-networking.address} -j ACCEPT | |
| ''; | |
| }; | |
| }) | |
| (mkIf (cfg.manual-networking.enable == false) { | |
| systemd.services."podman-${app}".preStart = ''${pkgs.podman}/bin/podman network exists manual-${app} && ${pkgs.podman}/bin/podman network rm manual-${app}''; | |
| }) | |
| ]); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # sample configuration of the module | |
| { | |
| inputs, | |
| ... | |
| }: { | |
| imports = [ | |
| ./nixos-module-dockurr-windows.nix | |
| ]; | |
| my-nix-lib.dockurr-windows = { | |
| enable = true; | |
| version = "4.11"; | |
| envConfig = { | |
| VERSION = "2025"; | |
| }; | |
| passthrough-usb-devices = [ | |
| # sandisk usb drive | |
| "0781:5583" | |
| # flydigi vader4, note: sometimes shows in device manager upon bootup but then disappears when trying to connect with its app... | |
| "05ac:0250" | |
| "045e:028e" # this one works in proxmox vm with wireless dongle | |
| "04b4:2412" | |
| ]; | |
| manual-networking = { | |
| enable = true; | |
| address = "10.89.73.101"; | |
| network = "10.89.73.0/24"; | |
| }; | |
| # remnant2 save games, for use with Remnant2 save guardian, which only works properly on windows | |
| shareDir = "/home/blazp/.steam/steam/steamapps/compatdata/1282100/pfx/drive_c/users/steamuser/Saved\ Games/Remnant2/Steam/109679480"; | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # idea of how to init the state | |
| systemd.services.init-dockurr-state = { | |
| before = ["podman-${app}.service"]; | |
| wantedBy = ["multi-user.target" "podman-${app}.service"]; | |
| serviceConfig = { | |
| Type = "oneshot"; | |
| ExecStart = lib.getExe (pkgs.writeShellApplication { | |
| name = "initialize-dockurr-state"; | |
| runtimeInputs = [pkgs.rsync]; | |
| text = '' | |
| # write a script that somehow copies premade dockurr files from a fileshare or whatever into `/var/lib/dockurr-windows` if dir is empty | |
| # the img file is 50GB or something, too large to include into nix flake configs | |
| ''; | |
| }); | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment