{ config, lib, pkgs, homeyConfig, ... }: # Nextcloud + PostgreSQL. # # Two containers: # nextcloud-postgres — PostgreSQL, bound to localhost:5432 # nextcloud — Nextcloud PHP-FPM + Apache, bound to localhost:8080 # # Volume layout: # /nextcloud/db/ → /var/lib/postgresql/data (postgres) # /nextcloud/html/ → /var/www/html (nextcloud) # # Secrets consumed from sops: # nextcloud/admin_password # nextcloud/postgres_password let cfg = config.homey.nextcloud; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; # Custom Nextcloud config mounted into the container as an extra config file. # Nextcloud auto-loads all *.config.php files in /var/www/html/config/. nextcloudCustomConfig = pkgs.writeText "zakobar.config.php" '' 1, 'preview_concurrency_all' => 1, // Cap preview dimensions to reduce per-preview write size. 'preview_max_x' => 1024, 'preview_max_y' => 1024, 'jpeg_quality' => 75, ]; ''; # Limit Apache's prefork MPM so at most 4 PHP processes write to the USB # drive simultaneously. Default is often 150, which causes an I/O storm # on slow USB HDDs. Lower = fewer concurrent writers = more stable I/O. apacheMpmConfig = pkgs.writeText "mpm_prefork.conf" '' StartServers 2 MinSpareServers 1 MaxSpareServers 3 MaxRequestWorkers 4 MaxConnectionsPerChild 500 ''; in { options.homey.nextcloud = { enable = lib.mkEnableOption "Nextcloud file server" // { default = true; }; image = lib.mkOption { type = lib.types.str; default = "docker.io/nextcloud:latest"; }; postgresImage = lib.mkOption { type = lib.types.str; default = "docker.io/postgres:16"; }; port = lib.mkOption { type = lib.types.port; default = 8080; description = "Host port Nextcloud listens on (bound to 127.0.0.1)."; }; postgresPort = lib.mkOption { type = lib.types.port; default = 5432; description = "Host port PostgreSQL listens on (bound to 127.0.0.1)."; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."nextcloud/admin_password" = { owner = "root"; }; sops.secrets."nextcloud/postgres_password" = { owner = "root"; }; # ----------------------------------------------------------------------- # PostgreSQL container # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.nextcloud-postgres = { image = cfg.postgresImage; # Exposed on localhost for debugging; nextcloud reaches it via the # container name "nextcloud-postgres" on the homey network. ports = [ "127.0.0.1:${toString cfg.postgresPort}:5432" ]; environment = { POSTGRES_DB = "nextcloud_db"; POSTGRES_USER = "postgres"; # Password injected via env file }; volumes = [ "${dataDir}/nextcloud/db:/var/lib/postgresql/data" ]; extraOptions = [ "--network=homey" "--env-file=/run/nc-postgres-secrets.env" ]; }; systemd.services."podman-nextcloud-postgres" = { serviceConfig = { LoadCredential = [ "nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}" ]; ExecStartPre = [ (pkgs.writeShellScript "nc-postgres-secrets-env" '' set -euo pipefail install -m 600 /dev/null /run/nc-postgres-secrets.env echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" \ >> /run/nc-postgres-secrets.env '') ]; }; postStop = "rm -f /run/nc-postgres-secrets.env"; after = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-homey-network.service" ]; }; # ----------------------------------------------------------------------- # Nextcloud container # ----------------------------------------------------------------------- virtualisation.oci-containers.containers.nextcloud = { image = cfg.image; # Apache inside the container listens on port 80; map it to cfg.port on # the host so Caddy can reach it. Postgres is reachable by container name. ports = [ "127.0.0.1:${toString cfg.port}:80" ]; environment = { POSTGRES_HOST = "nextcloud-postgres"; POSTGRES_DB = "nextcloud_db"; POSTGRES_USER = "postgres"; NEXTCLOUD_ADMIN_USER = "admin"; NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}"; OVERWRITEPROTOCOL = "https"; OVERWRITECLIURL = "https://nextcloud.${domain}"; OVERWRITEHOST = "nextcloud.${domain}"; # Trust the reverse proxy (Caddy on the host reaches the container # via the podman bridge; cover all RFC-1918 ranges to be robust). TRUSTED_PROXIES = "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.1 ::1"; # Passwords injected via env file }; volumes = [ "${dataDir}/nextcloud/html:/var/www/html" # Extra config auto-loaded by Nextcloud (throttles preview generation) "${nextcloudCustomConfig}:/var/www/html/config/zakobar.config.php:ro" # Apache MPM limits (caps concurrent PHP processes / disk writers) "${apacheMpmConfig}:/etc/apache2/mods-available/mpm_prefork.conf:ro" ]; extraOptions = [ "--network=homey" "--env-file=/run/nc-secrets.env" ]; }; # ----------------------------------------------------------------------- # Authelia access control — one_factor; Nextcloud manages its own login UI. # ----------------------------------------------------------------------- homey.authelia.accessControlRules = [{ priority = 55; domain = [ "nextcloud.${domain}" ]; policy = "one_factor"; }]; # ----------------------------------------------------------------------- # Caddy virtual host — no forward_auth; Nextcloud manages its own auth # ----------------------------------------------------------------------- homey.caddy.virtualHosts = [{ subdomain = "nextcloud"; port = cfg.port; auth = false; extraConfig = '' redir /.well-known/carddav /remote.php/dav/ 301 redir /.well-known/caldav /remote.php/dav/ 301 request_body { max_size 5GB } reverse_proxy localhost:${toString cfg.port} { header_up X-Forwarded-For {remote_host} } ''; extraHttpConfig = '' redir /.well-known/carddav /remote.php/dav/ 301 redir /.well-known/caldav /remote.php/dav/ 301 request_body { max_size 5GB } reverse_proxy localhost:${toString cfg.port} { header_up X-Forwarded-Proto https header_up X-Forwarded-For {remote_host} } ''; }]; # ----------------------------------------------------------------------- # Storage directories # UID 33 = www-data in the Nextcloud container # UID 999 = postgres — must own the db dir (creates files directly in it) # ----------------------------------------------------------------------- homey.storage.extraDirs = [ { path = "nextcloud"; } { path = "nextcloud/html"; user = "33"; group = "33"; } { path = "nextcloud/db"; mode = "0700"; user = "999"; group = "999"; } { path = "nextcloud/db-dump"; mode = "0700"; } ]; # ----------------------------------------------------------------------- # Backup — exclude raw DB dir (pg_dump file in db-dump/ is used instead) # ----------------------------------------------------------------------- homey.backup.extraPaths = [ "${dataDir}/nextcloud" ]; homey.backup.extraExcludePaths = [ "${dataDir}/nextcloud/db" ]; # ----------------------------------------------------------------------- # Uptime Kuma monitor for this service # ----------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "Nextcloud"; url = "https://nextcloud.${domain}/status.php"; interval = 60; keyword = "\"maintenance\":false"; # Nightly maintenance is expected — only alert if stuck for 4+ hours. # 240 retries × 60s = 4 hours of consecutive failures before notifying. maxretries = 240; }]; systemd.services."podman-nextcloud" = { serviceConfig = { LoadCredential = [ "nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}" "nextcloud_admin_password:${config.sops.secrets."nextcloud/admin_password".path}" ]; ExecStartPre = [ (pkgs.writeShellScript "nc-secrets-env" '' set -euo pipefail install -m 600 /dev/null /run/nc-secrets.env echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" >> /run/nc-secrets.env echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_admin_password")" >> /run/nc-secrets.env '') ]; }; postStop = "rm -f /run/nc-secrets.env"; after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" "podman-homey-network.service" ]; requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" "podman-homey-network.service" ]; }; }; }