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
|
||||
|
||||
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 ];
|
||||
};
|
||||
|
||||
packages = forAllSystems (pkgs: import ./pkgs pkgs);
|
||||
packages = forAllSystems (pkgs: import ./pkgs { inherit pkgs inputs; });
|
||||
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@
|
|||
{
|
||||
imports = [
|
||||
./boot.nix
|
||||
./ess-helm.nix
|
||||
./hardware.nix
|
||||
./networking.nix
|
||||
./nginx.nix
|
||||
./openssh.nix
|
||||
./packages.nix
|
||||
./services
|
||||
./users.nix
|
||||
|
||||
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 ];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
forceSSL = 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;
|
||||
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'
|
||||
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
|
||||
modifications =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue