260 lines
10 KiB
Nix
260 lines
10 KiB
Nix
{ 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:
|
||
# <dataDir>/nextcloud/db/ → /var/lib/postgresql/data (postgres)
|
||
# <dataDir>/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" ''
|
||
<?php
|
||
$CONFIG = [
|
||
// Throttle preview generation during bulk uploads.
|
||
// Generating thumbnails re-reads every uploaded file and writes preview
|
||
// files, roughly doubling disk I/O. Limiting concurrency to 1 prevents
|
||
// the drive from being hit by simultaneous read+write storms.
|
||
'preview_concurrency_new' => 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" ''
|
||
<IfModule mpm_prefork_module>
|
||
StartServers 2
|
||
MinSpareServers 1
|
||
MaxSpareServers 3
|
||
MaxRequestWorkers 4
|
||
MaxConnectionsPerChild 500
|
||
</IfModule>
|
||
'';
|
||
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" ];
|
||
};
|
||
};
|
||
}
|