Move frontends to 80xx and add mail server

Reassign multiple service frontends from 81xx to 80xx ports (Forgejo,
ntopng, AdGuard, Scrutiny, Paperless, Whats Up Docker, etc.) and update
homepage links.

Configure ACME (webroot) and add certs for kempinger.at,
webadmin.kempinger.at,
and bilder.kempinger.at; update nginx virtual hosts to use ACME hosts
and
serve the ACME challenge path.

Add users stalwart-mail and nginx to the acme group and open
SMTP-related
firewall ports (25, 587) plus mail UI ports (8090, 8091).

Add and configure the Stalwart mail service (SMTP, submissions, IMAP,
JMAP)
and adjust related service ports/settings (ntopng, scrutiny, influxdb,
WUD).
This commit is contained in:
Stefan Kempinger 2026-02-17 00:24:14 +01:00
parent fb39daf448
commit 143299ccf7

View file

@ -46,8 +46,6 @@
keyMap = "de";
# useXkbConfig = true; # use xkb.options in tty.
};
# i18n.defaultLocale = "en_US.UTF-8";
# Networking
networking = {
hostName = "heimserver";
@ -64,11 +62,13 @@
firewall.enable = true;
firewall.allowedTCPPorts = [
22
25
53
80
443
587
2222 # forgejo ssh
8184 # forgejo frontend
8084 # forgejo frontend
8123 # homeassistant
5580 # homeassistant matter
2283 # immich
@ -78,11 +78,13 @@
8554 # frigate rtsp
8555 # frigate rtsp
2055 # ntopng sink
8182 # ntopng frontend
8183 # adguardhome frontend
8185 # scrutiny frontend
8186 # wud frontend
8187 # paperless frontend
8088 # ntopng frontend
8083 # adguardhome frontend
8085 # scrutiny frontend
8089 # wud frontend
8087 # paperless frontend
8090 # mail
8091 # mail jmap
8080 # homepage
];
firewall.allowedUDPPorts = [
@ -99,6 +101,14 @@
];
};
users.users."stalwart-mail".extraGroups = [
"acme"
];
users.users."nginx".extraGroups = [
"acme"
];
users.users.immich.extraGroups = [
"video"
"render"
@ -143,7 +153,30 @@
security.acme = {
acceptTerms = true;
defaults.email = "mail@kempinger.xyz";
certs."kempinger.at".domain = "*.kempinger.at";
defaults.webroot = "/var/lib/acme/acme-challenge/";
certs."kempinger.at" = {
domain = "kempinger.at";
extraDomainNames = [
"git.kempinger.at"
];
reloadServices = [
"nginx"
];
};
certs."webadmin.kempinger.at" = {
domain = "webadmin.kempinger.at";
extraDomainNames = [
"mta-sts.kempinger.at"
"autoconfig.kempinger.at"
"autodiscover.kempinger.at"
"mail.kempinger.at"
"imap.kempinger.at"
"mx1.kempinger.at"
];
};
certs."bilder.kempinger.at" = {
domain = "bilder.kempinger.at";
};
};
#services.resolved.enable = true;
@ -163,15 +196,30 @@
};
virtualHosts."kempinger.at" = {
root = "/srv/website/public_html";
locations."/" = {
index = "index.html";
};
locations."/".index = "index.html";
forceSSL = true;
enableACME = true;
useACMEHost = "kempinger.at";
locations."/.well-known/".root = "/var/lib/acme/acme-challenge/";
};
virtualHosts."webadmin.kempinger.at" = {
forceSSL = true;
useACMEHost = "webadmin.kempinger.at";
#acmeRoot = null;
serverAliases = [
"mta-sts.kempinger.at"
"autoconfig.kempinger.at"
"autodiscover.kempinger.at"
"mail.kempinger.at"
"imap.kempinger.at"
"mx1.kempinger.at"
];
locations."/" = {
proxyPass = "http://127.0.0.1:8090";
};
};
virtualHosts.${config.services.forgejo.settings.server.DOMAIN} = {
forceSSL = true;
enableACME = true;
useACMEHost = "kempinger.at";
extraConfig = ''
client_max_body_size 512M;
'';
@ -179,8 +227,8 @@
"http://localhost:${toString config.services.forgejo.settings.server.HTTP_PORT}";
};
virtualHosts."bilder.kempinger.at" = {
enableACME = true;
forceSSL = true;
useACMEHost = "bilder.kempinger.at";
locations."/" = {
proxyPass = "http://[::1]:${toString config.services.immich.port}";
proxyWebsockets = true;
@ -205,7 +253,7 @@
DOMAIN = "git.kempinger.at";
# You need to specify this to remove the port from URLs in the web UI.
ROOT_URL = "https://${config.services.forgejo.settings.server.DOMAIN}/";
HTTP_PORT = 8184;
HTTP_PORT = 8084;
DISABLE_SSH = false;
SSH_PORT = 2222;
START_SSH_SERVER = true;
@ -347,8 +395,6 @@
};
};
#services.matter-server.enable = true;
virtualisation.oci-containers = {
backend = "podman";
containers.homeassistant = {
@ -440,8 +486,8 @@
"/var/run/podman/podman.sock:/var/run/docker.sock"
];
environment = {
WUD_SERVER_PORT = "8186";
WUD_TRIGGER_COMMAND_LOCAL_CMD="echo \${display_name} can be updated to \${update_kind_remote_value}";
WUD_SERVER_PORT = "8089";
WUD_TRIGGER_COMMAND_LOCAL_CMD = "echo \${display_name} can be updated to \${update_kind_remote_value}";
};
extraOptions = [
"--network=host"
@ -451,7 +497,7 @@
services.ntopng = {
enable = true;
httpPort = 8182;
httpPort = 8088;
interfaces = [ "tcp://0.0.0.0:5556" ];
extraConfig = ''
--dns-mode 1
@ -460,6 +506,9 @@
services.influxdb2 = {
enable = true;
settings = {
http-bind-address = ":8086";
};
# provision = {
# enable = true;
@ -490,7 +539,7 @@
services.scrutiny = {
enable = true;
settings.web.listen.port = 8185;
settings.web.listen.port = 8085;
influxdb.enable = true;
collector.schedule = "hourly";
settings.web.influxdb = {
@ -504,14 +553,14 @@
enable = true;
# You can select any ip and port, just make sure to open firewalls where needed
host = "0.0.0.0";
port = 8183;
port = 8083;
};
services.paperless = {
enable = true;
consumptionDirIsPublic = true;
address = "0.0.0.0";
port = 8187;
port = 8087;
settings = {
PAPERLESS_CONSUMER_IGNORE_PATTERN = [
".DS_STORE/*"
@ -525,7 +574,6 @@
};
};
services.homepage-dashboard = {
enable = true;
listenPort = 8080;
@ -628,7 +676,7 @@
icon = "forgejo.png";
widget = {
type = "gitea"; # Forgejo uses Gitea API
url = "http://192.168.69.69:8184";
url = "http://192.168.69.69:8084";
key = "{{HOMEPAGE_VAR_FORGEJO_TOKEN}}"; # Create in Forgejo settings
# Shows: repository count, issue count, pull requests
};
@ -640,12 +688,12 @@
"Network & Monitoring" = [
{
"AdGuard Home" = {
href = "http://192.168.69.69:8183";
href = "http://192.168.69.69:8083";
description = "DNS filtering & ad blocking";
icon = "adguard-home.png";
widget = {
type = "adguard";
url = "http://192.168.69.69:8183";
url = "http://192.168.69.69:8083";
username = "{{HOMEPAGE_VAR_ADGUARD_USER}}";
password = "{{HOMEPAGE_VAR_ADGUARD_PASS}}";
# Shows: queries blocked, % blocked, queries processed
@ -667,29 +715,29 @@
}
{
"Scrutiny" = {
href = "http://192.168.69.69:8185";
href = "http://192.168.69.69:8085";
description = "S.M.A.R.T Monitoring";
icon = "scrutiny.png";
widget = {
type = "scrutiny";
url = "http://192.168.69.69:8185";
url = "http://192.168.69.69:8085";
};
};
}
{
"Whats Up Docker" = {
href = "http://192.168.69.69:8186";
href = "http://192.168.69.69:8089";
description = "Docker Image Updates";
icon = "whats-up-docker.png";
widget = {
type = "whatsupdocker";
url = "http://192.168.69.69:8186";
url = "http://192.168.69.69:8089";
};
};
}
{
"ntopng" = {
href = "http://192.168.69.69:8182";
href = "http://192.168.69.69:8088";
description = "Network traffic analysis";
icon = "ntopng.png";
# No official widget, but could use iframe or custom API
@ -787,6 +835,64 @@
environmentFile = "/var/lib/homepage-dashboard/secrets.env";
};
services.stalwart = {
enable = true;
openFirewall = true;
settings = {
server = {
hostname = "mx1.kempinger.at";
tls = {
enable = true;
implicit = true;
};
listener = {
smtp = {
protocol = "smtp";
bind = "192.168.69.69:25";
};
submissions = {
bind = "192.168.69.69:587";
protocol = "smtp";
tls.implicit = true;
};
imaps = {
bind = "[::]:993";
protocol = "imap";
tls.implicit = true;
};
jmap = {
bind = "0.0.0.0:8091";
url = "https://mail.kempinger.at";
protocol = "http";
};
management = {
bind = [ "127.0.0.1:8090" ];
protocol = "http";
};
};
};
resolver.type = "custom";
resolver.custom = [ "udp://127.0.0.1:53" ];
certificate."default" = {
cert = "%{file:${config.security.acme.certs."webadmin.kempinger.at".directory}/fullchain.pem}%";
private-key = "%{file:${config.security.acme.certs."webadmin.kempinger.at".directory}/key.pem}%";
};
lookup.default = {
hostname = "mx1.kempinger.at";
domain = "kempinger.at";
};
session.rcpt.directory = "'internal'";
directory."imap".lookup.domains = [ "kempinger.at" ];
# authentication.fallback-admin = {
# user = "admin";
# secret = "bcrypt-hash";
# };
};
};
nixpkgs.config.allowUnfree = true;
# nixpkgs.overlays = [