initial ESS Community NixOS module
This commit is contained in:
parent
18b970a750
commit
997726aecd
11 changed files with 443 additions and 39 deletions
117
README.md
117
README.md
|
|
@ -1 +1,118 @@
|
||||||
# ess-helm-nixos
|
# ess-helm-nixos
|
||||||
|
|
||||||
|
NixOS configuration for hosting [Element Server Suite Community](https://element.io/server-suite/community) on a single VPS using K3s, Helm, and NixOS-managed nginx as the TLS-terminating reverse proxy.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
|
|
||||||
|
v :80 / :443
|
||||||
|
NixOS nginx (TLS via Let's Encrypt / ACME)
|
||||||
|
|
|
||||||
|
v :30080 (plain HTTP)
|
||||||
|
K3s ingress-nginx
|
||||||
|
|
|
||||||
|
v ClusterIP
|
||||||
|
ESS Helm chart
|
||||||
|
├── Synapse ess-helm.de
|
||||||
|
├── MAS auth.ess-helm.de
|
||||||
|
├── Element Web chat.ess-helm.de
|
||||||
|
├── Element Admin admin.ess-helm.de
|
||||||
|
└── Matrix RTC SFU mrtc.ess-helm.de (:30001 TCP / :30002 UDP direct)
|
||||||
|
```
|
||||||
|
|
||||||
|
Matrix user IDs: `@user:ess-helm.de`
|
||||||
|
|
||||||
|
## DNS setup
|
||||||
|
|
||||||
|
Create the following **A records** pointing to the VPS public IP:
|
||||||
|
|
||||||
|
Record | Type | Value
|
||||||
|
---|---|---
|
||||||
|
`ess-helm.de` | A | `<VPS public IP>`
|
||||||
|
`auth.ess-helm.de` | A | `<VPS public IP>`
|
||||||
|
`chat.ess-helm.de` | A | `<VPS public IP>`
|
||||||
|
`admin.ess-helm.de` | A | `<VPS public IP>`
|
||||||
|
`mrtc.ess-helm.de` | A | `<VPS public IP>`
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
| Port | Protocol | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| 80 | TCP | HTTP (ACME challenge + redirect to HTTPS) |
|
||||||
|
| 443 | TCP | HTTPS (all ESS services via nginx) |
|
||||||
|
| 30001 | TCP | Matrix RTC WebRTC TCP transport |
|
||||||
|
| 30002 | UDP | Matrix RTC WebRTC muxed UDP transport |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Element Web: [chat.ess-helm.de](https://chat.ess-helm.de)
|
||||||
|
- Federation tester: [federationtester.matrix.org/?server_name=ess-helm.de](https://federationtester.matrix.org/?server_name=ess-helm.de)
|
||||||
|
- Matrix client well-known: [ess-helm.de/.well-known/matrix/client](https://ess-helm.de/.well-known/matrix/client)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# flake.nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
|
||||||
|
|
||||||
|
ess-helm.url = "github:sid/ess-helm";
|
||||||
|
ess-helm.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11";
|
||||||
|
synix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ess-helm, synix, ... }: {
|
||||||
|
nixosConfigurations.my-server = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
ess-helm.nixosModules.ess-helm
|
||||||
|
synix.nixosModules.nginx
|
||||||
|
./configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
{
|
||||||
|
services.ess-helm = {
|
||||||
|
enable = true;
|
||||||
|
serverName = "example.com";
|
||||||
|
openFirewall = true;
|
||||||
|
configureNginx = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
security.acme.defaults.email = "admin@example.com";
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
forceSSL = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Subdomains default to `auth.`, `chat.`, `admin.`, `mrtc.`. Override individually if needed:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.ess-helm.elementWeb.subdomain = "element"; # -> element.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Upgrade the ESS or ingress-nginx chart version:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.ess-helm.ess = {
|
||||||
|
version = "26.X.Y";
|
||||||
|
hash = "...";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ESS Community](https://element.io/en/server-suite/community)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
ess-helm = mkNixosConfiguration "x86_64-linux" [ ./hosts/ess-helm ];
|
ess-helm = mkNixosConfiguration "x86_64-linux" [ ./hosts/ess-helm ];
|
||||||
};
|
};
|
||||||
|
|
||||||
packages = forAllSystems (pkgs: import ./pkgs pkgs);
|
packages = forAllSystems (pkgs: import ./pkgs { inherit pkgs inputs; });
|
||||||
|
|
||||||
devShells = forAllSystems (pkgs: {
|
devShells = forAllSystems (pkgs: {
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./boot.nix
|
./boot.nix
|
||||||
|
./ess-helm.nix
|
||||||
./hardware.nix
|
./hardware.nix
|
||||||
./networking.nix
|
./networking.nix
|
||||||
|
./nginx.nix
|
||||||
|
./openssh.nix
|
||||||
./packages.nix
|
./packages.nix
|
||||||
./services
|
|
||||||
./users.nix
|
./users.nix
|
||||||
|
|
||||||
inputs.synix.nixosModules.common
|
inputs.synix.nixosModules.common
|
||||||
|
|
|
||||||
12
hosts/ess-helm/ess-helm.nix
Normal file
12
hosts/ess-helm/ess-helm.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{ outputs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [ outputs.nixosModules.ess-helm ];
|
||||||
|
|
||||||
|
services.ess-helm = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
configureNginx = true;
|
||||||
|
serverName = "ess-helm.de";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
{
|
{ inputs, ... }:
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [ inputs.synix.nixosModules.nginx ];
|
imports = [ inputs.synix.nixosModules.nginx ];
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
forceSSL = true;
|
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
|
forceSSL = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./ess.nix
|
|
||||||
./nginx.nix
|
|
||||||
./openssh.nix
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
{ pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
services.k3s = {
|
|
||||||
enable = true;
|
|
||||||
role = "server";
|
|
||||||
extraFlags = toString [
|
|
||||||
"--disable traefik"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
kubectl
|
|
||||||
kubernetes-helm
|
|
||||||
];
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# system.activationScripts.install-ess = {
|
|
||||||
# text = ''
|
|
||||||
# ${pkgs.kubernetes-helm}/bin/helm upgrade --install ess element-hq/element-server-suite -f /path/to/values.yaml -n ess --create-namespace
|
|
||||||
# '';
|
|
||||||
# deps = [ ];
|
|
||||||
# };
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
common = import ./common;
|
common = import ./common;
|
||||||
|
ess-helm = import ./ess-helm;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
301
modules/nixos/ess-helm/default.nix
Normal file
301
modules/nixos/ess-helm/default.nix
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.ess-helm;
|
||||||
|
|
||||||
|
inherit (lib)
|
||||||
|
literalExpression
|
||||||
|
mkDefault
|
||||||
|
mkEnableOption
|
||||||
|
mkIf
|
||||||
|
mkOption
|
||||||
|
recursiveUpdate
|
||||||
|
types
|
||||||
|
;
|
||||||
|
|
||||||
|
chartModule =
|
||||||
|
{
|
||||||
|
defaultRepo,
|
||||||
|
defaultName,
|
||||||
|
defaultVersion,
|
||||||
|
defaultHash,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
types.submodule {
|
||||||
|
options = {
|
||||||
|
repo = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultRepo;
|
||||||
|
description = "Helm chart repository URL. For OCI registries this must include the chart name (e.g. `oci://ghcr.io/org/repo/chart`).";
|
||||||
|
};
|
||||||
|
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultName;
|
||||||
|
description = "Chart name. For HTTP repositories appended to the pull command. For OCI repositories only used to name the store derivation.";
|
||||||
|
};
|
||||||
|
|
||||||
|
version = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultVersion;
|
||||||
|
description = "Chart version to deploy.";
|
||||||
|
example = "26.5.0";
|
||||||
|
};
|
||||||
|
|
||||||
|
hash = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultHash;
|
||||||
|
description = "SRI hash of the chart `.tgz` archive.";
|
||||||
|
example = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraValues = mkOption {
|
||||||
|
type = types.attrs;
|
||||||
|
default = { };
|
||||||
|
description = "Additional Helm values merged on top of the module-generated ones";
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
synapse.additional."user-config.yaml".config = "max_upload_size: 100M";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceModule =
|
||||||
|
{ defaultSubdomain }:
|
||||||
|
types.submodule (
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
subdomain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultSubdomain;
|
||||||
|
description = "Subdomain for this service. Full hostname is `<subdomain>.<serverName>`, or just `<serverName>` when empty (Synapse).";
|
||||||
|
};
|
||||||
|
|
||||||
|
forceSSL = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Redirect plain-HTTP requests to HTTPS.";
|
||||||
|
};
|
||||||
|
|
||||||
|
enableACME = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
description = "Obtain a Let's Encrypt certificate for this vhost. Defaults to the value of `forceSSL`.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config.enableACME = mkDefault config.forceSSL;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
hostFor = svc: if svc.subdomain == "" then cfg.serverName else "${svc.subdomain}.${cfg.serverName}";
|
||||||
|
|
||||||
|
vhostFor = svc: {
|
||||||
|
inherit (svc) forceSSL enableACME;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString cfg.ingressNginxPort}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 50M;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.ess-helm = {
|
||||||
|
|
||||||
|
enable = mkEnableOption "Element Server Suite Community";
|
||||||
|
|
||||||
|
serverName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Matrix server name — the domain part of every Matrix ID (`@user:<serverName>`). Must point at this host and **cannot be changed** after the first deployment without wiping the database.";
|
||||||
|
example = "example.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Open the Matrix RTC WebRTC ports in the firewall (TCP 30001, UDP 30002).";
|
||||||
|
};
|
||||||
|
|
||||||
|
configureNginx = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Configure nginx as a TLS-terminating reverse proxy for all ESS services, proxying to ingress-nginx on `ingressNginxPort`.";
|
||||||
|
};
|
||||||
|
|
||||||
|
ingressNginxPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 30080;
|
||||||
|
description = "NodePort for ingress-nginx plain HTTP. Must be in the Kubernetes NodePort range (30000–32767).";
|
||||||
|
};
|
||||||
|
|
||||||
|
ingressNginxTlsPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 30443;
|
||||||
|
description = "NodePort reserved for ingress-nginx HTTPS (unused at runtime but required by the chart schema). Must be in the Kubernetes NodePort range (30000–32767).";
|
||||||
|
};
|
||||||
|
|
||||||
|
ipFamily = mkOption {
|
||||||
|
type = types.enum [
|
||||||
|
"ipv4"
|
||||||
|
"ipv6"
|
||||||
|
"dual"
|
||||||
|
];
|
||||||
|
default = "ipv4";
|
||||||
|
description = "IP family for the ESS cluster networking.";
|
||||||
|
};
|
||||||
|
|
||||||
|
synapse = mkOption {
|
||||||
|
type = serviceModule { defaultSubdomain = ""; };
|
||||||
|
default = { };
|
||||||
|
description = "Synapse homeserver (served at the root serverName domain).";
|
||||||
|
};
|
||||||
|
|
||||||
|
matrixAuthenticationService = mkOption {
|
||||||
|
type = serviceModule { defaultSubdomain = "auth"; };
|
||||||
|
default = { };
|
||||||
|
description = "Matrix Authentication Service (MAS).";
|
||||||
|
};
|
||||||
|
|
||||||
|
elementWeb = mkOption {
|
||||||
|
type = serviceModule { defaultSubdomain = "chat"; };
|
||||||
|
default = { };
|
||||||
|
description = "Element Web client.";
|
||||||
|
};
|
||||||
|
|
||||||
|
elementAdmin = mkOption {
|
||||||
|
type = serviceModule { defaultSubdomain = "admin"; };
|
||||||
|
default = { };
|
||||||
|
description = "Element Admin console.";
|
||||||
|
};
|
||||||
|
|
||||||
|
matrixRTC = mkOption {
|
||||||
|
type = serviceModule { defaultSubdomain = "mrtc"; };
|
||||||
|
default = { };
|
||||||
|
description = "Matrix RTC SFU signalling endpoint.";
|
||||||
|
};
|
||||||
|
|
||||||
|
ess = mkOption {
|
||||||
|
type = chartModule {
|
||||||
|
name = "ess";
|
||||||
|
defaultRepo = "oci://ghcr.io/element-hq/ess-helm/matrix-stack";
|
||||||
|
defaultName = "matrix-stack";
|
||||||
|
defaultVersion = "26.5.0";
|
||||||
|
defaultHash = "sha256-2YfDk29c8MoEa7rJXnHiKgKVqL7I0mmIcbtqgh2tunU=";
|
||||||
|
};
|
||||||
|
default = { };
|
||||||
|
description = "ESS Community Helm chart pin and extra values.";
|
||||||
|
};
|
||||||
|
|
||||||
|
ingressNginx = mkOption {
|
||||||
|
type = chartModule {
|
||||||
|
name = "ingressNginx";
|
||||||
|
defaultRepo = "https://kubernetes.github.io/ingress-nginx";
|
||||||
|
defaultName = "ingress-nginx";
|
||||||
|
defaultVersion = "4.15.1";
|
||||||
|
defaultHash = "sha256-Pv8L0YFR1uaxxEFGNBBXFEPdoax4KSyxiTRmKN54Tww=";
|
||||||
|
};
|
||||||
|
default = { };
|
||||||
|
description = "ingress-nginx Helm chart pin and extra values.";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
|
services.k3s = {
|
||||||
|
enable = true;
|
||||||
|
role = "server";
|
||||||
|
disable = [ "traefik" ]; # use ingress-nginx
|
||||||
|
|
||||||
|
autoDeployCharts = {
|
||||||
|
|
||||||
|
ingress-nginx = recursiveUpdate {
|
||||||
|
inherit (cfg.ingressNginx)
|
||||||
|
repo
|
||||||
|
name
|
||||||
|
version
|
||||||
|
hash
|
||||||
|
;
|
||||||
|
targetNamespace = "ingress-nginx";
|
||||||
|
createNamespace = true;
|
||||||
|
values = {
|
||||||
|
controller = {
|
||||||
|
ingressClassResource.default = true;
|
||||||
|
service = {
|
||||||
|
type = "NodePort";
|
||||||
|
nodePorts = {
|
||||||
|
http = cfg.ingressNginxPort;
|
||||||
|
https = cfg.ingressNginxTlsPort;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
use-forwarded-headers = "true";
|
||||||
|
compute-full-forwarded-for = "true";
|
||||||
|
proxy-read-timeout = "86400";
|
||||||
|
proxy-send-timeout = "86400";
|
||||||
|
proxy-body-size = "50m";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} { values = cfg.ingressNginx.extraValues; };
|
||||||
|
|
||||||
|
ess = recursiveUpdate {
|
||||||
|
inherit (cfg.ess)
|
||||||
|
repo
|
||||||
|
name
|
||||||
|
version
|
||||||
|
hash
|
||||||
|
;
|
||||||
|
targetNamespace = "ess";
|
||||||
|
createNamespace = true;
|
||||||
|
values = {
|
||||||
|
inherit (cfg) serverName;
|
||||||
|
ingress = {
|
||||||
|
className = "nginx";
|
||||||
|
tlsEnabled = false;
|
||||||
|
};
|
||||||
|
synapse.ingress.host = hostFor cfg.synapse;
|
||||||
|
matrixAuthenticationService.ingress.host = hostFor cfg.matrixAuthenticationService;
|
||||||
|
elementWeb.ingress.host = hostFor cfg.elementWeb;
|
||||||
|
elementAdmin.ingress.host = hostFor cfg.elementAdmin;
|
||||||
|
matrixRTC.ingress.host = hostFor cfg.matrixRTC;
|
||||||
|
networking.ipFamily = cfg.ipFamily;
|
||||||
|
};
|
||||||
|
} { values = cfg.ess.extraValues; };
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = mkIf cfg.configureNginx {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts = {
|
||||||
|
"${hostFor cfg.synapse}" = vhostFor cfg.synapse;
|
||||||
|
"${hostFor cfg.matrixAuthenticationService}" = vhostFor cfg.matrixAuthenticationService;
|
||||||
|
"${hostFor cfg.elementWeb}" = vhostFor cfg.elementWeb;
|
||||||
|
"${hostFor cfg.elementAdmin}" = vhostFor cfg.elementAdmin;
|
||||||
|
"${hostFor cfg.matrixRTC}" = vhostFor cfg.matrixRTC;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.openFirewall {
|
||||||
|
allowedTCPPorts = [ 30001 ];
|
||||||
|
allowedUDPPorts = [ 30002 ];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.variables.KUBECONFIG = "/etc/rancher/k3s/k3s.yaml";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
# packages in `pkgs/` accessible through 'pkgs.local'
|
# packages in `pkgs/` accessible through 'pkgs.local'
|
||||||
local-packages = final: _prev: { local = import ../pkgs { pkgs = final; }; };
|
local-packages = final: _prev: {
|
||||||
|
local = import ../pkgs {
|
||||||
|
pkgs = final;
|
||||||
|
inherit inputs;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# https://nixos.wiki/wiki/Overlays
|
# https://nixos.wiki/wiki/Overlays
|
||||||
modifications =
|
modifications =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue