Using nixos-shell it is easy to build and run a virtual machine from a Nix configuration file describing the system. I started using this tool to create clean and reproducible test environments for developing Nextcloud apps.

The requirement for this setup was to automatically configure Nextcloud, additional apps and users for testing. The Nextcloud service should be able to deliver mails to a local mail server with preconfigured mailboxes and users.

Unfortunately the development setup requires NixOS and nixos-shell installed and most probably won’t work on other systems. The following commands clone the repositories and run the virtual machine. After that Nextcloud will be available at http://localhost:8080 and the mail server on port 1433 (IMAP)

git clone https://github.com/NixOS/nixpkgs.git
git clone https://github.com/onny/nixos-nextcloud-testumgebung.git
cd nixos-nextcloud-testumgebung
QEMU_NET_OPTS="hostfwd=tcp::8080-:80,hostfwd=tcp::1433-:143,hostfwd=tcp::5877-:587" NIX_PATH=nixpkgs=../nixpkgs nixos-shell vm-nextcloud.nix

The state of the virtual machine is stored in an image file called nixos.qcow2. Deleting this file will reset the state back to a clean environment.

The configuration explained

In the following part I’m going to describe some parts of this configuration found in vm-nixos.nix in more detail.

The first part overrides the Nextcloud package from the NixOS repository. Overriding in NixOS means, that we’ll going to change parts of the package. In this case, we’ll remove default apps which we don’t need in our test environment

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

  nixpkgs = {
    overlays = [
      (self: super: {
        # Remove first run wizard and password policy check from Nextcloud
        # package
        nextcloud25 = super.nextcloud25.overrideAttrs (oldAttrs: rec {
          installPhase = oldAttrs.installPhase + ''
            rm -r $out/apps/firstrunwizard
            rm -r $out/apps/password_policy
          '';
        });
      })
    ];
  };

This section installs and enables the Nextcloud server. We define the username and password for the administrator account. Further we install the circle app from a release tarball, the mail app from the repository and the compiled calendar app from a local git checkout / development directory. The last part configures local mail delivery using sendmail

  # Setup Nextcloud including apps
  services.nextcloud = {
    enable = true;
    package = pkgs.nextcloud25;
    hostName = "localhost";
    config = {
      adminuser = "admin";
      adminpassFile = "${pkgs.writeText "adminpass" "test123"}";
    };
    extraApps = {
      circles = pkgs.fetchNextcloudApp rec {
        url = "https://github.com/nextcloud-releases/circles/releases/download/0.21.4/circles-0.21.4.tar.gz";
        sha256 = "sha256-gkW9jZvXS86ScuM434mUbvQajYKwHVjm9PfTMNgHL/Q=";
      };
      calendar = pkgs.stdenvNoCC.mkDerivation rec {
        name = "calendar";
        src = /home/onny/projects/calendar;
        unpackPhase = ''cp -r --no-preserve=mode $src/* .'';
        dontBuild = true;
        installPhase = ''cp -r --no-preserve=mode . $out/'';
      };
      mail = pkgs.nextcloud25Packages.apps.mail;
    };
    extraOptions = {
      mail_smtpmode = "sendmail";
      mail_sendmailmode = "pipe";
    };
  };

The next service is maddy, a simple and small mail server. The configuration below doesn’t use any TLS or other secure settings since we’re only using it in the development environment. The msmtp section configures the sendmail client to use the local mailbox for mail delivery

  # Setup mail server
  services.maddy = {
    enable = true;
    hostname = "localhost";
    primaryDomain = "localhost";
    # Disable any sender validation checks
    config = lib.concatStrings (
      builtins.match "(.*)authorize_sender.*identity\n[ ]+\}(.*)" options.services.maddy.config.default
    );
  };

  # Configure local mail delivery
  programs.msmtp = {
    enable = true;
    accounts.default = {
      host = "localhost";
      port = 587;
      auth = "login";
      tls = "off";
      from = "admin@localhost";
      user = "admin@localhost";
      password = "test123";
    };
  };

To further automate the bootstrapping process, two systemd services create users and mailboxes for maddy and Nextcloud

  # Creating mail users and inboxes
  systemd.services.maddy-accounts = {
    script = ''
      ${pkgs.maddy}/bin/maddyctl creds create --password test123 user1@localhost
      ${pkgs.maddy}/bin/maddyctl imap-acct create user1@localhost
      ${pkgs.maddy}/bin/maddyctl creds create --password test123 user2@localhost
      ${pkgs.maddy}/bin/maddyctl imap-acct create user2@localhost
      ${pkgs.maddy}/bin/maddyctl creds create --password test123 admin@localhost
      ${pkgs.maddy}/bin/maddyctl imap-acct create admin@localhost
    '';
    serviceConfig = {
      Type = "oneshot";
      User= "maddy";
    };
    after = [ "maddy.service" ];
    wantedBy = [ "multi-user.target" ];
  };

  # Creating Nextcloud users and configure mail adresses
  systemd.services.nextcloud-add-user = {
    script = ''
      export OC_PASS="test123"
      ${config.services.nextcloud.occ}/bin/nextcloud-occ user:add --password-from-env user1
      ${config.services.nextcloud.occ}/bin/nextcloud-occ user:setting user1 settings email "user1@localhost"
      ${config.services.nextcloud.occ}/bin/nextcloud-occ user:add --password-from-env user2
      ${config.services.nextcloud.occ}/bin/nextcloud-occ user:setting user2 settings email "user2@localhost"
      ${config.services.nextcloud.occ}/bin/nextcloud-occ user:setting admin settings email "admin@localhost"
    '';
    serviceConfig = {
      Type = "oneshot";
      User= "nextcloud";
    };
    after = [ "nextcloud-setup.service" ];
    wantedBy = [ "multi-user.target" ];
  };

[...]

💬 Are you interested in our work or have some questions? Join us in our public Signal chat pi crew 👋
🪙 If you like our work or want to supprot us, you can donate MobileCoins to our address.