1. Overview

Nextcloud runs on a NixOS VM at 192.168.3.225 (hostname: nextcloud).

URL

https://nextcloud.osisa.com:666

NixOS Version

25.11 (Xantusia)

Nextcloud Version

33.0.0

OS Config

/etc/nixos/configuration.nix + /etc/nixos/nextcloud.nix

Data

/var/lib/nextcloud

Database

PostgreSQL (local socket, DB nextcloud)

Cache

Redis (unix socket)

Web Server

nginx (ports 80, 443, 666)

Authentication

LDAP (Active Directory at 192.168.3.201)

SSH

root / NixNext2024$.

2. SSL Certificate Management

SSL is managed via osisa.SSL using Let’s Encrypt HTTP-01 challenge validation. The NixOS built-in ACME service (enableACME) is not used because port 80 is forwarded to a different server (the Windows Chocolatey server at 192.168.3.220).

2.1. Architecture

Internet --> Port 666 --> NixOS (nginx, SSL termination)
Internet --> Port 80  --> Windows Server (IIS/Chocolatey, ACME challenges)
  • Certificate issuer: Let’s Encrypt (R12)

  • Challenge type: HTTP-01

  • Challenge file location: \\192.168.3.220\c$\tools\chocolatey.server\.well-known\acme-challenge\

  • Cert files on NixOS: /var/lib/acme/nextcloud.osisa.com/ (fullchain.pem, key.pem)

  • Recovery file: /opt/osisa-ssl/nextcloud-ssl-recovery.json

  • CLI binary: /opt/osisa-ssl/osisa-ssl-cli (self-contained linux-x64 ELF)

2.2. IIS Web.Config for ACME Challenges

The Chocolatey Server on 192.168.3.220 runs IIS with an ASP.NET application that intercepts extensionless URLs. A custom web.config in .well-known/acme-challenge/ overrides this so Let’s Encrypt can fetch challenge files:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <handlers>
      <clear />
      <add name="StaticFile" path="*" verb="*"
           modules="StaticFileModule,DefaultDocumentModule"
           resourceType="Either" requireAccess="Read" />
    </handlers>
    <staticContent>
      <mimeMap fileExtension="." mimeType="text/plain" />
    </staticContent>
  </system.webServer>
  <system.web>
    <authorization>
      <allow users="*" />
    </authorization>
  </system.web>
</configuration>

Without this, IIS returns 404 for the extensionless ACME challenge token files.

2.3. Manual Certificate Generation

From a Windows workstation with access to both machines:

# 1. Init recovery file (only needed once)
dotnet run --project src/osisa.SSL.Cli -- init \
  --subject "C=CH, O=osisa GmbH, OU=IT" \
  --email info@osisa.com \
  --websites "nextcloud.osisa.com" \
  --export-dir ./certs/nextcloud \
  --export-name nextcloud \
  --production

# 2. Map the IIS challenge directory via SMB (PowerShell)
# $cred = New-Object PSCredential('osisa\amaske', (ConvertTo-SecureString 'NachtLicht.72' -AsPlainText -Force))
# New-PSDrive -Name CHOCO -PSProvider FileSystem -Root '\\192.168.3.220\c$' -Credential $cred
# subst Z: '\\192.168.3.220\c$\tools\chocolatey.server\.well-known\acme-challenge'

# 3. Generate cert via HTTP-01
dotnet run --project src/osisa.SSL.Cli -- generate-http \
  -r nextcloud-ssl-recovery.json \
  --challenge-path 'Z:\'

# 4. Upload to NixOS
pscp certs/nextcloud/nextcloud.pem root@192.168.3.225:/var/lib/acme/nextcloud.osisa.com/fullchain.pem
pscp certs/nextcloud/nextcloud.private.pem root@192.168.3.225:/var/lib/acme/nextcloud.osisa.com/key.pem

# 5. Fix permissions and reload
# ssh root@192.168.3.225
chown acme:nginx /var/lib/acme/nextcloud.osisa.com/fullchain.pem /var/lib/acme/nextcloud.osisa.com/key.pem
chmod 640 /var/lib/acme/nextcloud.osisa.com/fullchain.pem /var/lib/acme/nextcloud.osisa.com/key.pem
systemctl reload nginx

2.4. Automatic Monthly Renewal

A renewal script at /opt/osisa-ssl/renew-cert.sh handles automatic renewal:

  1. Mounts the Windows ACME challenge directory via CIFS

  2. Runs osisa-ssl-cli generate-http with the mounted challenge path

  3. Copies exported PEM files to /var/lib/acme/nextcloud.osisa.com/

  4. Reloads nginx

A systemd timer (osisa-ssl-renew.timer) triggers this monthly. Timer units are in /etc/systemd-overrides/ with runtime symlinks (volatile until NixOS rebuild integrates them).

The nextcloud.nix config already declares the timer for the next nixos-rebuild switch.

Note

The CIFS mount requires cifs-utils in the NixOS system packages (already added to configuration.nix). The Windows share credentials are: osisa\amaske / NachtLicht.72.

2.5. Publishing the osisa.SSL.Cli Linux Binary

cd C:/_gh/main/q/netbase
dotnet publish src/osisa.SSL.Cli -r linux-x64 --self-contained \
  -p:PublishSingleFile=true \
  -p:IncludeNativeLibrariesForSelfExtract=true \
  -o ./publish/ssl-cli-linux

# Upload to NixOS
pscp publish/ssl-cli-linux/osisa.SSL.Cli root@192.168.3.225:/opt/osisa-ssl/osisa-ssl-cli
# ssh root@192.168.3.225 chmod +x /opt/osisa-ssl/osisa-ssl-cli

3. Nextcloud Major Version Upgrades

Nextcloud does not support jumping across multiple major versions. You must upgrade one major version at a time (e.g., 28 → 29 → 30 → …​ → 33).

3.1. NixOS Channel / Nextcloud Version Matrix

Different NixOS channels ship different Nextcloud versions. You must switch channels as needed during the upgrade chain.

NixOS Channel Available Nextcloud Packages

nixos-23.11

nextcloud27, nextcloud28

nixos-24.05

nextcloud28, nextcloud29

nixos-25.05

nextcloud30, nextcloud31

nixos-25.11

nextcloud30, nextcloud31, nextcloud32, nextcloud33

Important

Older Nextcloud versions (e.g., NC30, NC31) may be marked as insecure (EOL) in newer NixOS channels. You must temporarily add them to nixpkgs.config.permittedInsecurePackages in configuration.nix:

nixpkgs.config = {
  allowUnfree = true;
  permittedInsecurePackages = [ "nextcloud-30.0.17" "nextcloud-31.0.14" ];
};

Remove this after completing the upgrade chain.

3.2. Upgrade Procedure (Step by Step)

For each major version step:

# 1. Switch NixOS channel (if needed)
nix-channel --remove nixos
nix-channel --add https://nixos.org/channels/<channel> nixos
nix-channel --update

# 2. Update packageName in /etc/nixos/nextcloud.nix
sed -i 's/packageName = "nextcloud[0-9]*"/packageName = "nextcloudXX"/' /etc/nixos/nextcloud.nix

# 3. Rebuild and switch
nixos-rebuild switch

# 4. The nextcloud-setup service runs occ upgrade automatically.
#    If it fails, run manually:
nextcloud-occ upgrade
nextcloud-occ maintenance:mode --off

# 5. Verify
curl -s https://localhost/status.php   # check version + maintenance=false

3.3. Example: Full Upgrade 28 → 33

This was performed on 2026-03-18:

NC28 (nixos-23.11)  -- starting point
  |
  v  Switch to nixos-24.05, set nextcloud29
NC29 (nixos-24.05)
  |
  v  Switch to nixos-25.05, set nextcloud30, allow insecure
NC30 (nixos-25.05)
  |
  v  Switch to nixos-25.11, set nextcloud31, allow insecure
NC31 (nixos-25.11)
  |
  v  set nextcloud32, allow insecure
NC32 (nixos-25.11)
  |
  v  set nextcloud33, remove insecure list
NC33 (nixos-25.11)  -- current

3.4. Known Issues During Upgrades

3.4.1. Redis RDB Format Incompatibility

When downgrading NixOS channels (e.g., from 25.11 back to 24.05), Redis may fail with:

Can't handle RDB format version 12

Fix: Delete the Redis dump file and restart:

rm -f /var/lib/redis-nextcloud/dump.rdb
systemctl restart redis-nextcloud

Redis is only used as a cache — no data is lost.

3.4.2. PostgreSQL Collation Version Mismatch

After upgrading glibc (via NixOS channel switch), PostgreSQL may warn:

database "nextcloud" has a collation version mismatch

Fix:

sudo -u postgres psql nextcloud -c 'ALTER DATABASE nextcloud REFRESH COLLATION VERSION;'

3.4.3. nc4nix Package Repository

The original config used nc4nix (Helsinki Systems) for extra Nextcloud apps. This was removed because:

  • nc4nix master branch doesn’t always have attributes for the latest NC version

  • NixOS 25.11 ships built-in Nextcloud app packages via config.services.nextcloud.package.packages.apps

Some apps previously from nc4nix are not available in the built-in packages: passwords, files_zip, files_fulltextsearch. These were removed from the extraApps declaration.

3.4.4. NixOS Config Deprecation Warnings (25.11)

NixOS 25.11 renamed several Nextcloud options. These still work but produce warnings:

Old New

services.nextcloud.extraOptions

services.nextcloud.settings

services.nextcloud.config.overwriteProtocol

services.nextcloud.settings.overwriteprotocol

services.nextcloud.config.defaultPhoneRegion

services.nextcloud.settings.default_phone_region

services.nextcloud.config.dbport

Removed (include port in dbhost)

programs.bash.enableCompletion

programs.bash.completion.enable

4. LDAP Authentication

Authentication is via Active Directory LDAP.

LDAP Server

192.168.3.201:389 (no TLS)

Base DN

ou=osisa,dc=corp,dc=osisa,dc=com

Bind DN

cn=nextadmin,ou=admins,ou=users,ou=osisa,dc=corp,dc=osisa,dc=com

User Filter

objectclass=user with memberof=CN=OSISA,OU=USERS,…​ or primaryGroupID=1139

Group Filter

objectclass=group, cn=OSISA

Login Attribute

samaccountname

The user_ldap app and its configuration survive Nextcloud upgrades (stored in oc_appconfig table).

Warning

During the NC28→33 upgrade, all non-core apps were temporarily disabled via direct DB update to work around a schema migration issue. After the upgrade chain completed, apps were re-enabled with:

UPDATE oc_appconfig SET configvalue='yes'
WHERE configkey='enabled'
AND appid IN ('files_sharing','files_trashbin','files_versions',
  'notifications','photos','comments','systemtags','text',
  'sharebymail','contactsinteraction','dashboard','user_ldap', ...);

Always verify LDAP and other critical apps are enabled after a major upgrade.

4.1. Verifying LDAP Status

# Check user_ldap is enabled
sudo -u postgres psql nextcloud -c \
  "SELECT configvalue FROM oc_appconfig WHERE appid='user_ldap' AND configkey='enabled';"

# Check LDAP config is active
sudo -u postgres psql nextcloud -c \
  "SELECT configvalue FROM oc_appconfig WHERE appid='user_ldap' AND configkey='s01ldap_configuration_active';"

# Check login page shows LDAP fields
curl -sk https://nextcloud.osisa.com:666/login | grep -o 'ldap'

5. NixOS Configuration Reference

5.1. Key Files

/etc/nixos/configuration.nix

Base system config (SSH, packages, firewall, boot)

/etc/nixos/nextcloud.nix

Nextcloud module (nginx, NC, PostgreSQL, SSL, renewal timer)

/etc/nixos/hardware-configuration.nix

Auto-generated hardware config

5.2. nginx Virtual Host

The nextcloud.nix configures nginx with manual SSL certificates:

virtualHosts = {
  "nextcloud.osisa.com" = {
    forceSSL = true;
    sslCertificate = "/opt/osisa-ssl/certs/fullchain.pem";      # (1)
    sslCertificateKey = "/opt/osisa-ssl/certs/key.pem";
    listen = [
      { addr = "0.0.0.0"; port = 443; ssl = true; }
      { addr = "0.0.0.0"; port = 666; ssl = true; }             # (2)
      { addr = "0.0.0.0"; port = 80; ssl = false; }
    ];
  };
};
  1. osisa.SSL-managed Let’s Encrypt certificates (not NixOS ACME)

  2. External access port (forwarded from router)

Note
The cert files are also copied to /var/lib/acme/nextcloud.osisa.com/ for compatibility with the original ACME-based config. Both paths work; the renewal script updates both.

5.3. Firewall

Ports 22 (SSH), 80, 443, and 666 are open.

6. Maintenance Commands

# Check Nextcloud status
nextcloud-occ status
curl -sk https://nextcloud.osisa.com:666/status.php

# Disable/enable maintenance mode
nextcloud-occ maintenance:mode --on
nextcloud-occ maintenance:mode --off

# Check SSL certificate
echo | openssl s_client -connect localhost:443 -servername nextcloud.osisa.com 2>/dev/null \
  | openssl x509 -noout -subject -dates

# Force SSL renewal
/opt/osisa-ssl/renew-cert.sh

# Check renewal timer
systemctl list-timers osisa-ssl-renew

# NixOS rebuild
nixos-rebuild switch

# Check NixOS version
nixos-version

7. Backup Considerations

  • Database: PostgreSQL nextcloud database. Use pg_dump or NixOS’s PostgreSQL backup services.

  • Data: /var/lib/nextcloud/data contains user files.

  • Config: /var/lib/nextcloud/config/config.php contains the Nextcloud configuration.

  • SSL Recovery: /opt/osisa-ssl/nextcloud-ssl-recovery.json contains the ACME account keys and certificate state. Back this up to regenerate certificates without creating a new account.

  • NixOS Config: /etc/nixos/ — the full system is reproducible from these files.

8. Appendix: Current nextcloud.nix (2026-03-18)

# Nextcloud Nix Configuration Module
{self, config, lib, pkgs, ...}: let

  email = "info@osisa.com";
  #ncConfig = builtins.fromJSON (builtins.readFile ./nc.json);
  packageName = "nextcloud33";
  #ncConfig = import ./nc.nix;

in {
  # use sops-nix
  environment.etc = {
    "nextcloud-db-pass".text = "VXQgaW4gYWxpcXV5YW0gbnVsbGEgZG9sb3IganVzdG8gdm9sdXB0dWEgZHVvIGVhIGVsaXQgdGFraW1hdGEgZWlybW9kIGV0IGlwc3VtIG1hZ25hLg==";
    "nextcloud-admin-pass".text = "NixNext2024$.";
    "nextcloud-secrets.json".text = ''
    {
      "secret": "zjJL3HL0ONNBa4eU1tF93p3nqB5RWW",
      "instanceid": "a9H6YQXbj7h9EaqTsRHw9XBtJaMGRN"
    }
    '';
  };

  users.users.nextcloud.extraGroups = ["render" "users"];

  networking.firewall.allowedTCPPorts = [ 80 443 666 ];
  security.acme = {
    acceptTerms = true;
    defaults.email = email;
    #defaults.email = ncConfig.acmeEmail;
  };
  services = {
    nginx = {
      enable = true;

      # Use recommended settings
      recommendedGzipSettings = true;
      recommendedOptimisation = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;

      # Only allow PFS-enabled ciphers with AES256
      sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";

      # Setup Nextcloud virtual host to listen on ports
      virtualHosts = {
        "nextcloud.osisa.com" = {
          forceSSL = true;
          sslCertificate = "/opt/osisa-ssl/certs/fullchain.pem";
          sslCertificateKey = "/opt/osisa-ssl/certs/key.pem";
          listen = [
            { addr = "0.0.0.0"; port = 443; ssl = true; }
            { addr = "0.0.0.0"; port = 666; ssl = true; }
            { addr = "0.0.0.0"; port = 80; ssl = false; }
          ];
        };
      };
    };

    nextcloud = {
      # Always check these
      enable = true;
      package = pkgs.${packageName};
      hostName = "nextcloud.osisa.com";
      maxUploadSize = "8G";

      # Can leave as is
      https = true;
      home = "/var/lib/nextcloud";
      notify_push.enable = false;
      configureRedis = true;
      database.createLocally = false;
      autoUpdateApps = {
        enable = true;
        startAt = "05:00:00";
      };
      appstoreEnable = false; # should be false if apps are declared below
      extraAppsEnable = true;
      extraApps =
        # nc4nix removed - using built-in NixOS packages only
        with config.services.nextcloud.package.packages.apps;
      {
        inherit
        contacts
        notes
        calendar
        deck
        tasks
        spreed
        groupfolders
        mail
        unroundedcorners
        #richdocuments
        #richdocumentscode
        ; # intentional LEAVE IT
      };

      phpExtraExtensions = all: [ all.ldap ];

      config = {
        # Further forces Nextcloud to use HTTPS
        overwriteProtocol = "https";

        # Nextcloud PostegreSQL database configuration
        dbtype = "pgsql";
        dbuser = "nextcloud";
        dbhost = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
        dbname = "nextcloud";
        dbpassFile = "/etc/nextcloud-db-pass";

        adminpassFile = "/etc/nextcloud-admin-pass";
        adminuser = "nextadmin";

        defaultPhoneRegion = "CH";
      };

      extraOptions = {
        enabledPreviewProviders = [
          "OC\\Preview\\BMP"
          "OC\\Preview\\GIF"
          "OC\\Preview\\JPEG"
          "OC\\Preview\\Krita"
          "OC\\Preview\\MarkDown"
          "OC\\Preview\\MP3"
          "OC\\Preview\\OpenDocument"
          "OC\\Preview\\PNG"
          "OC\\Preview\\TXT"
          "OC\\Preview\\XBitmap"
          "OC\\Preview\\HEIC"
        ];
      };

      phpOptions = {
        "opcache.interned_strings_buffer" = "16";
      };
    };

    postgresql = {
      enable = true;

      # Ensure the database, user, and permissions always exist
      ensureDatabases = [ "nextcloud" ];
      ensureUsers = [
        {
          name = "nextcloud";
          ensureDBOwnership = true;
        }
      ];
    };
  };

  systemd.services."nextcloud-setup" = {
    requires = ["postgresql.service"];
    after = ["postgresql.service"];
  };

  # osisa.SSL certificate renewal - runs monthly via HTTP-01
  systemd.services."osisa-ssl-renew" = {
    description = "Renew SSL certificate for nextcloud.osisa.com via osisa.SSL";
    serviceConfig = {
      Type = "oneshot";
      ExecStart = "/opt/osisa-ssl/renew-cert.sh";
    };
    path = [ pkgs.cifs-utils pkgs.util-linux ];
  };

  systemd.timers."osisa-ssl-renew" = {
    description = "Monthly SSL certificate renewal timer";
    wantedBy = [ "timers.target" ];
    timerConfig = {
      OnCalendar = "monthly";
      Persistent = true;
      RandomizedDelaySec = "1h";
    };
  };
}