Compare commits

...

10 Commits

Author SHA1 Message Date
aloslider 13e407847f Add selfhosted services 2026-06-30 04:26:40 +03:00
aloslider f212c874b5 WIP 2026-05-10 11:46:58 +03:00
aloslider 64b87222bd Add .gitignore 2026-04-29 06:32:56 +03:00
aloslider 91a36ec0c6 Restructure home module 2026-04-17 16:00:29 +03:00
aloslider 4a21cb9a9d Fix network module 2026-04-17 15:58:47 +03:00
aloslider 6ea769417b Fix disks id and netowork config 2026-04-12 17:50:35 +03:00
aloslider 6a3ff12527 Restructure
- Restructure
- Add sops+age
2026-04-10 23:49:32 +03:00
aloslider 03548606e4 Add git module 2026-03-14 16:16:20 +03:00
aloslider 7c82eb5dc5 Fix docker daemon not setup 2026-03-07 17:20:54 +03:00
aloslider e4abb866e9 Add system modules 2026-03-07 01:40:27 +03:00
76 changed files with 2907 additions and 120 deletions
+1
View File
@@ -0,0 +1 @@
result
-51
View File
@@ -1,51 +0,0 @@
{ config, lib, pkgs, ... }:
{
imports = [
./home-manager.nix
./hardware-configuration.nix
];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
boot.loader = {
limine = {
enable = true;
};
efi.canTouchEfiVariables = true;
};
networking.hostName = "benq-serv";
networking.networkmanager.enable = true;
time.timeZone = "Europe/Moscow";
i18n = {
defaultLocale = "en_US.UTF-8";
extraLocaleSettings = {
LC_TIME = "en_GB.UTF-8";
};
};
services.xserver.xkb = {
layout = "us,ru";
options = "eurosign:e,caps:escape";
};
services.pipewire = {
enable = true;
pulse.enable = true;
};
environment.systemPackages = with pkgs; [
git
vim
];
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [ 22 80 443 ];
networking.firewall.allowedUDPPorts = [];
system.stateVersion = "25.05";
}
Generated
+112 -4
View File
@@ -1,12 +1,82 @@
{
"nodes": {
"disko": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1777713215,
"narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=",
"owner": "nix-community",
"repo": "disko",
"rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"dotfiles": {
"flake": false,
"locked": {
"lastModified": 1778445660,
"narHash": "sha256-eiGc//3MdHJJSlIXcdtjT+Fdgg+xrK8eqYpRBALzFyw=",
"owner": "aloslider",
"repo": "dotfiles",
"rev": "5cf0727f8aede69b350e46284cf7147d6185ef56",
"type": "github"
},
"original": {
"owner": "aloslider",
"repo": "dotfiles",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778365864,
"narHash": "sha256-ImoT/wqmgMImf2dAC+E0MverAdA4QXsedOeES9B7Ezw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f419037039a152448c5f4ae9494154753d1b399",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772542754,
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
"lastModified": 1773628058,
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1777954456,
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
"rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
"type": "github"
},
"original": {
@@ -16,9 +86,47 @@
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1775888245,
"narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "13043924aaa7375ce482ebe2494338e058282925",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"disko": "disko",
"dotfiles": "dotfiles",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs_2",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1777944972,
"narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=",
"owner": "mic92",
"repo": "sops-nix",
"rev": "c591bf665727040c6cc5cb409079acb22dcce33c",
"type": "github"
},
"original": {
"owner": "mic92",
"repo": "sops-nix",
"type": "github"
}
}
},
+40 -10
View File
@@ -1,16 +1,46 @@
{
description = "Homelab server flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
disko.url = "github:nix-community/disko";
dotfiles = {
url = "github:aloslider/dotfiles";
flake = false;
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix.url = "github:mic92/sops-nix";
};
outputs = { self, nixpkgs }@inputs: {
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
];
};
};
outputs =
{ self, nixpkgs, ... }@inputs:
let
system = "x86_64-linux";
pkgs = nixpkgs;
customLib = pkgs.lib.extend (
self: super: {
custom = (
import ./lib {
inherit (pkgs) lib;
}
);
}
);
in
{
nixosConfigurations = {
homelab = pkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit inputs;
inherit (inputs) disko;
lib = customLib;
};
modules = [
./hosts/homelab/configuration.nix
];
};
};
};
}
-30
View File
@@ -1,30 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "sr_mod" "virtio_blk" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/5454b429-e268-4399-9eaf-48e16951d1b4";
fsType = "btrfs";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/F6C1-695C";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
-25
View File
@@ -1,25 +0,0 @@
{ config, pkgs, lib, ... }:
let
home-manager = builtins.fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/release-25.05.tar.gz";
sha256 = "07pk5m6mxi666dclaxdwf7xrinifv01vvgxn49bjr8rsbh31syaq";
};
in
{
imports = [
(import "${home-manager}/nixos")
];
users.users.benq.isNormalUser = true;
home-manager = {
useGlobalPkgs = true;
users.benq = { pkgs, ... }: {
home.packages = with pkgs; [
lazygit
tmux
];
home.stateVersion = "25.05";
};
};
}
+36
View File
@@ -0,0 +1,36 @@
{
config,
inputs,
lib,
pkgs,
...
}:
{
imports = [
inputs.home-manager.nixosModules.home-manager
];
home-manager = {
extraSpecialArgs = {
inherit (inputs) disko dotfiles sops-nix;
};
useGlobalPkgs = true;
useUserPackages = true;
users.benq =
{ pkgs, ... }:
{
imports = lib.flatten [
(map lib.custom.relativeToRoot (
[
"home/common/core"
]
++ (map (f: "home/common/optional/${f}") [
])
))
];
home.stateVersion = "25.05";
};
};
}
+8
View File
@@ -0,0 +1,8 @@
{ config, ... }:
{
imports = [
./git.nix
./packages.nix
# ./sops.nix
];
}
+29
View File
@@ -0,0 +1,29 @@
{ config, lib, ... }:
with lib;
let
cfg = config.programs.git.options;
in
{
options.programs.git.options = {
username = mkOption {
type = types.str;
default = "aloslider";
};
email = mkOption {
type = types.str;
default = "53711835+aloslider@users.noreply.github.com";
};
};
config = {
programs.git = {
settings = {
user = {
name = cfg.username;
email = cfg.email;
};
init.defaultBranch = "master";
};
};
};
}
+10
View File
@@ -0,0 +1,10 @@
{ config, pkgs, ... }:
{
home.packages = with pkgs; [
chezmoi
fzf
lazygit
lazydocker
sops
];
}
+18
View File
@@ -0,0 +1,18 @@
{ config, dotfiles, sops-nix, ... }:
{
imports = [
sops-nix.homeManagerModules.sops
];
sops = {
defaultSopsFile = "${dotfiles}/dot_config/sops/secrets.yaml";
age.keyFile = "${config.xdg.configHome}/sops/age/keys.txt";
};
home.file.".sops.yaml".source = "${dotfiles}/dot_sops.yaml";
xdg.configFile."sops" = {
source = "${dotfiles}/dot_config/sops";
recursive = true;
};
}
+11
View File
@@ -0,0 +1,11 @@
{ config, ... }:
{
imports = [
./disko.nix
./locale.nix
./nix.nix
./packages.nix
./sops.nix
./zsh.nix
];
}
+61
View File
@@ -0,0 +1,61 @@
{
config,
disko,
lib,
...
}:
with lib;
let
cfg = config.disko.cfg;
in
{
imports = [
disko.nixosModules.disko
];
options.disko.cfg = {
mainDevice = mkOption {
type = types.str;
description = "Main disk name";
};
};
config = {
disko.devices.disk.ssd = {
device = cfg.mainDevice;
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
name = "ESP";
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
name = "root";
size = "100%";
content = {
type = "btrfs";
extraArgs = [
"-f"
"-L"
"root"
];
mountpoint = "/";
mountOptions = [
"compress=zstd"
"noatime"
];
};
};
};
};
};
};
}
+16
View File
@@ -0,0 +1,16 @@
{ config, ... }:
{
time.timeZone = "Europe/Moscow";
i18n = {
defaultLocale = "en_US.UTF-8";
extraLocaleSettings = {
LC_TIME = "en_GB.UTF-8";
};
};
services.xserver.xkb = {
layout = "us,ru";
options = "eurosign:e,caps:escape";
};
}
+17
View File
@@ -0,0 +1,17 @@
{ config, ... }:
{
nix = {
gc = {
automatic = true;
dates = "daily";
options = "--delete-older-than 7d";
};
settings = {
experimental-features = [
"nix-command"
"flakes"
];
auto-optimise-store = true;
};
};
}
+17
View File
@@ -0,0 +1,17 @@
{ congif, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
age
bat
bind
btop
git
jq
openssl
ripgrep
tmux
tree
vim
wget
];
}
+12
View File
@@ -0,0 +1,12 @@
{ config, inputs, ... }:
{
imports = [
inputs.sops-nix.nixosModules.sops
];
sops = {
defaultSopsFile = "/etc/sops/secrets.yaml";
age.keyFile = "/etc/age/key.txt";
validateSopsFiles = false;
};
}
+5
View File
@@ -0,0 +1,5 @@
{ config, ... }:
{
# programs.zsh.enable = true;
programs.nix-ld.enable = true;
}
+7
View File
@@ -0,0 +1,7 @@
{ config, ... }:
{
services.pipewire = {
enable = true;
pulse.enable = true;
};
}
+8
View File
@@ -0,0 +1,8 @@
{ config, ... }:
{
boot.kernelModules = [ "tcp_bbr" ];
boot.kernel.sysctl = {
"net.ipv4.tcp_congestion_control" = "bbr";
"net.core.deafult_qdisc" = "fq";
};
}
+13
View File
@@ -0,0 +1,13 @@
{ config, ... }:
{
services.endlessh = {
enable = true;
port = 22;
openFirewall = true;
};
systemd.services.endlessh = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
};
}
+8
View File
@@ -0,0 +1,8 @@
{ config, ... }:
{
services.fail2ban = {
enable = true;
maxretry = 3;
bantime = "48h";
};
}
+4
View File
@@ -0,0 +1,4 @@
{ config, pkgs, ... }:
{
fonts.fontDir.enable = true;
}
+14
View File
@@ -0,0 +1,14 @@
{ config, ... }:
{
services.openssh = {
enable = true;
ports = [ 6969 ];
settings = {
PasswordAuthentication = false;
PermitEmptyPasswords = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
PubkeyAuthentication = true;
};
};
}
@@ -0,0 +1,288 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted;
in
{
imports = [
./gitea
./homarr
./immich
./jellyfin
./netbird
./pocket-id
./seafile
./traefik
./vaultwarden
./zapret2
./zerobyte
];
options.selfhosted.common = {
backend = mkOption {
type = types.str;
};
rootTarget = {
name = mkOption {
type = types.str;
};
};
network = {
name = mkOption {
type = types.str;
};
address = mkOption {
type = types.str;
};
mask = mkOption {
type = types.str;
};
gateway = mkOption {
type = types.str;
};
};
dataDir = mkOption {
type = types.str;
};
user = {
name = mkOption {
type = types.str;
};
uid = mkOption {
type = types.str;
};
};
group = {
name = mkOption {
type = types.str;
};
gid = mkOption {
type = types.str;
};
};
};
config = {
selfhosted = {
common = {
backend = "docker";
rootTarget.name = "sh-root";
network = {
name = "selfhosted";
address = "172.30.0.0";
mask = "24";
gateway = "172.30.0.1";
};
dataDir = "/mnt/data";
user = {
name = config.users.users.benq.name;
uid = "1000";
};
group = {
name = "users";
gid = "1000";
};
};
jellyfin = {
name = "jellyfin";
subdomain = "media";
hostHttpPort = 5040;
hostUdpPort = 5041;
};
gitea = {
targetName = "gitea";
gitea = {
name = "gitea";
subdomain = "gitea";
hostWebPort = 5060;
hostSshPort = 5061;
};
postgresql = {
name = "gitea-db";
};
};
immich = {
targetName = "immich";
version = "v2";
immich = {
name = "immich";
subdomain = "photos";
hostPort = 5090;
};
ml = {
name = "immich-ml";
};
redis = {
name = "immich-cache";
};
postgresql = {
name = "immich-db";
db = "immich";
user = "immich";
};
};
netbird = {
targetName = "netbird";
subdomain = "nb";
dashboard = {
name = "nb-dashboard";
};
proxy = {
name = "nb-proxy";
};
server = {
name = "nb-server";
};
};
homarr = {
name = "homarr";
subdomain = "dash";
hostPort = 5030;
};
pocket-id = {
name = "pocket-id";
subdomain = "id";
hostPort = 5020;
};
seafile = {
targetName = "seafile";
server = {
name = "sf-server";
subdomain = "cloud";
hostHttpPort = 5050;
};
mariadb = {
name = "sf-db";
};
redis = {
name = "sf-cache";
};
seadoc = {
name = "sf-seadoc";
hostHttpPort = 5051;
};
notification = {
name = "sf-notification";
hostHttpPort = 5052;
};
};
traefik = {
name = "traefik";
ip = "172.30.0.10";
};
vaultwarden = {
name = "vaultwarden";
subdomain = "vault";
hostPort = 5010;
};
zapret2 = {
name = "zapret2";
socksPort = 6000;
ssPort = 6001;
};
zerobyte = {
name = "zerobyte";
subdomain = "backup";
hostPort = 5070;
};
};
virtualisation = lib.mkMerge [
{ oci-containers.backend = config.selfhosted.common.backend; }
(
if config.selfhosted.common.backend == "docker" then
{
docker = {
enable = true;
};
}
else if config.selfhosted.common.backend == "podman" then
{
podman = {
enable = true;
dockerCompat = true;
};
}
else
abort "virtualisation.backend is not set"
)
];
services.netbird.clients = {
homelab = {
port = 51820;
openFirewall = true;
openInternalFirewall = true;
login = {
enable = true;
setupKeyFile = config.sops.secrets."netbird/setup_keys/homelab".path;
};
};
};
sops.secrets = {
"netbird/setup_keys/homelab" = { };
"netbird/setup_keys/site" = { };
};
sops.templates."netbird-homelab.env".content = ''
NB_MANAGEMENT_URL=https://${cfg.netbird.subdomain}.${config.sops.placeholder."domains/serv"}
'';
# Override to pass env file
systemd.services = {
"netbird-homelab" = {
serviceConfig = {
EnvironmentFile = [ config.sops.templates."netbird-homelab.env".path ];
};
after = [ "sops-nix.service" ];
wants = [ "sops-nix.service" ];
};
"netbird-homelab-login" = {
after = [ "sops-nix.service" ];
wants = [ "sops-nix.service" ];
};
};
systemd.targets."${common.backend}-${common.rootTarget.name}" = {
unitConfig = {
Description = "Root target for homelab stack";
};
wantedBy = [ "multi-user.target" ];
};
systemd.services."${common.backend}-network-${common.network.name}" = {
path = [ pkgs.${common.backend} ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "${common.backend} network rm -f ${common.network.name}";
};
script = ''
${common.backend} network inspect ${common.network.name} \
|| ${common.backend} network create ${common.network.name} --driver=bridge --subnet=${common.network.address}/${common.network.mask} --gateway=${common.network.gateway}
'';
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
networking.firewall.interfaces = lib.mkMerge [
(lib.mkIf (config.selfhosted.common.backend == "podman") (
let
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
in
{
"${matchAll}" = {
allowedUDPPorts = [ 53 ];
};
}
))
];
};
}
@@ -0,0 +1,38 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.gitea;
in
{
imports = [
./gitea
./postgresql
];
options.selfhosted.gitea = {
targetName = mkOption {
type = types.str;
};
};
config = {
sops.secrets = {
"gitea/db_password" = { };
"gitea/client_id" = { };
"gitea/client_secret" = { };
};
systemd.targets."${common.backend}-${cfg.targetName}" = {
unitConfig = {
Description = "Root target for ${cfg.targetName}";
};
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
};
}
@@ -0,0 +1,53 @@
{ config, ... }:
let
common = config.selfhosted.common;
cfg = config.selfhosted.gitea;
giteaDomain = "${cfg.gitea.subdomain}.${config.sops.placeholder."domains/serv"}";
in
{
sops.templates."gitea.env".content = ''
USER_UID=${common.user.uid}
USER_GID=${common.group.gid}
GITEA__database__DB_TYPE=postgres
GITEA__database__HOST=${cfg.postgresql.name}:5432
GITEA__database__NAME=gitea
GITEA__database__USER=gitea
GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"}
TZ=Europe/Moscow
GITEA__time__DEFAULT_UI_LOCATION=Europe/Moscow
GITEA__server__DOMAIN=${giteaDomain}
GITEA__server__ROOT_URL=https://${giteaDomain}/
GITEA__server__HTTP_PORT=3000
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=false
GITEA__service__DISABLE_REGISTRATION=true
GITEA__service__SHOW_REGISTRATION_BUTTON=false
GITEA__service__ENABLE_BASIC_AUTHENTICATION=false
GITEA__service__ENABLE_PASSKEY_AUTHENTICATION=false
GITEA__server__START_SSH_SERVER=true
GITEA__server__SSH_DOMAIN=git.${config.sops.placeholder."domains/serv"}
GITEA__server__SSH_LISTEN_HOST=0.0.0.0
GITEA__server__SSH_LISTEN_PORT=2222
GITEA__server__SSH_PORT=2222
GITEA__security__INSTALL_LOCK=true
GITEA__repository__MAX_CREATION_LIMIT=50
GITEA__server__ENABLE_PASSWORD_SIGNIN_FORM=false
GITEA__server__ENABLE_BASIC_AUTHENTICATION=false
GITEA__openid__ENABLE_OPENID_SIGNIN=false
GITEA__openid__ENABLE_OPENID_SIGNUP=false
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION=true
GITEA__oauth2_client__ACCOUNT_LINKING=auto
GITEA__oauth2_client__USERNAME=preferred_username
GITEA__session__COOKIE_SECURE=true
GITEA__log__LEVEL=Info
'';
}
@@ -0,0 +1,111 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.gitea;
hostDataPath = "${common.dataDir}/gitea";
in
{
imports = [ ./config.nix ];
options.selfhosted.gitea.gitea = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostWebPort = mkOption {
type = types.port;
};
hostSshPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.gitea.name}" = {
image = "gitea/gitea:latest";
ports = [
"0.0.0.0:${toString cfg.gitea.hostWebPort}:3000"
"0.0.0.0:${toString cfg.gitea.hostSshPort}:2222"
];
environmentFiles = [ config.sops.templates."gitea.env".path ];
volumes = [
"${hostDataPath}/data:/data"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.gitea.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-${cfg.postgresql.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-${cfg.postgresql.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/data
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}/data
chmod -R 0775 ${hostDataPath}/data
'';
postStart =
let
engine =
if common.backend == "podman" then
pkgs.podman
else if common.backend == "docker" then
pkgs.docker
else
abort "Unknown backend ${common.backend}";
in
''
set -euo pipefail
DOMAIN=$(cat ${config.sops.secrets."domains/serv".path})
CLIENT_ID=$(cat ${config.sops.secrets."gitea/client_id".path})
CLIENT_SECRET=$(cat ${config.sops.secrets."gitea/client_secret".path})
for i in $(seq 1 30); do
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.gitea.hostWebPort}/api/v1/version >/dev/null 2>&1; then
break
fi
echo "Waiting for Gitea to be ready... ($i/20)"
sleep 5
done
if ${engine}/bin/${common.backend} exec --user ${common.user.uid} ${cfg.gitea.name} \
gitea admin auth list 2>/dev/null | grep -q 'PocketID'; then
echo "PocketID auth source already registered"
exit 0
fi
${engine}/bin/${common.backend} exec --user ${common.user.uid} ${cfg.gitea.name} \
gitea admin auth add-oauth \
--name "PocketID" \
--provider "openidConnect" \
--key "$CLIENT_ID" \
--secret "$CLIENT_SECRET" \
--auto-discover-url "https://${config.selfhosted.pocket-id.subdomain}.$DOMAIN/.well-known/openid-configuration" \
--scopes "openid email profile" \
--skip-local-2fa
echo "PocketID auth source registered successfully"
'';
};
};
}
@@ -0,0 +1,11 @@
{ config, ... }:
let
cfg = config.selfhosted.gitea;
in
{
sops.templates."gitea-db.env".content = ''
POSTGRES_DB=gitea
POSTGRES_USER=gitea
POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"}
'';
}
@@ -0,0 +1,48 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.gitea;
hostDataPath = "${common.dataDir}/gitea";
in
{
imports = [ ./config.nix ];
options.selfhosted.gitea.postgresql = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.postgresql.name}" = {
image = "postgres:16";
environmentFiles = [ config.sops.templates."gitea-db.env".path ];
volumes = [
"${hostDataPath}/dbData:/var/lib/postgresql/data"
];
extraOptions = [
"--network=${common.network.name}"
"--network-alias=${cfg.postgresql.name}"
"--health-cmd=pg_isready -U gitea"
"--health-interval=10s"
"--health-timeout=5s"
"--health-retries=5"
];
};
systemd.services."${common.backend}-${cfg.postgresql.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/dbData
chown 70:70 ${hostDataPath}/dbData
'';
};
};
}
@@ -0,0 +1,30 @@
{ config, ... }:
let
cfg = config.selfhosted.homarr;
poCfg = config.selfhosted.pocket-id;
oidpDomain = "https://${poCfg.subdomain}.${config.sops.placeholder."domains/serv"}";
in
{
sops.secrets = {
"homarr/enc_key" = { };
"homarr/client_id" = { };
"homarr/client_secret" = { };
"homarr/nextauth_secret" = { };
};
sops.templates."homarr.env" = {
content = ''
SECRET_ENCRYPTION_KEY=${config.sops.placeholder."homarr/enc_key"}
NEXTAUTH_SECRET=${config.sops.placeholder."homarr/nextauth_secret"}
AUTH_PROVIDERS=oidc
AUTH_OIDC_CLIENT_ID=${config.sops.placeholder."homarr/client_id"}
AUTH_OIDC_CLIENT_SECRET=${config.sops.placeholder."homarr/client_secret"}
AUTH_OIDC_ISSUER=${oidpDomain}
AUTH_OIDC_CLIENT_NAME="Pocket ID"
AUTH_OIDC_SCOPE_OVERWRITE=openid email profile groups
AUTH_OIDC_GROUPS_ATTRIBUTE=groups
AUTH_LOGOUT_REDIRECT_URL=${oidpDomain}
AUTH_OIDC_AUTO_LOGIN=true
'';
};
}
@@ -0,0 +1,59 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.homarr;
hostDataPath = "${common.dataDir}/homarr";
in
{
imports = [ ./config.nix ];
options.selfhosted.homarr = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.name}" = {
image = "ghcr.io/homarr-labs/homarr:latest";
ports = [
"0.0.0.0:${toString cfg.hostPort}:7575"
];
environmentFiles = [ config.sops.templates."homarr.env".path ];
volumes = [
"${hostDataPath}/data:/appdata"
"/var/run/docker.sock:/var/run/docker.sock"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/data
'';
};
};
}
@@ -0,0 +1,38 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.immich;
in
{
imports = [
./immich
./ml
./postgresql
./redis
];
options.selfhosted.immich = {
targetName = mkOption {
type = types.str;
};
version = mkOption {
type = types.str;
};
};
config = {
sops.secrets = {
"immich/db_password" = { };
"immich/client_id" = { };
"immich/client_secret" = { };
};
systemd.targets."${common.backend}-${cfg.targetName}" = {
unitConfig = {
Description = "Root target for ${cfg.targetName}";
};
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
};
}
@@ -0,0 +1,6 @@
{ config, ... }:
{
sops.templates."immich.env".content = ''
DB_PASSWORD=${config.sops.placeholder."immich/db_password"}
'';
}
@@ -0,0 +1,71 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.immich;
cfgPath = "/config/immich.json";
hostDataPath = "${common.dataDir}/immich";
in
{
imports = [ ./config.nix ];
options.selfhosted.immich.immich = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.immich.name}" = {
image = "ghcr.io/immich-app/immich-server:${cfg.version}";
ports = [
"0.0.0.0:${toString cfg.immich.hostPort}:2283"
];
environmentFiles = [ config.sops.templates."immich.env".path ];
environment = {
IMMICH_VERSION = cfg.version;
DB_HOSTNAME = cfg.postgresql.name;
DB_DATABASE_NAME = cfg.postgresql.db;
DB_USERNAME = cfg.postgresql.user;
REDIS_HOSTNAME = cfg.redis.name;
};
volumes = [
"${hostDataPath}/data:/data"
"/etc/localtime:/etc/localtime:ro"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.immich.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${cfg.postgresql.name}.service"
"${common.backend}-${cfg.redis.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${cfg.postgresql.name}.service"
"${common.backend}-${cfg.redis.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/data
'';
};
};
}
@@ -0,0 +1,6 @@
{ config, ... }:
{
sops.templates."immich-db.env".content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."immich/db_password"}
'';
}
@@ -0,0 +1,42 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.immich;
hostDataPath = "${common.dataDir}/immich";
in
{
options.selfhosted.immich.ml = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.ml.name}" = {
image = "ghcr.io/immich-app/immich-machine-learning:${cfg.version}";
environment = {
IMMICH_VERSION = cfg.version;
};
volumes = [
"${hostDataPath}/cache:/cache"
];
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.ml.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/cache
'';
};
};
}
@@ -0,0 +1,6 @@
{ config, ... }:
{
sops.templates."immich-db.env".content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."immich/db_password"}
'';
}
@@ -0,0 +1,55 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.immich;
hostDataPath = "${common.dataDir}/immich";
in
{
imports = [ ./config.nix ];
options.selfhosted.immich.postgresql = {
name = mkOption {
type = types.str;
};
db = mkOption {
type = types.str;
};
user = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.postgresql.name}" = {
image = "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23";
environmentFiles = [ config.sops.templates."immich-db.env".path ];
environment = {
POSTGRES_DB = cfg.postgresql.db;
POSTGRES_USER = cfg.postgresql.user;
POSTGRES_INITDB_ARGS = "--data-checksums";
};
volumes = [
"${hostDataPath}/db:/var/lib/postgresql/data"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
"--shm-size=128m"
];
};
systemd.services."${common.backend}-${cfg.postgresql.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/db
'';
};
};
}
@@ -0,0 +1,34 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.immich;
in
{
options.selfhosted.immich.redis = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.redis.name}" = {
image = "docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9";
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
"--health-cmd=redis-cli ping || exit 1"
];
};
systemd.services."${common.backend}-${cfg.redis.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
};
};
}
@@ -0,0 +1,12 @@
{ config, ... }:
let
cfg = config.selfhosted.jellyfin;
in
{
sops.secrets = {
};
sops.templates."jellyfin.env".content = ''
'';
}
@@ -0,0 +1,66 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.jellyfin;
hostDataPath = "${common.dataDir}/jellyfin";
in
{
imports = [ ./config.nix ];
options.selfhosted.jellyfin = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostHttpPort = mkOption {
type = types.port;
};
hostUdpPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.name} = {
image = "docker.io/jellyfin/jellyfin:10.11.11";
ports = [
"0.0.0.0:${toString cfg.hostHttpPort}:8096"
"0.0.0.0:${toString cfg.hostUdpPort}:7359"
];
user = "${common.user.uid}:${common.group.gid}";
environmentFiles = [ config.sops.templates."jellyfin.env".path ];
volumes = [
"${hostDataPath}/config:/config"
"${hostDataPath}/cache:/cache"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/{config,cache}
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0775 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,23 @@
{ config, ... }:
let
fullDomain = "${config.selfhosted.netbird.subdomain}.${config.sops.placeholder."domains/serv"}";
in
{
sops.templates."nb-dashboard.env".content = ''
NETBIRD_MGMT_API_ENDPOINT=https://${fullDomain}
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${fullDomain}
# Initial NB setup: custom OIDCP is added manually in panel later
AUTH_AUTHORITY=https://${fullDomain}/oauth2
AUTH_AUDIENCE=netbird-dashboard
AUTH_CLIENT_ID=netbird-dashboard
AUTH_CLIENT_SECRET=
USE_AUTH0=false
AUTH_SUPPORTED_SCOPES=openid profile email groups
AUTH_REDIRECT_URI=/nb-auth
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
LETSENCRYPT_DOMAIN=none
LETSENCRYPT_EMAIL=${config.sops.placeholder."letsEncrypt/email"}
'';
}
@@ -0,0 +1,43 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
in
{
imports = [ ./config.nix ];
options.selfhosted.netbird.dashboard = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.dashboard.name} = {
image = "netbirdio/dashboard:latest";
environmentFiles = [ config.sops.templates."nb-dashboard.env".path ];
log-driver = "journald";
extraOptions = [
"--network-alias=${cfg.dashboard.name}"
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.dashboard.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-${cfg.server.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-${cfg.server.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
};
};
}
@@ -0,0 +1,43 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
in
{
imports = [
./dashboard
./proxy
./server
];
options.selfhosted.netbird = {
targetName = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
};
config = {
sops.secrets = {
"netbird/proxy/token" = { };
};
systemd.targets."${common.backend}-${cfg.targetName}" = {
unitConfig = {
Description = "Root target for ${cfg.targetName}";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
};
}
@@ -0,0 +1,20 @@
{ config, ... }:
let
cfg = config.selfhosted.netbird;
in
{
sops.templates."nb-proxy.env".content = ''
NB_PROXY_DEBUG_LOGS=false
NB_PROXY_PROXY_PROTOCOL=true
NB_PROXY_MANAGEMENT_ADDRESS=http://${cfg.server.name}:80
NB_PROXY_ALLOW_INSECURE=true
NB_PROXY_DOMAIN=${cfg.subdomain}.${config.sops.placeholder."domains/serv"}
NB_PROXY_ADDRESS=:8443
NB_PROXY_TOKEN=${config.sops.placeholder."netbird/proxy/token"}
NB_PROXY_FORWARDED_PROTO=https
NB_PROXY_CERTIFICATE_DIRECTORY=/certs
NB_PROXY_ACME_CERTIFICATES=true
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
NB_PROXY_TRUSTED_PROXIES=${config.selfhosted.traefik.ip}
'';
}
@@ -0,0 +1,60 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
hostDataPath = "${common.dataDir}/netbird/proxy";
in
{
imports = [ ./config.nix ];
options.selfhosted.netbird.proxy = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.proxy.name} = {
image = "netbirdio/reverse-proxy:latest";
environmentFiles = [ config.sops.templates."nb-proxy.env".path ];
volumes = [
"${hostDataPath}/certs:/certs:rw"
];
ports = [
"0.0.0.0:2222:2222/tcp" # gitea
"0.0.0.0:7359:7359/udp" # jelly
"0.0.0.0:51820:51820/udp"
];
dependsOn = [ cfg.server.name ];
log-driver = "journald";
extraOptions = [
"--network-alias=${cfg.proxy.name}"
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.proxy.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${cfg.server.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${cfg.server.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/certs
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0755 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,43 @@
{ config, ... }:
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
fullDomain = "${cfg.subdomain}.${config.sops.placeholder."domains/serv"}";
in
{
sops.secrets = {
"netbird/server/auth_secret" = { };
"netbird/server/enc_key" = { };
};
sops.templates."nb-server.yaml".content = ''
server:
listenAddress: ":80"
exposedAddress: "https://${fullDomain}:443"
stunPorts:
- 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
authSecret: "${config.sops.placeholder."netbird/server/auth_secret"}"
auth:
issuer: "https://${fullDomain}/oauth2"
dashboardRedirectURIs:
- "https://${fullDomain}/nb-auth"
- "https://${fullDomain}/nb-silent-auth"
cliRedirectURIs:
- "http://localhost:53000/"
localAuthDisabled: false
store:
engine: "sqlite"
encryptionKey: "${config.sops.placeholder."netbird/server/enc_key"}"
reverseProxy:
trustedHTTPProxies:
- "${config.selfhosted.traefik.ip}/32"
'';
}
@@ -0,0 +1,59 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
hostDataPath = "${common.dataDir}/netbird/server";
in
{
imports = [ ./config.nix ];
options.selfhosted.netbird.server = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.server.name} = {
image = "netbirdio/netbird-server:latest";
volumes = [
"${config.sops.templates."nb-server.yaml".path}:/etc/netbird/config.yaml:rw"
"${hostDataPath}/data:/var/lib/netbird:rw"
];
ports = [
"0.0.0.0:3478:3478/udp"
];
cmd = [
"--config"
"/etc/netbird/config.yaml"
];
log-driver = "journald";
extraOptions = [
"--network-alias=${cfg.server.name}"
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.server.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/data
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0755 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,20 @@
{ config, ... }:
let
cfg = config.selfhosted.pocket-id;
in
{
sops.secrets = {
"pocket_id/enc_key" = { };
};
sops.templates."pocket-id.env" = {
content = ''
APP_URL=https://${cfg.subdomain}.${config.sops.placeholder."domains/serv"}
ENCRYPTION_KEY=${config.sops.placeholder."pocket_id/enc_key"}
TRUST_PROXY=true
ANALYTICS_DISABLED=true
PUID=1000
PGID=1000
'';
};
}
@@ -0,0 +1,61 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.pocket-id;
hostDataPath = "${common.dataDir}/pocket-id";
in
{
imports = [ ./config.nix ];
options.selfhosted.pocket-id = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.name} = {
image = "ghcr.io/pocket-id/pocket-id:latest";
ports = [
"0.0.0.0:${toString cfg.hostPort}:1411"
];
user = "${common.user.uid}:${common.group.gid}";
environmentFiles = [ config.sops.templates."pocket-id.env".path ];
volumes = [
"${hostDataPath}/data:/app/data"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/letsencrypt
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0775 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,33 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
in
{
imports = [
./mariadb
./notification
./redis
./seadoc
./server
];
options.selfhosted.seafile = {
targetName = mkOption {
type = types.str;
};
};
config = {
systemd.targets."${common.backend}-${cfg.targetName}" = {
unitConfig = {
Description = "Root target for ${cfg.targetName}";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
};
}
@@ -0,0 +1,12 @@
{ config, ... }:
{
sops.secrets = {
"seafile/mysql_password" = { };
};
sops.templates."seafile-db.env".content = ''
MYSQL_ROOT_PASSWORD=${config.sops.placeholder."seafile/mysql_password"}
MYSQL_LOG_CONSOLE=true
MARIADB_AUTO_UPGRADE=1
'';
}
@@ -0,0 +1,48 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
hostDataPath = "${common.dataDir}/seafile";
in
{
imports = [ ./config.nix ];
options.selfhosted.seafile.mariadb = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.mariadb.name} = {
image = "docker.io/mariadb:10.11";
environmentFiles = [ config.sops.templates."seafile-db.env".path ];
volumes = [
"${hostDataPath}/mysql:/var/lib/mysql"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
"--health-cmd=/usr/local/bin/healthcheck.sh --connect --mariadbupgrade --innodb_initialized"
"--health-interval=20s"
"--health-start-period=30s"
"--health-timeout=5s"
"--health-retries=10"
];
};
systemd.services."${common.backend}-${cfg.mariadb.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/mysql
'';
};
};
}
@@ -0,0 +1,17 @@
{ config, ... }:
let
cfg = config.selfhosted.seafile;
in
{
sops.templates."seafile-notification.env".content = ''
SEAFILE_MYSQL_DB_HOST=${cfg.mariadb.name}
SEAFILE_MYSQL_DB_PORT=3306
SEAFILE_MYSQL_DB_USER=seafile
SEAFILE_MYSQL_DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"}
SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db
SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db
JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"}
SEAFILE_LOG_TO_STDOUT=false
NOTIFICATION_SERVER_LOG_LEVEL=info
'';
}
@@ -0,0 +1,73 @@
{ config, lib, pkgs, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
hostDataPath = "${common.dataDir}/seafile";
in
{
imports = [ ./config.nix ];
options.selfhosted.seafile.notification = {
name = mkOption {
type = types.str;
};
hostHttpPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.notification.name} = {
image = "docker.io/seafileltd/notification-server:13.0-latest";
ports = [
"0.0.0.0:${toString cfg.notification.hostHttpPort}:8083"
];
environmentFiles = [ config.sops.templates."seafile-notification.env".path ];
volumes = [
"${hostDataPath}/notification/logs:/shared/seafile/logs"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.notification.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${cfg.mariadb.name}.service"
"${common.backend}-${cfg.server.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${cfg.mariadb.name}.service"
"${common.backend}-${cfg.server.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
path = with pkgs; [ curl gawk ];
preStart = ''
echo "Waiting for Seafile server to be ready..."
for i in {1..60}; do
status=$(curl -sI --max-time 5 "http://localhost:${toString cfg.server.hostHttpPort}" | head -1 | awk '{print $2}')
if [[ "$status" =~ ^(200|301|302)$ ]]; then
echo "Seafile server is up (HTTP $status, attempt $i)"
break
fi
echo "Seafile not ready yet (status: $status) ... ($i/60)"
sleep 1
done
mkdir -p ${hostDataPath}/notification
'';
};
};
}
@@ -0,0 +1,10 @@
{ config, ... }:
{
sops.secrets = {
"seafile/redis_password" = { };
};
sops.templates."seafile-redis.env".content = ''
REDIS_PASSWORD=${config.sops.placeholder."seafile/redis_password"}
'';
}
@@ -0,0 +1,37 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
in
{
imports = [ ./config.nix ];
options.selfhosted.seafile.redis = {
name = mkOption {
type = types.str;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.redis.name} = {
image = "docker.io/redis:7";
environmentFiles = [ config.sops.templates."seafile-redis.env".path ];
cmd = [ "/bin/sh" "-c" "exec redis-server --requirepass \"$REDIS_PASSWORD\"" ];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.redis.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
};
};
}
@@ -0,0 +1,17 @@
{ config, ... }:
let
cfg = config.selfhosted.seafile;
in
{
sops.templates."seadoc.env".content = ''
DB_HOST=${cfg.mariadb.name}
DB_PORT=3306
DB_USER=seafile
DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"}
DB_NAME=seahub_db
TIME_ZONE=${config.time.timeZone}
JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"}
NON_ROOT=false
SEAHUB_SERVICE_URL=http://${cfg.server.name}
'';
}
@@ -0,0 +1,57 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
hostDataPath = "${common.dataDir}/seafile";
in
{
imports = [ ./config.nix ];
options.selfhosted.seafile.seadoc = {
name = mkOption {
type = types.str;
};
hostHttpPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.seadoc.name} = {
image = "docker.io/seafileltd/sdoc-server:2.0-latest";
ports = [
"0.0.0.0:${toString cfg.seadoc.hostHttpPort}:80"
];
volumes = [
"${hostDataPath}/seadoc/shared:/shared"
];
environmentFiles = [ config.sops.templates."seadoc.env".path ];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.seadoc.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${cfg.mariadb.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${cfg.mariadb.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/seadoc/shared
'';
};
};
}
@@ -0,0 +1,103 @@
{ config, ... }:
let
cfg = config.selfhosted.seafile;
poCfg = config.selfhosted.pocket-id;
domain = config.sops.placeholder."domains/serv";
in
{
sops.secrets = {
"seafile/admin_email" = { };
"seafile/admin_password" = { };
"seafile/client_id" = { };
"seafile/client_secret" = { };
"seafile/jwt_private_key" = { };
};
sops.templates."seafile.env".content = ''
SEAFILE_MYSQL_DB_HOST=${cfg.mariadb.name}
SEAFILE_MYSQL_DB_PORT=3306
SEAFILE_MYSQL_DB_USER=seafile
SEAFILE_MYSQL_DB_PASSWORD=${config.sops.placeholder."seafile/mysql_password"}
SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db
SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db
SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db
TIME_ZONE=${config.time.timeZone}
INIT_SEAFILE_MYSQL_ROOT_PASSWORD=${config.sops.placeholder."seafile/mysql_password"}
INIT_SEAFILE_ADMIN_EMAIL=${config.sops.placeholder."seafile/admin_email"}
INIT_SEAFILE_ADMIN_PASSWORD=${config.sops.placeholder."seafile/admin_password"}
SEAFILE_SERVER_HOSTNAME=${cfg.server.subdomain}.${domain}
SEAFILE_SERVER_PROTOCOL=https
SEAFILE_SERVER_LETSENCRYPT=false
SITE_ROOT=/
NON_ROOT=false
JWT_PRIVATE_KEY=${config.sops.placeholder."seafile/jwt_private_key"}
SEAFILE_LOG_TO_STDOUT=false
ENABLE_GO_FILESERVER=true
ENABLE_SEADOC=true
SEADOC_SERVER_URL=https://${cfg.server.subdomain}.${domain}/sdoc-server
CACHE_PROVIDER=redis
REDIS_HOST=${cfg.redis.name}
REDIS_PORT=6379
REDIS_PASSWORD=${config.sops.placeholder."seafile/redis_password"}
ENABLE_NOTIFICATION_SERVER=true
INNER_NOTIFICATION_SERVER_URL=http://${cfg.notification.name}:8083
NOTIFICATION_SERVER_URL=https://${cfg.server.subdomain}.${domain}/notification
ENABLE_SEAFILE_AI=false
MD_FILE_COUNT_LIMIT=100000
'';
sops.templates."seahub_settings.py".content = ''
ENABLE_VIDEO_THUMBNAIL = True
ENABLE_OAUTH = True
OAUTH_CREATE_UNKNOWN_USER = True
OAUTH_ACTIVATE_USER_AFTER_CREATION = True
OAUTH_ENABLE_INSECURE_TRANSPORT = False
OAUTH_CLIENT_ID = "${config.sops.placeholder."seafile/client_id"}"
OAUTH_CLIENT_SECRET = "${config.sops.placeholder."seafile/client_secret"}"
OAUTH_REDIRECT_URL = "https://${cfg.server.subdomain}.${domain}/oauth/callback"
OAUTH_PROVIDER = "pocket-id"
OAUTH_PROVIDER_DOMAIN = "pocket-id"
OAUTH_AUTHORIZATION_URL = "https://${poCfg.subdomain}.${domain}/authorize"
OAUTH_TOKEN_URL = "https://${poCfg.subdomain}.${domain}/api/oidc/token"
OAUTH_USER_INFO_URL = "https://${poCfg.subdomain}.${domain}/api/oidc/userinfo"
OAUTH_SCOPE = [
"openid",
"profile",
"email"
]
OAUTH_ATTRIBUTE_MAP = {
"sub": (True, "uid"),
"name": (False, "name"),
"email": (False, "contact_email"),
}
CLIENT_SSO_VIA_LOCAL_BROWSER = True
ENABLE_WEBDAV_SECRET = True
WEBDAV_SECRET_MIN_LENGTH = 8
SERVICE_URL = "https://${cfg.server.subdomain}.${domain}"
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
ALLOWED_HOSTS = [".${domain}"]
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "Lax"
CSRF_TRUSTED_ORIGINS = [
"https://${cfg.server.subdomain}.${domain}"
]
'';
sops.templates."seafdav.conf".content = ''
[WEBDAV]
enabled = true
port = 8080
debug = true
share_name = /seafdav
workers = 5
timeout = 1200
'';
sops.templates."seafile.conf".content = ''
[quota]
default = 100
'';
}
@@ -0,0 +1,70 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.seafile;
hostDataPath = "${common.dataDir}/seafile";
in
{
imports = [ ./config.nix ];
options.selfhosted.seafile.server = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostHttpPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.server.name} = {
image = "docker.io/seafileltd/seafile-mc:13.0-latest";
ports = [
"0.0.0.0:${toString cfg.server.hostHttpPort}:80"
];
environmentFiles = [ config.sops.templates."seafile.env".path ];
volumes = [
"${hostDataPath}/server/shared:/shared"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.server.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${config.selfhosted.seafile.mariadb.name}.service"
"${common.backend}-${config.selfhosted.seafile.redis.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
"${common.backend}-${cfg.mariadb.name}.service"
"${common.backend}-${cfg.redis.name}.service"
];
partOf = [ "${common.backend}-${cfg.targetName}.target" ];
wantedBy = [ "${common.backend}-${cfg.targetName}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/server/shared/seafile/conf
cp -f ${config.sops.templates."seahub_settings.py".path} \
${hostDataPath}/server/shared/seafile/conf/seahub_settings.py
cp -f ${config.sops.templates."seafdav.conf".path} \
${hostDataPath}/server/shared/seafile/conf/seafdav.conf
cp -f ${config.sops.templates."seafile.conf".path} \
${hostDataPath}/server/shared/seafile/conf/seafile.conf
chmod 644 ${hostDataPath}/server/shared/seafile/conf/{seafile.conf,seafdav.conf,seahub_settings.py}
'';
};
};
}
@@ -0,0 +1,118 @@
{ config, ... }:
let
common = config.selfhosted.common;
cfg = config.selfhosted.netbird;
in
{
sops.templates."traefik.env".content = ''
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=${config.sops.placeholder."letsEncrypt/email"}
'';
sops.templates."traefik-static-config.yaml".content = ''
entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
to: websecure
scheme: https
websecure:
address: ":443"
allowACMEByPass: true
transport:
respondingTimeouts:
readTimeout: 0
writeTimeout: 0
idleTimeout: 0
proxyProtocol:
trustedIPs:
- "${common.network.address}/${common.network.mask}"
serversTransport:
forwardingTimeouts:
responseHeaderTimeout: 0
idleConnTimeout: 0
providers:
docker:
exposedByDefault: false
network: "${common.network.name}"
file:
filename: /etc/traefik/dynamic.yaml
watch: true
certificatesResolvers:
letsencrypt:
acme:
storage: /letsencrypt/acme.json
tlsChallenge: true
'';
sops.templates."traefik-dynamic-config.yaml".content = ''
http:
routers:
netbird-dashboard:
entryPoints:
- websecure
rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`)"
service: dashboard
priority: 1
tls:
certResolver: letsencrypt
netbird-backend:
entryPoints:
- websecure
rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
service: netbird-server
priority: 100
tls:
certResolver: letsencrypt
netbird-grpc:
entryPoints:
- websecure
rule: "Host(`${cfg.subdomain}.${config.sops.placeholder."domains/serv"}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))"
service: netbird-server-h2c
priority: 100
tls:
certResolver: letsencrypt
services:
dashboard:
loadBalancer:
servers:
- url: "http://${cfg.dashboard.name}:80"
netbird-server:
loadBalancer:
servers:
- url: "http://${cfg.server.name}:80"
netbird-server-h2c:
loadBalancer:
servers:
- url: "h2c://${cfg.server.name}:80"
tcp:
serversTransports:
pp-v2:
proxyProtocol:
version: 2
routers:
proxy-passthrough:
entrypoints:
- websecure
rule: HostSNI(`*`)
service: proxy-tls
priority: 1
tls:
passthrough: true
services:
proxy-tls:
loadBalancer:
serversTransport: pp-v2@file
servers:
- address: "${cfg.proxy.name}:8443"
'';
}
@@ -0,0 +1,72 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.traefik;
hostDataPath = "${common.dataDir}/traefik";
in
{
imports = [ ./config.nix ];
options.selfhosted.traefik = {
name = mkOption {
type = types.str;
};
ip = mkOption {
type = types.str;
};
};
config = {
sops.secrets = {
"domains/serv" = { };
"letsEncrypt/email" = { };
};
virtualisation.oci-containers.containers.${cfg.name} = {
image = "traefik:latest";
environmentFiles = [ config.sops.templates."traefik.env".path ];
volumes = [
"/var/run/docker.sock:/var/run/docker.sock:ro"
"${config.sops.templates."traefik-static-config.yaml".path}:/etc/traefik/traefik.yaml:ro"
"${config.sops.templates."traefik-dynamic-config.yaml".path}:/etc/traefik/dynamic.yaml:ro"
"${hostDataPath}/letsencrypt:/letsencrypt:rw"
];
ports = [
"0.0.0.0:80:80"
"0.0.0.0:443:443"
];
cmd = [
"--log.level=INFO"
"--accesslog=true"
"--configfile=/etc/traefik/traefik.yaml"
];
log-driver = "journald";
extraOptions = [
"--ip=172.30.0.10"
"--network-alias=${cfg.name}"
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/letsencrypt
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0600 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,27 @@
{ config, ... }:
let
cfg = config.selfhosted;
domain = "${config.sops.placeholder."domains/serv"}";
in
{
sops.secrets = {
"vaultwarden/admin_token" = { };
"vaultwarden/client_id" = { };
"vaultwarden/client_secret" = { };
};
sops.templates."vaultwarden.env".content = ''
DOMAIN=https://${cfg.vaultwarden.subdomain}.${domain}
SIGNUPS_ALLOWED=false
ADMIN_TOKEN=${config.sops.placeholder."vaultwarden/admin_token"}
# SSO
SSO_ENABLED=true
SSO_ONLY=false
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION=true
SSO_AUTHORITY=https://${cfg.pocket-id.subdomain}.${domain}
SSO_CLIENT_ID=${config.sops.placeholder."vaultwarden/client_id"}
SSO_CLIENT_SECRET=${config.sops.placeholder."vaultwarden/client_secret"}
SSO_AUTH_ONLY_NOT_SESSION=true
'';
}
@@ -0,0 +1,61 @@
{ config, lib, ... }:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.vaultwarden;
hostDataPath = "${common.dataDir}/vaultwarden";
in
{
imports = [ ./config.nix ];
options.selfhosted.vaultwarden = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers."${cfg.name}" = {
image = "vaultwarden/server:latest";
ports = [
"0.0.0.0:${toString cfg.hostPort}:80"
];
user = "${common.user.uid}:${common.group.gid}";
environmentFiles = [ config.sops.templates."vaultwarden.env".path ];
volumes = [
"${hostDataPath}/data:/data"
];
log-driver = "journald";
extraOptions = [
"--network=${common.network.name}"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/data
chown -R ${common.user.uid}:${common.group.gid} ${hostDataPath}
chmod -R 0775 ${hostDataPath}
'';
};
};
}
@@ -0,0 +1,58 @@
{ pkgs }:
let
configFiles = {
config = ''
FWTYPE=iptables
SET_MAXELEM=522288
IPSET_OPT="hashsize 262144 maxelem $SET_MAXELEM"
IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4"
IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5"
AUTOHOSTLIST_INCOMING_MAXSEQ=4096
AUTOHOSTLIST_RETRANS_MAXSEQ=32768
AUTOHOSTLIST_RETRANS_RESET=1
AUTOHOSTLIST_RETRANS_THRESHOLD=3
AUTOHOSTLIST_FAIL_THRESHOLD=3
AUTOHOSTLIST_FAIL_TIME=60
AUTOHOSTLIST_UDP_IN=1
AUTOHOSTLIST_UDP_OUT=4
AUTOHOSTLIST_DEBUGLOG=0
MDIG_THREADS=30
GZIP_LISTS=1
DESYNC_MARK=0x40000000
DESYNC_MARK_POSTNAT=0x20000000
NFQWS2_ENABLE=1
NFQWS2_PORTS_TCP=80,443
NFQWS2_PORTS_UDP=443
NFQWS2_TCP_PKT_OUT=20
NFQWS2_TCP_PKT_IN=10
NFQWS2_UDP_PKT_OUT=5
NFQWS2_UDP_PKT_IN=3
NFQWS2_OPT="
--filter-tcp=80 --filter-l7=http --payload http_req --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1 --new
--filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --hostlist-domains=x.com --lua-desync=hostfakesplit:nofake1:midhost=midsld:ip_ttl=6:repeats=1 --new
--filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1 --new
--filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=1
"
MODE_FILTER=none
FLOWOFFLOAD=donttouch
INIT_APPLY_FW=1
DISABLE_IPV6=1
FILTER_TTL_EXPIRED_ICMP=1
'';
};
in
pkgs.lib.mapAttrs (name: content: pkgs.writeText name content) configFiles
@@ -0,0 +1,67 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.zapret2;
in
{
options.selfhosted.zapret2 = {
name = mkOption {
type = types.str;
};
socksPort = mkOption {
type = types.port;
};
ssPort = mkOption {
type = types.port;
};
};
config =
let
socksPort = "${toString cfg.socksPort}";
ssPort = "${toString cfg.ssPort}";
configFiles = import ./config.nix { inherit pkgs; };
in
{
virtualisation.oci-containers.containers.${cfg.name} = {
image = "vernette/ss-zapret2:latest";
volumes = [
"${configFiles.config}:/opt/zapret2/config"
];
environment = {
SOCKS_PORT = socksPort;
SS_PORT = ssPort;
SS_PASSWORD = "test";
SS_ENCRYPT_METHOD = "chacha20-ietf-poly1305";
SS_TIMEOUT = "300";
};
ports = [
"127.0.0.1:${socksPort}:${socksPort}"
"127.0.0.1:${ssPort}:${ssPort}"
];
extraOptions = [
"--cap-add=NET_ADMIN"
"--network=host"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
postStart = ''
mv /opt/zapret2/lua.dist /opt/zapret2/lua || true
'';
after = [ "${common.backend}-network-${common.network.name}.service" ];
requires = [ "${common.backend}-network-${common.network.name}.service" ];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
};
};
}
@@ -0,0 +1,24 @@
{ config, lib, ... }:
with lib;
let
cfg = config.selfhosted;
domain = "${config.sops.placeholder."domains/serv"}";
zerobyteDomain = "${cfg.zerobyte.subdomain}.${domain}";
trustedOrigins = [
"https://${zerobyteDomain}"
"https://${cfg.pocket-id.subdomain}.${domain}"
];
in
{
sops.secrets = {
"zerobyte/app_secret" = { };
};
sops.templates."zerobyte.env".content = ''
TZ=${config.time.timeZone}
BASE_URL=https://${zerobyteDomain}
APP_SECRET=${config.sops.placeholder."zerobyte/app_secret"}
TRUST_PROXY=true
TRUSTED_ORIGINS=${(concatStringsSep "," trustedOrigins)}
'';
}
@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
common = config.selfhosted.common;
cfg = config.selfhosted.zerobyte;
hostDataPath = "${common.dataDir}/zerobyte";
in
{
imports = [ ./config.nix ];
options.selfhosted.zerobyte = {
name = mkOption {
type = types.str;
};
subdomain = mkOption {
type = types.str;
};
hostPort = mkOption {
type = types.port;
};
};
config = {
virtualisation.oci-containers.containers.${cfg.name} = {
image = "ghcr.io/nicotsx/zerobyte:latest";
environmentFiles = [ config.sops.templates."zerobyte.env".path ];
ports = [
"0.0.0.0:${toString cfg.hostPort}:4096"
];
volumes = [
"/etc/localtime:/etc/localtime:ro"
"${hostDataPath}/data:/var/lib/zerobyte"
"${hostDataPath}/backups:/mydata"
];
};
systemd.services."${common.backend}-${cfg.name}" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
};
after = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
requires = [
"${common.backend}-network-${common.network.name}.service"
"${common.backend}-${config.selfhosted.traefik.name}.service"
];
partOf = [ "${common.backend}-${common.rootTarget.name}.target" ];
wantedBy = [ "${common.backend}-${common.rootTarget.name}.target" ];
preStart = ''
mkdir -p ${hostDataPath}/{data,backups}
'';
};
};
}
+53
View File
@@ -0,0 +1,53 @@
{
config,
lib,
pkgs,
...
}:
with lib;
{
sops.secrets = {
benq-password = {
neededForUsers = true;
};
};
users = {
mutableUsers = false;
users.benq = {
shell = pkgs.zsh;
isNormalUser = true;
ignoreShellProgramCheck = true;
hashedPasswordFile = config.sops.secrets.benq-password.path;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeIuJzR68xA4ugJjtWbwvaWEU852Hg9FAAhXNw8ou43 benq"
];
linger = true;
subUidRanges = [
{
startUid = 100000;
count = 65536;
}
];
subGidRanges = [
{
startGid = 100000;
count = 65536;
}
];
extraGroups =
let
ifTheyExist = groups: filter (group: hasAttr group config.users.groups) groups;
in
flatten [
"wheel"
(ifTheyExist [
"docker"
"git"
"networkmanager"
"video"
])
];
};
};
}
+10
View File
@@ -0,0 +1,10 @@
{ config, ... }:
{
boot.loader = {
systemd-boot = {
enable = true;
configurationLimit = 5;
};
efi.canTouchEfiVariables = true;
};
}
+32
View File
@@ -0,0 +1,32 @@
{
config,
lib,
pkgs,
...
}:
{
imports = lib.flatten [
./boot.nix
./network.nix
./hardware-configuration.nix
(map lib.custom.relativeToRoot (
[
"hosts/common/core"
"hosts/common/users/benq.nix"
"home/benq/homelab.nix"
]
++ (map (f: "hosts/common/optional/${f}") [
"bbr.nix"
"endlessh.nix"
"fail2ban.nix"
"fonts.nix"
"openssh.nix"
"selfhosted"
])
))
];
disko.cfg.mainDevice = "/dev/disk/by-id/ata-AMD_R5M120G8_07092225C0040";
environment.shells = [ pkgs.zsh ];
system.stateVersion = "26.05";
}
+36
View File
@@ -0,0 +1,36 @@
{
config,
lib,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"ahci"
"xhci_pci"
"nvme"
"usb_storage"
"usbhid"
"sd_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
# hdd
fileSystems."/mnt/data" = {
device = "/dev/disk/by-id/ata-WDC_WD20EFPX-68C4TN0_WD-WX32DA423Z41";
fsType = "ext4";
options = [
"noatime"
"nofail"
"x-systemd.device-timeout=5s"
];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
+14
View File
@@ -0,0 +1,14 @@
{ config, ... }:
{
networking = {
hostName = "homelab";
firewall = {
allowedTCPPorts = [ 22 80 443 2222 6969 ];
allowedUDPPorts = [ 3478 7359 ];
};
};
boot.kernel.sysctl = {
"net.ipv4.ip_unprivileged_port_start" = 80;
};
}
+4
View File
@@ -0,0 +1,4 @@
{ lib, ... }:
{
relativeToRoot = lib.path.append ../.;
}