Port to NixOS: replace Helm chart with flake-based NixOS config
Replaces the Helm/k3s setup with a declarative NixOS configuration targeting
a Raspberry Pi 4. Services run as podman containers under systemd, with data
on an external HD at /mnt/data. Key components:
- flake.nix: multi-host flake with pi-main (aarch64) and a placeholder for a
second machine
- modules/common.nix: shared system config (nix, podman, sops, SSH)
- modules/storage.nix: external HD mount with per-service subdirs
- modules/caddy.nix: Caddy with cloudflare DNS-01 ACME + authelia forward_auth
- modules/cloudflared.nix: Cloudflare tunnel for remote access
- modules/backup.nix: restic daily backups with NC maintenance mode pre-hook
- modules/services/{openldap,authelia,gitea,nextcloud,phpldapadmin}.nix: core services
- modules/services/{jellyfin,transmission}.nix: media services (disabled by default)
- secrets/: sops-nix scaffold with .sops.yaml age key config
- hosts/pi-main/: hardware config + service selection for the Pi
- PORTING.md: step-by-step migration guide (SD card → data restore → verify)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# Authelia — SSO gateway.
|
||||
#
|
||||
# Connects to OpenLDAP on 127.0.0.1:389.
|
||||
# Exposes port 9091 on localhost; Caddy reverse-proxies it and provides
|
||||
# the forward_auth endpoint for protected vhosts.
|
||||
#
|
||||
# Volume layout:
|
||||
# <dataDir>/authelia/config/ → /config (sqlite db, notification log, etc.)
|
||||
#
|
||||
# The configuration file is rendered by Nix (no Go templates) and written
|
||||
# to a NixOS-managed path, then bind-mounted read-only into the container.
|
||||
#
|
||||
# Secrets consumed from sops:
|
||||
# authelia/jwt_secret
|
||||
# authelia/session_secret
|
||||
# authelia/storage_encryption_key
|
||||
# openldap/ro_password (shared with openldap module)
|
||||
|
||||
let
|
||||
cfg = config.homey.authelia;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
domain = homeyConfig.domain;
|
||||
|
||||
# LDAP base DN derived from domain: home.zakobar.com → dc=home,dc=zakobar,dc=com
|
||||
ldapBaseDN = lib.concatStringsSep ","
|
||||
(map (p: "dc=${p}") (lib.splitString "." domain));
|
||||
|
||||
# The authelia config is written as a Nix string so all values are
|
||||
# resolved at build time except for secrets, which are injected at
|
||||
# runtime via a wrapper script (same pattern as openldap).
|
||||
autheliaConfig = ''
|
||||
###############################################################
|
||||
# Authelia configuration #
|
||||
# Generated by NixOS — do not edit by hand #
|
||||
###############################################################
|
||||
theme: "light"
|
||||
log:
|
||||
level: "info"
|
||||
|
||||
# jwt_secret injected at runtime via env var AUTHELIA_JWT_SECRET_FILE
|
||||
authentication_backend:
|
||||
ldap:
|
||||
implementation: "custom"
|
||||
url: "ldap://127.0.0.1:389"
|
||||
timeout: "5s"
|
||||
start_tls: false
|
||||
base_dn: "${ldapBaseDN}"
|
||||
users_filter: "({username_attribute}={input})"
|
||||
username_attribute: "uid"
|
||||
additional_users_dn: "ou=users"
|
||||
groups_filter: "(&(uniquemember=uid={input},ou=users,${ldapBaseDN})(objectclass=groupOfUniqueNames))"
|
||||
group_name_attribute: "cn"
|
||||
additional_groups_dn: "ou=groups"
|
||||
mail_attribute: "mail"
|
||||
display_name_attribute: "uid"
|
||||
permit_referrals: false
|
||||
permit_unauthenticated_bind: false
|
||||
user: "cn=readonly,${ldapBaseDN}"
|
||||
# password injected at runtime via AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE
|
||||
|
||||
totp:
|
||||
issuer: "${domain}"
|
||||
disable: false
|
||||
|
||||
session:
|
||||
name: authelia_session
|
||||
# secret injected at runtime via AUTHELIA_SESSION_SECRET_FILE
|
||||
expiration: 3600
|
||||
inactivity: 7200
|
||||
domain: "${domain}"
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: "/config/db.sqlite3"
|
||||
# encryption_key injected at runtime via AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
|
||||
|
||||
access_control:
|
||||
default_policy: "deny"
|
||||
rules:
|
||||
- domain:
|
||||
- "auth.${domain}"
|
||||
policy: "bypass"
|
||||
- domain:
|
||||
- "ldapadmin.${domain}"
|
||||
subject:
|
||||
- "group:admins"
|
||||
policy: "two_factor"
|
||||
- domain:
|
||||
- "ldapadmin.${domain}"
|
||||
policy: "deny"
|
||||
- domain:
|
||||
- "torrent.${domain}"
|
||||
subject:
|
||||
- "group:admins"
|
||||
policy: "two_factor"
|
||||
- domain:
|
||||
- "torrent.${domain}"
|
||||
policy: "deny"
|
||||
- domain:
|
||||
- "git.${domain}"
|
||||
policy: "one_factor"
|
||||
- domain:
|
||||
- "nextcloud.${domain}"
|
||||
policy: "one_factor"
|
||||
- domain:
|
||||
- "jellyfin.${domain}"
|
||||
policy: "one_factor"
|
||||
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: "/config/emails.txt"
|
||||
|
||||
ntp:
|
||||
address: "udp://time.cloudflare.com:123"
|
||||
version: 3
|
||||
max_desync: "3s"
|
||||
disable_startup_check: false
|
||||
disable_failure: true
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.homey.authelia = {
|
||||
enable = lib.mkEnableOption "Authelia SSO gateway";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/authelia/authelia:latest";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 9091;
|
||||
description = "Host port Authelia listens on (bound to 127.0.0.1).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# -----------------------------------------------------------------------
|
||||
# Secrets
|
||||
# -----------------------------------------------------------------------
|
||||
sops.secrets."authelia/jwt_secret" = { owner = "root"; };
|
||||
sops.secrets."authelia/session_secret" = { owner = "root"; };
|
||||
sops.secrets."authelia/storage_encryption_key" = { owner = "root"; };
|
||||
# openldap/ro_password is declared in openldap.nix; reference it here too
|
||||
# (sops-nix deduplicates identical declarations)
|
||||
sops.secrets."openldap/ro_password" = { owner = "root"; };
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Write the config file into /etc (read-only in the container)
|
||||
# -----------------------------------------------------------------------
|
||||
environment.etc."authelia/configuration.yml" = {
|
||||
text = autheliaConfig;
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Container
|
||||
# -----------------------------------------------------------------------
|
||||
virtualisation.oci-containers.containers.authelia = {
|
||||
image = cfg.image;
|
||||
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
|
||||
|
||||
environment = {
|
||||
TZ = homeyConfig.timezone;
|
||||
# Tell authelia to read secrets from files (its native mechanism)
|
||||
AUTHELIA_JWT_SECRET_FILE = "/run/secrets/jwt_secret";
|
||||
AUTHELIA_SESSION_SECRET_FILE = "/run/secrets/session_secret";
|
||||
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = "/run/secrets/storage_encryption_key";
|
||||
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = "/run/secrets/ldap_ro_password";
|
||||
};
|
||||
|
||||
volumes = [
|
||||
"/etc/authelia/configuration.yml:/config/configuration.yml:ro"
|
||||
"${dataDir}/authelia/config:/config"
|
||||
# Mount sops secret files into the container under /run/secrets/
|
||||
"${config.sops.secrets."authelia/jwt_secret".path}:/run/secrets/jwt_secret:ro"
|
||||
"${config.sops.secrets."authelia/session_secret".path}:/run/secrets/session_secret:ro"
|
||||
"${config.sops.secrets."authelia/storage_encryption_key".path}:/run/secrets/storage_encryption_key:ro"
|
||||
"${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap_ro_password:ro"
|
||||
];
|
||||
|
||||
extraOptions = [
|
||||
"--network=host"
|
||||
"--hostname=authelia"
|
||||
];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Systemd — wait for openldap and external HD
|
||||
# -----------------------------------------------------------------------
|
||||
systemd.services."podman-authelia" = {
|
||||
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# Gitea — self-hosted Git service.
|
||||
#
|
||||
# Auth model: LDAP authentication is configured through Gitea's admin UI
|
||||
# (or CLI) after first start. Reverse proxy auth headers from Caddy/Authelia
|
||||
# handle transparent login.
|
||||
#
|
||||
# Volume layout:
|
||||
# <dataDir>/gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.)
|
||||
#
|
||||
# The app.ini is rendered by Nix and bind-mounted read-only.
|
||||
#
|
||||
# Secrets consumed from sops:
|
||||
# gitea/admin_password
|
||||
# gitea/lfs_jwt_secret
|
||||
# gitea/oauth2_jwt_secret
|
||||
# gitea/internal_token
|
||||
|
||||
let
|
||||
cfg = config.homey.gitea;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
domain = homeyConfig.domain;
|
||||
|
||||
# Gitea app.ini — generated at build time.
|
||||
# Secrets that Gitea reads from env vars are referenced as env var names here.
|
||||
# The actual values are injected by the ExecStartPre wrapper below.
|
||||
giteaAppIni = ''
|
||||
APP_NAME = ${homeyConfig.organization}
|
||||
RUN_MODE = prod
|
||||
RUN_USER = git
|
||||
WORK_PATH = /data/gitea
|
||||
|
||||
[repository]
|
||||
ROOT = /data/git/repositories
|
||||
|
||||
[repository.local]
|
||||
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
|
||||
|
||||
[repository.upload]
|
||||
TEMP_PATH = /data/gitea/uploads
|
||||
|
||||
[server]
|
||||
APP_DATA_PATH = /data/gitea
|
||||
DOMAIN = git.${domain}
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = https://git.${domain}/
|
||||
DISABLE_SSH = true
|
||||
LFS_START_SERVER = true
|
||||
; LFS_JWT_SECRET injected at container start via env var / startup script
|
||||
LFS_JWT_SECRET = __GITEA_LFS_JWT_SECRET__
|
||||
OFFLINE_MODE = false
|
||||
|
||||
[lfs]
|
||||
PATH = /data/git/lfs
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /data/gitea/gitea.db
|
||||
LOG_SQL = false
|
||||
|
||||
[indexer]
|
||||
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
|
||||
|
||||
[session]
|
||||
PROVIDER_CONFIG = /data/gitea/sessions
|
||||
PROVIDER = file
|
||||
|
||||
[picture]
|
||||
AVATAR_UPLOAD_PATH = /data/gitea/avatars
|
||||
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
|
||||
DISABLE_GRAVATAR = false
|
||||
|
||||
[attachment]
|
||||
PATH = /data/gitea/attachments
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = info
|
||||
ROUTER = console
|
||||
ROOT_PATH = /data/gitea/log
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
REVERSE_PROXY_LIMIT = 1
|
||||
REVERSE_PROXY_TRUSTED_PROXIES = *
|
||||
; INTERNAL_TOKEN injected at container start
|
||||
INTERNAL_TOKEN = __GITEA_INTERNAL_TOKEN__
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
REGISTER_EMAIL_CONFIRM = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = true
|
||||
ENABLE_CAPTCHA = false
|
||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||
DEFAULT_ENABLE_TIMETRACKING = true
|
||||
NO_REPLY_ADDRESS = noreply.localhost
|
||||
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
|
||||
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true
|
||||
|
||||
[mailer]
|
||||
ENABLED = false
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = false
|
||||
ENABLE_OPENID_SIGNUP = false
|
||||
|
||||
[oauth2]
|
||||
ENABLE = false
|
||||
; JWT_SECRET injected at container start
|
||||
JWT_SECRET = __GITEA_OAUTH2_JWT_SECRET__
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
options.homey.gitea = {
|
||||
enable = lib.mkEnableOption "Gitea Git server";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/gitea/gitea:latest";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 3000;
|
||||
description = "Host port Gitea listens on (bound to 127.0.0.1).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# -----------------------------------------------------------------------
|
||||
# Secrets
|
||||
# -----------------------------------------------------------------------
|
||||
sops.secrets."gitea/admin_password" = { owner = "root"; };
|
||||
sops.secrets."gitea/lfs_jwt_secret" = { owner = "root"; };
|
||||
sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; };
|
||||
sops.secrets."gitea/internal_token" = { owner = "root"; };
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Write the app.ini template to /etc (will be processed by ExecStartPre)
|
||||
# -----------------------------------------------------------------------
|
||||
environment.etc."gitea/app.ini.tpl" = {
|
||||
text = giteaAppIni;
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Container
|
||||
# -----------------------------------------------------------------------
|
||||
virtualisation.oci-containers.containers.gitea = {
|
||||
image = cfg.image;
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:3000" ];
|
||||
|
||||
environment = {
|
||||
USER_UID = "1000";
|
||||
USER_GID = "1000";
|
||||
# Tell gitea where to look for the config
|
||||
GITEA_CUSTOM = "/data/gitea";
|
||||
};
|
||||
|
||||
volumes = [
|
||||
"${dataDir}/gitea/data:/data"
|
||||
# The processed app.ini is written by ExecStartPre into /run/gitea-conf/
|
||||
"/run/gitea-conf/app.ini:/data/gitea/conf/app.ini:ro"
|
||||
];
|
||||
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# ExecStartPre: substitute secret placeholders into the ini template
|
||||
# -----------------------------------------------------------------------
|
||||
systemd.services."podman-gitea" = {
|
||||
serviceConfig = {
|
||||
ExecStartPre = [
|
||||
(pkgs.writeShellScript "gitea-build-config" ''
|
||||
set -euo pipefail
|
||||
install -d -m 700 /run/gitea-conf
|
||||
LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path})
|
||||
OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path})
|
||||
TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path})
|
||||
sed \
|
||||
-e "s|__GITEA_LFS_JWT_SECRET__|$LFS|g" \
|
||||
-e "s|__GITEA_OAUTH2_JWT_SECRET__|$OAUTH|g" \
|
||||
-e "s|__GITEA_INTERNAL_TOKEN__|$TOKEN|g" \
|
||||
/etc/gitea/app.ini.tpl > /run/gitea-conf/app.ini
|
||||
chmod 444 /run/gitea-conf/app.ini
|
||||
'')
|
||||
];
|
||||
};
|
||||
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# Jellyfin — media server. (Deferred — enable when ready.)
|
||||
#
|
||||
# Volume layout:
|
||||
# <dataDir>/jellyfin/config/ → /config
|
||||
# <dataDir>/media/movies/ → /data/movies
|
||||
# <dataDir>/media/tvshows/ → /data/tvshows
|
||||
|
||||
let
|
||||
cfg = config.homey.jellyfin;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
domain = homeyConfig.domain;
|
||||
in
|
||||
{
|
||||
options.homey.jellyfin = {
|
||||
enable = lib.mkEnableOption "Jellyfin media server";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/jellyfin/jellyfin:latest";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8096;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
virtualisation.oci-containers.containers.jellyfin = {
|
||||
image = cfg.image;
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:8096" ];
|
||||
|
||||
environment = {
|
||||
JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}";
|
||||
PUID = "1000";
|
||||
PGID = "1000";
|
||||
};
|
||||
|
||||
volumes = [
|
||||
"${dataDir}/jellyfin/config:/config"
|
||||
"${dataDir}/media/movies:/data/movies:ro"
|
||||
"${dataDir}/media/tvshows:/data/tvshows:ro"
|
||||
];
|
||||
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
systemd.services."podman-jellyfin" = {
|
||||
after = lib.mkAfter [ "mnt-data.mount" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{ 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;
|
||||
in
|
||||
{
|
||||
options.homey.nextcloud = {
|
||||
enable = lib.mkEnableOption "Nextcloud file server";
|
||||
|
||||
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;
|
||||
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=host" ];
|
||||
};
|
||||
|
||||
systemd.services."podman-nextcloud-postgres" = {
|
||||
serviceConfig = {
|
||||
ExecStartPre = [
|
||||
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
|
||||
set -euo pipefail
|
||||
install -m 600 /dev/null /run/nc-postgres-secrets.env
|
||||
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" \
|
||||
>> /run/nc-postgres-secrets.env
|
||||
'')
|
||||
];
|
||||
EnvironmentFile = "/run/nc-postgres-secrets.env";
|
||||
};
|
||||
postStop = "rm -f /run/nc-postgres-secrets.env";
|
||||
after = lib.mkAfter [ "mnt-data.mount" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Nextcloud container
|
||||
# -----------------------------------------------------------------------
|
||||
virtualisation.oci-containers.containers.nextcloud = {
|
||||
image = cfg.image;
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
||||
|
||||
environment = {
|
||||
POSTGRES_HOST = "127.0.0.1";
|
||||
POSTGRES_DB = "nextcloud_db";
|
||||
POSTGRES_USER = "postgres";
|
||||
NEXTCLOUD_ADMIN_USER = "admin";
|
||||
NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}";
|
||||
OVERWRITEPROTOCOL = "https";
|
||||
OVERWRITECLIURL = "https://nextcloud.${domain}";
|
||||
# Passwords injected via env file
|
||||
};
|
||||
|
||||
volumes = [
|
||||
"${dataDir}/nextcloud/html:/var/www/html"
|
||||
];
|
||||
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
systemd.services."podman-nextcloud" = {
|
||||
serviceConfig = {
|
||||
ExecStartPre = [
|
||||
(pkgs.writeShellScript "nc-secrets-env" ''
|
||||
set -euo pipefail
|
||||
install -m 600 /dev/null /run/nc-secrets.env
|
||||
echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" >> /run/nc-secrets.env
|
||||
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat ${config.sops.secrets."nextcloud/admin_password".path})" >> /run/nc-secrets.env
|
||||
'')
|
||||
];
|
||||
EnvironmentFile = "/run/nc-secrets.env";
|
||||
};
|
||||
postStop = "rm -f /run/nc-secrets.env";
|
||||
after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# OpenLDAP — central identity provider.
|
||||
#
|
||||
# Runs as a podman container (osixia/openldap).
|
||||
# Listens on localhost:389 only — not exposed to the outside world.
|
||||
# Authelia and other services connect to it over the container network (127.0.0.1).
|
||||
#
|
||||
# Volume layout on host:
|
||||
# <dataDir>/openldap/etc-ldap-slapd.d/ → /etc/ldap/slapd.d (config DB)
|
||||
# <dataDir>/openldap/var-lib-ldap/ → /var/lib/ldap (data)
|
||||
#
|
||||
# Secrets consumed from sops:
|
||||
# openldap/admin_password
|
||||
# openldap/config_password
|
||||
# openldap/ro_password
|
||||
|
||||
let
|
||||
cfg = config.homey.openldap;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
in
|
||||
{
|
||||
options.homey.openldap = {
|
||||
enable = lib.mkEnableOption "OpenLDAP identity provider";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/osixia/openldap:latest";
|
||||
description = "Container image to use for OpenLDAP.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 389;
|
||||
description = "Host port OpenLDAP listens on (bound to 127.0.0.1).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# -----------------------------------------------------------------------
|
||||
# Secrets
|
||||
# -----------------------------------------------------------------------
|
||||
sops.secrets."openldap/admin_password" = { owner = "root"; };
|
||||
sops.secrets."openldap/config_password" = { owner = "root"; };
|
||||
sops.secrets."openldap/ro_password" = { owner = "root"; };
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Container
|
||||
# -----------------------------------------------------------------------
|
||||
virtualisation.oci-containers.containers.openldap = {
|
||||
image = cfg.image;
|
||||
|
||||
# Bind only to localhost — no external exposure
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:389" ];
|
||||
|
||||
environment = {
|
||||
LDAP_ORGANISATION = homeyConfig.organization;
|
||||
LDAP_DOMAIN = homeyConfig.domain;
|
||||
LDAP_ADMIN_USERNAME = "admin";
|
||||
LDAP_READONLY_USER = "true";
|
||||
# TLS disabled — traffic stays on localhost
|
||||
LDAP_TLS = "false";
|
||||
};
|
||||
|
||||
# Inject passwords from sops-managed secret files
|
||||
environmentFiles = []; # we use secretFiles below instead
|
||||
|
||||
# sops writes secret values to files; we read them into env vars
|
||||
# via a wrapper script run as ExecStartPre (see systemd override below).
|
||||
# Podman's --env-file doesn't support arbitrary paths, so we use
|
||||
# a secrets tmpfile approach via the systemd unit override.
|
||||
|
||||
volumes = [
|
||||
"${dataDir}/openldap/etc-ldap-slapd.d:/etc/ldap/slapd.d"
|
||||
"${dataDir}/openldap/var-lib-ldap:/var/lib/ldap"
|
||||
];
|
||||
|
||||
extraOptions = [
|
||||
"--network=host" # simplest for single-host: services talk on 127.0.0.1
|
||||
"--hostname=openldap"
|
||||
];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Systemd override to inject sops secrets as env vars
|
||||
# -----------------------------------------------------------------------
|
||||
# podman containers are managed by systemd units named
|
||||
# podman-<container-name>.service
|
||||
systemd.services."podman-openldap" = {
|
||||
serviceConfig = {
|
||||
# Write an env file with secret values before the container starts,
|
||||
# then pass it to podman run via EnvironmentFile.
|
||||
ExecStartPre = [
|
||||
(pkgs.writeShellScript "openldap-secrets-env" ''
|
||||
set -euo pipefail
|
||||
install -m 600 /dev/null /run/openldap-secrets.env
|
||||
echo "LDAP_ADMIN_PASSWORD=$(cat ${config.sops.secrets."openldap/admin_password".path})" >> /run/openldap-secrets.env
|
||||
echo "LDAP_CONFIG_PASSWORD=$(cat ${config.sops.secrets."openldap/config_password".path})" >> /run/openldap-secrets.env
|
||||
echo "LDAP_READONLY_USER_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})" >> /run/openldap-secrets.env
|
||||
'')
|
||||
];
|
||||
EnvironmentFile = "/run/openldap-secrets.env";
|
||||
};
|
||||
# Clean up the env file on stop
|
||||
postStop = "rm -f /run/openldap-secrets.env";
|
||||
# Wait for the external HD to be mounted before starting
|
||||
after = lib.mkAfter [ "mnt-data.mount" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||
};
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Firewall — openldap port is NOT opened externally (localhost only)
|
||||
# -----------------------------------------------------------------------
|
||||
# No firewall rule needed; bound to 127.0.0.1.
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# phpLDAPadmin — web UI for OpenLDAP management.
|
||||
#
|
||||
# Stateless container (no persistent volumes needed).
|
||||
# Protected by Authelia two_factor, admins-only policy (defined in authelia.nix).
|
||||
# Bound to localhost:8081; Caddy reverse-proxies it.
|
||||
|
||||
let
|
||||
cfg = config.homey.phpldapadmin;
|
||||
in
|
||||
{
|
||||
options.homey.phpldapadmin = {
|
||||
enable = lib.mkEnableOption "phpLDAPadmin web interface";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/osixia/phpldapadmin:latest";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8081;
|
||||
description = "Host port phpLDAPadmin listens on (bound to 127.0.0.1).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
virtualisation.oci-containers.containers.phpldapadmin = {
|
||||
image = cfg.image;
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
||||
|
||||
environment = {
|
||||
PHPLDAPADMIN_HTTPS = "false";
|
||||
PHPLDAPADMIN_LDAP_HOSTS = "127.0.0.1"; # openldap on host network
|
||||
};
|
||||
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
systemd.services."podman-phpldapadmin" = {
|
||||
after = lib.mkAfter [ "podman-openldap.service" ];
|
||||
wants = lib.mkAfter [ "podman-openldap.service" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{ config, lib, pkgs, homeyConfig, ... }:
|
||||
|
||||
# Transmission — BitTorrent client. (Deferred — enable when ready.)
|
||||
#
|
||||
# NOTE: Transmission's web UI also runs on port 9091. To avoid clashing
|
||||
# with Authelia (also 9091), this module binds Transmission to 9092.
|
||||
#
|
||||
# Volume layout:
|
||||
# <dataDir>/transmission/config/ → /config
|
||||
# <dataDir>/media/movies/ → /downloads/movies
|
||||
# <dataDir>/media/tvshows/ → /downloads/tvshows
|
||||
# <dataDir>/media/general/ → /downloads/general
|
||||
# <dataDir>/media/complete/ → /downloads/complete
|
||||
|
||||
let
|
||||
cfg = config.homey.transmission;
|
||||
dataDir = config.homey.storage.mountPoint;
|
||||
in
|
||||
{
|
||||
options.homey.transmission = {
|
||||
enable = lib.mkEnableOption "Transmission torrent client";
|
||||
|
||||
image = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "docker.io/linuxserver/transmission:latest";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 9092;
|
||||
description = "Host port for Transmission web UI (9092 to avoid clash with authelia@9091).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
virtualisation.oci-containers.containers.transmission = {
|
||||
image = cfg.image;
|
||||
ports = [ "127.0.0.1:${toString cfg.port}:9091" ];
|
||||
|
||||
environment = {
|
||||
PUID = "1000";
|
||||
PGID = "1000";
|
||||
};
|
||||
|
||||
volumes = [
|
||||
"${dataDir}/transmission/config:/config"
|
||||
"${dataDir}/media/movies:/downloads/movies"
|
||||
"${dataDir}/media/tvshows:/downloads/tvshows"
|
||||
"${dataDir}/media/general:/downloads/general"
|
||||
"${dataDir}/media/complete:/downloads/complete"
|
||||
];
|
||||
|
||||
extraOptions = [ "--network=host" ];
|
||||
};
|
||||
|
||||
systemd.services."podman-transmission" = {
|
||||
after = lib.mkAfter [ "mnt-data.mount" ];
|
||||
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user