Nix / NixOS

2730 readers
1 users here now

Main links

Videos

founded 2 years ago
MODERATORS
1
 
 

I need to use multiple ? in one of my flake input's url, the issue is only the first ? gets registered but not the second. I've tried github:SteamClientHomebrew/Millennium?tag=latest?dir=packages/nix which returns this:

error:
       … while updating the lock file of flake 'git+file:///home/claymorwan/.dotfiles?dir=NixOS'

       … while updating the flake input 'millennium'

       error: path '«github:SteamClientHomebrew/Millennium/fcb0e5627deecf2fc03067ee3dfb63d37e8e82d4»/flake.nix' does not exist

Then I've tried github:SteamClientHomebrew/Millennium?dir=packages/nix?tag=latest which returns this:

error:
       … while updating the lock file of flake 'git+file:///home/claymorwan/.dotfiles?dir=NixOS'

       … while updating the flake input 'millennium'

       error: path '«github:SteamClientHomebrew/Millennium/fcb0e5627deecf2fc03067ee3dfb63d37e8e82d4»/packages/nix?tag=latest/flake.nix' does not exist

I've also tried to write the input differently like this

millenium = {
  type = "github";
  owner = "SteamClientHomebrew";
  repo = "Millennium";
  tag = "latest";
  dir = "packages/nix";
  inputs.nixpkgs.follows = "nixpkgs";
}

which also didn't work and returns this

error:
       … while evaluating flake input
         at /home/claymorwan/.dotfiles/NixOS/flake.nix:48:5:
           47|
           48|     millennium = {
             |     ^
           49|       type = "github";

       error: input attribute 'tag' not supported by scheme 'github'

So not really sure how to set that up

2
3
 
 

Skills are basically markdown documents providing documentation/instructions for LLMs.

As an example, I would like to have a "Nix/NixOS skill" available to opencode when I'm in my NixOS configuration directory. For example one from agentskills.io . What's the best way to install that with Nix?

There seem to be some non-Nix tools for doing this, like skills.

I did some searching online but I couldn't find a simple solution to this.

4
5
6
 
 

TL;DR fetchmail to move all emails from email provider to local mailbox that is then served via IMAP by dovecot

Hi, I like being able to switch between email providers easily without having to change my email address (related post). For example right now people have to go through the hassle of going from mygreatusername@some.host to anotherusername@another.host. It's a big barrier because you now have to update that email address everywhere. Imagine having everything on gmail and then moving to startmail, fastmail, posteo, or whatever else.

A solution I was made aware of is to:

  • pay for a domain e.g mydomain.org for 10 years (can be cheap)
  • use their inbuilt email (sometimes free) or pick an email provider that allows custom domains
  • pull all the email to server you host
  • serve that email

That way, you will have your myname@mydomain.org and switch email providers underneath while keeping all your emails.

Example config

This config uses the module I wrote (maybe something else exists, but I couldn't find it). It pulls emails of myaccount@my.domain from pop.remote.host to my.host and exposes them via IMAPS as myaccount@my.domain on my.host.

Notice that my.domain need not be the same as my.host. This allows me to hide my IMAP server. Somebody looking at the MX record of my.domain won't immediately find the IMAP server.

{ config, ... }:
{
  /**
    configuration to for fetchmail to retrieve from the remote host
    emails will be moved into a the **local** mailbox of a user with the same email address
  */
  environment.etc."mail/fetchmailrc" = {
    text = ''
      poll pop.remote.host protocol pop3 port 995:
            user "myaccount@my.domain" with password "passwordWithouQuotes" is vmail here
            options fetchall
            ssl
            mda "dovecot-deliver -d myaccount@my.domain"
    '';
    user = config.services.email-fetch-serve.daemonUser;
    group = config.services.email-fetch-serve.daemonGroup;
  };
  
  /**
    usernames and passwords used to log into the **self-hosted** IMAP service
    Uses same format as /etc/passwd
    https://doc.dovecot.org/2.4.3/core/config/auth/databases/passwd_file.html
  */
  environment.etc."mail/imap.passwd" = {
    text = ''
      myAccount@my.domain:{plain}password
    '';
    user = config.services.email-fetch-serve.daemonUser;
    group = config.services.email-fetch-serve.daemonGroup;
  };
  services.email-fetch-serve = {
    enable = true;
    sslCertPath = "/var/acme/certs/mydomain.crt";
    sslCertKey = "/var/acme/certs/mydomain.key";
    fetchmailRcPath = "/etc/mail/fetchmailrc";
    imap = {
      port = 993;
      openFirewall = true;
      passdb = "/etc/mail/imap.passwd";
    };
  };
}

the module

{
  config,
  lib,
  pkgs,
  ...
}:

let
  cfg = config.services.email-fetch-serve;
  daemonUserHome = "/var/spool/${cfg.daemonUser}";
  sslEnabled = (cfg.sslCertPath != null) && (cfg.sslCertKey != null);
  /**
    Used by fetchmail to deliver mail to dovecot
  */
  dovecot-deliver-wrapper = pkgs.writeShellScriptBin "dovecot-deliver" ''${pkgs.dovecot}/libexec/dovecot/deliver "''${@}"'';
in
{
  /**
    A self-hosted "email relay" that allows fetching emails from a server and then serving it
    via IMAP.

    Emails are retrieved with fetchmail and exposed via dovecot.

    By default, dovecot used IMAP which unencrypted, but with an ssl certificate and key, it can
    be encrypted and thus turned into IMAPS.
    To generate SSL certs, the `security.acme` option is powerful, but you can also use a
    self-signed certificate.

    To store secrets, do consider using
    - agenix: https://github.com/ryantm/agenix
    - sopsnix: https://github.com/Mic92/sops-nix
  */
  options = with lib; {
    services.email-fetch-serve = {
      enable = mkEnableOption "emails from an email server and serve them via IMAP";
      sslCertPath = mkOption {
        type = types.nullOr types.externalPath;
        description = "Giving a path to an SSL cert **and** key will enable IMAPS and disable IMAP";
        default = null;
      };
      sslCertKey = mkOption {
        type = types.nullOr types.externalPath;
        description = "Giving a path to an SSL key **and** cert will enable IMAPS and disable IMAP";
        default = null;
      };
      fetchmailRcPath = mkOption {
        type = types.externalPath;
        description = "Configuration for fetchmail";
        example = ''
          poll pop.remote.host protocol pop3 port 995:
            user "accountName@remote.host" with password "passwordWithouQuotes" is vmail here
            options fetchall
            ssl
            mda "dovecot-deliver -d accountName@remote.host"
        '';
      };
      imap = {
        port = mkOption {
          type = types.int;
          description = ''
            Which port to host the IMAP service on. If sslCertPath is set this will
                        be the port of othe IMAPS service'';
          default = 143; # Default IMAP port
        };
        openFirewall = lib.mkOption {
          type = lib.types.bool;
          default = false;
          example = true;
          description = "Allow external traffic to reach the IMAP(S) port";
        };
        passdb = mkOption {
          type = types.externalPath;
          description = ''
            Where passwords for IMAP are stored. Should be secret and accessible by vmail user
                        https://doc.dovecot.org/2.4.3/core/config/auth/databases/passwd_file.html
                        https://doc.dovecot.org/2.4.3/core/config/auth/passdb.html
          '';
        };
      };
      daemonUser = mkOption {
        type = types.str;
        description = "Name of the user running the daemons";
        default = "vmail";
      };
      daemonGroup = mkOption {
        type = types.str;
        description = "Name of the user's group running the daemons";
        default = "vmail";
      };
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        # Either both SSL vars are set or none are set
        assertion =
          (cfg.sslCertPath == null && cfg.sslCertKey == null)
          || (cfg.sslCertPath != null && cfg.sslCertKey != null);
        message = "email-fetch-serve service must have sslCertPath AND sslCertKey to have functional SSL";
      }
    ];
    # How electronic email works
    # https://tldp.org/HOWTO/Mail-Administrator-HOWTO-3.html

    # ${daemonUserHome} needs to be created and owned by vmail
    users.users."${cfg.daemonUser}" = {
      createHome = true;
      home = daemonUserHome;
      group = cfg.daemonGroup;
      isSystemUser = true;
    };
    users.groups."${cfg.daemonGroup}" = { };

    services.dovecot2 = lib.mkMerge [
      ({
        # Taken and adapted from https://wiki.nixos.org/wiki/Dovecot
        enable = cfg.enable;
        createMailUser = true;

        enableImap = true;

        mailUser = cfg.daemonUser;
        mailGroup = cfg.daemonGroup;

        # implement virtual users
        # https://doc.dovecot.org/2.3/configuration_manual/howto/simple_virtual_install/
        # store virtual mail under
        # /var/spool/mail/vmail/<DOMAIN>/<USER>/Maildir/
        mailLocation = "maildir:~/Maildir";

        mailboxes = {
          # use rfc standard https://apple.stackexchange.com/a/201346
          All = {
            auto = "create";
            autoexpunge = null;
            specialUse = "All";
          };
          Archive = {
            auto = "create";
            autoexpunge = null;
            specialUse = "Archive";
          };
          Drafts = {
            auto = "create";
            autoexpunge = null;
            specialUse = "Drafts";
          };
          Flagged = {
            auto = "create";
            autoexpunge = null;
            specialUse = "Flagged";
          };
          Junk = {
            auto = "create";
            autoexpunge = "60d";
            specialUse = "Junk";
          };
          Sent = {
            auto = "create";
            autoexpunge = null;
            specialUse = "Sent";
          };
          Trash = {
            auto = "create";
            autoexpunge = "60d";
            specialUse = "Trash";
          };
        };

        extraConfig = lib.concatStrings [
          ''
            # force to use full user name plus domain name
            # for disambiguation
            auth_username_format = %Lu

            # Authentication configuration:
            auth_mechanisms = plain
            passdb {
              driver = passwd-file
              args = ${cfg.imap.passdb}
            }

            userdb {
              driver = static
              # the full e-mail address inside passwd-file is the username (%u)
              # user@example.com
              # %d for domain_name %n for user_name
              args = uid=${cfg.daemonUser} gid=${cfg.daemonGroup} username_format=%u home=${daemonUserHome}/%d/%n
            }
          ''

          (lib.optionalString (!sslEnabled) ''
            service imap-login {
              inet_listener imap {
                port = ${builtins.toString cfg.imap.port}
              }
              inet_listener imaps {
                port = 0
              }
          '')
          (lib.optionalString (sslEnabled) ''
            service imap-login {
              inet_listener imap {
                port = 0
              }
              inet_listener imaps {
                port = ${builtins.toString cfg.imap.port}
              }
            }'')
        ];
      })
      (lib.mkIf sslEnabled {
        sslServerCert = cfg.sslCertPath;
        sslServerKey = cfg.sslCertKey;
      })
    ];

    # Open the firewall port to be able to be contacted
    networking.firewall.allowedTCPPorts = [ cfg.imap.port ];
    networking.firewall.allowedUDPPorts = [ cfg.imap.port ];

    #####################
    # To fetch the emails
    systemd.services.fetchmail = {
      enable = cfg.enable;
      after = [ "dovecot2.service" ];
      wantedBy = [ "dovecot2.service" ];
      path = [ dovecot-deliver-wrapper ];
      serviceConfig = {
        User = cfg.daemonUser;
        ExecStart = "${pkgs.fetchmail}/bin/fetchmail --fetchmailrc ${cfg.fetchmailRcPath} --daemon 60";
      };
    };
  };
}

7
 
 

Running NixOS Micro VMs on MacOS

https://abhinavsarkar.net/notes/2026-microvm-nix/

@nix

8
1
submitted 3 weeks ago* (last edited 3 weeks ago) by abnv@fantastic.earth to c/nix@programming.dev
9
10
 
 

I've always hated the fact that Nix silently ignores any file that's not tracked by Git. Partly because I always forget this and have to re-build every time that I add a new file. But also the fact that Nix is coupled to VCS.

jj is a new VCS that wraps git. jj has a slightly different (improved) workflow, and doesn't have a staging area. What this means for Nix is that whenever I create a new file I have to run jj status (the equivalent to git status) before I can build with Nix, which feels incredibly silly to me.

Thanks for coming to my Ted talk.

11
 
 

TLDW: NixOS placed as average...

12
13
14
15
16
 
 

long story short: i have a surface tablet with an sd card in it. id like the sd card to hold /nix. i cant seem to rsync without (im guessing the links are expanding) the bigger drive (sdcard) filling up. thank you

17
 
 

I've got a local unstable package and I want it to always be at the latest commit from its source's upstream. For that I added passthru.updateScript = nix-update-script { extraArgs = [ "--version=branch" ]; }; in the package, but when I build the package/install it, it doesn't update.

I don't wanna have to manually update the source's rev and hash, maybe I'm just not using it right ?

This is the package: https://codeberg.org/claymorwan/dotfiles/src/commit/9ca46a651a16a5dc85f35d3c0d19bcd03d692fdd/NixOS/pkgs/fluxer/default.nix

And this is how I install it in my config: https://codeberg.org/claymorwan/dotfiles/src/commit/9ca46a651a16a5dc85f35d3c0d19bcd03d692fdd/NixOS/modules/home/Packages/default.nix#L41

18
19
1
submitted 1 month ago* (last edited 1 month ago) by hallettj@leminal.space to c/nix@programming.dev
 
 

My wife brought home a TV from a thrift store. So I took the easy route of setting up a media center to make it play videos. I already had a headless Jellyfin server running on a small Beelink computer. I connected that to the TV, installed Kodi, installed the Yatse remote control app on my phone, and we're all set.

It took me some research and trial and error to get the remote control app connected with Zeroconf auto discovery working. So I thought I'd share what I learned. Here is my entire Kodi module:

{ pkgs, ... }:

{
  # Enable a graphical shell
  services.xserver.enable = true;

  services.xserver.desktopManager.kodi = {
    enable = true;
    package = pkgs.kodi.withPackages (
      kodiPackages: with kodiPackages; [
        jellyfin
        netflix
      ]
    );
  };

  # To view plugins available in nixpkgs run:
  #
  # $ nix repl
  # > pkgs = import <nixpkgs> {}
  # > builtins.attrNames pkgs.kodiPackages
  #
  # Or search for plugins on https://search.nixos.org/, and in the left-sidebar
  # under "Package sets" click "kodiPackages"

  services.displayManager.autoLogin = {
    enable = true;
    user = "kodi";
  };

  users.users.kodi.isNormalUser = true;

  # Allow reboot and shut down from Kodi UI
  security.polkit = {
    enable = true;
    extraConfig = /* javascript */ ''
      polkit.addRule(function(action, subject) {
        if (
          (
            action.id == "org.freedesktop.login1.reboot" ||
            action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
            action.id == "org.freedesktop.login1.power-off" ||
            action.id == "org.freedesktop.login1.power-off-multiple-sessions"
          )
          && subject.user == "kodi"
        ) {
          return polkit.Result.YES;
        }
      });
    '';
  };

  # Allow access to web UI & remote control API
  networking.firewall = {
    allowedTCPPorts = [
      8010 # the port I configured for "allow remote control via HTTP"
      9090 # also event server?
    ];
    allowedUDPPorts = [
      9777 # event server
    ];
  };

  # Allows Kodi to advertise to remote control apps using Zeroconf.
  services.avahi = {
    enable = true;
    publish.enable = true;
    publish.userServices = true;
  };
}

Kodi is a graphical shell. Like I said, this box was previously headless; so I enabled Kodi as my DE and set it up to automatically log in.

To make the remote control work there are some necessary settings changes to make in the Kodi UI. I had to connect a keyboard temporarily to set this up:

  • Go to Settings > Services
  • Enable General > Announce services to other systems (for Zeroconf auto discovery)
  • In Control enable:
    • Allow remote control via HTTP (match port number in NixOS firewall settings)
    • Allow remote control from applications on this system
    • Allow remote control from applications on other systems
  • I'm not sure if this is needed, but the Yatse docs recommend these settings in UPnP / DLNA for "some streaming cases":
    • Share my libraries
    • Allow remote control via UPnP

Declarative settings would be nicer, but there doesn't seem to be a NixOS module that does that yet. It's the same situation with Jellyfin.

20
21
 
 

NixOS config walkthrough around 7min mark.

22
23
24
 
 

A follow-up to my post last week, the title is a bit unfortunate but it is what it is.

Anyhow, here I set up a repository that defines one host using the dendritic approach in a very simple manner (plus another one before to introduce how flake-parts does this). It uses the package and service defined in another repository in Part 1, also linked in this post.

It only goes into the basics and is already long enough in my opinion, but I tried to write everything down to start with a clean base from scratch. There is a lot of room for improvement, but it should get the basics right.

25
 
 

I'm using xdg.dataFile in order to install krita plugin, the issue is that krita plugin need to have their content placed in the `~/.local/share/krita/pykrita" and cannot be put in subfolder. For now I'm doing

  xdg.dataFile = {
    "krita/pykrita" = {
      enable = true;
      source = pkgs.fetchFromGitHub {
        owner = "veryprofessionaldodo";
        repo = "Krita-UI-Redesign";
        rev = "df37ade2334b09ca30820286e3e16c26b0fbb4f8";
        hash = "sha256-kGs1K2aNIiQq//W8IQ2JX4iyXq43z2I/WnI8aJjg8Yk=";
      };
      recursive = true;
    };
  };

It's fine when i use a single plugin, but when a use multiple plugins i gotta use multiple fetchers which im not sure how to do. tried to do source = <fetcher 1> + <fetcher 2> which surprisingly builds but then the home-manager service fails

view more: next ›