1. Overview
Nextcloud runs on a NixOS VM at 192.168.3.225 (hostname: nextcloud).
URL |
|
NixOS Version |
25.11 (Xantusia) |
Nextcloud Version |
33.0.0 |
OS Config |
|
Data |
|
Database |
PostgreSQL (local socket, DB |
Cache |
Redis (unix socket) |
Web Server |
nginx (ports 80, 443, 666) |
Authentication |
LDAP (Active Directory at |
SSH |
|
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:
-
Mounts the Windows ACME challenge directory via CIFS
-
Runs
osisa-ssl-cli generate-httpwith the mounted challenge path -
Copies exported PEM files to
/var/lib/acme/nextcloud.osisa.com/ -
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 |
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
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 |
|---|---|
|
|
|
|
|
|
|
Removed (include port in |
|
|
4. LDAP Authentication
Authentication is via Active Directory LDAP.
LDAP Server |
|
Base DN |
|
Bind DN |
|
User Filter |
|
Group Filter |
|
Login Attribute |
|
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:
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
|
Base system config (SSH, packages, firewall, boot) |
|
Nextcloud module (nginx, NC, PostgreSQL, SSL, renewal timer) |
|
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; }
];
};
};
-
osisa.SSL-managed Let’s Encrypt certificates (not NixOS ACME)
-
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
nextclouddatabase. Usepg_dumpor NixOS’s PostgreSQL backup services. -
Data:
/var/lib/nextcloud/datacontains user files. -
Config:
/var/lib/nextcloud/config/config.phpcontains the Nextcloud configuration. -
SSL Recovery:
/opt/osisa-ssl/nextcloud-ssl-recovery.jsoncontains 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";
};
};
}