home lab init

This commit is contained in:
plasmagoat 2025-06-03 23:07:46 +02:00
commit 7278922625
65 changed files with 27336 additions and 0 deletions

View file

@ -0,0 +1,46 @@
{ config, pkgs, modulesPath, lib, ... }:
{
# Pull in all the shared settings from configuration.nix
imports = [ ../configuration.nix ];
config = lib.recursiveUpdate config ({
# (Here, add anything liveVMspecific—e.g. NFS mounts, Docker, Compose service,
# static IP, or “import users/plasmagoat.nix” if you prefer.)
networking.interfaces.enp0s25 = {
useDHCP = false;
ipv4.addresses = [ { address = "192.168.1.50"; prefixLength = 24; } ];
ipv4.gateway = "192.168.1.1";
};
# Docker + Compose bits, for example:
fileSystems."/mnt/nas" = {
device = "192.168.1.100:/export/docker-volumes";
fsType = "nfs";
options = [ "defaults" "nofail" "x-systemd.requires=network-online.target" ];
};
environment.systemPackages = with pkgs; [
pkgs.docker
pkgs.docker-compose
# …plus anything else you want only on live VM…
];
services.docker.enable = true;
systemd.services.dockerComposeApp = {
description = "Auto-start DockerCompose stack";
after = [ "network-online.target" "docker.service" ];
wants = [ "network-online.target" "docker.service" ];
serviceConfig = {
WorkingDirectory = "/etc/docker-compose-app";
ExecStart = "${pkgs.docker-compose}/bin/docker-compose -f /etc/docker-compose-app/docker-compose.yml up";
ExecStop = "${pkgs.docker-compose}/bin/docker-compose -f /etc/docker-compose-app/docker-compose.yml down";
Restart = "always";
RestartSec = 10;
};
wantedBy = [ "multi-user.target" ];
};
});
}

View file

@ -0,0 +1,17 @@
🥇 Phase 1: Git + Secrets
✅ Set up Forgejo VM (NixOS declarative)
✅ Set up sops-nix + age keys (can live in the Git repo)
✅ Push flake + ansible + secrets to Forgejo
✅ Write a basic README with how to rebuild infra
🥈 Phase 2: GitOps
🔁 Add CI runner VM
🔁 Configure runner to deploy (nixos-rebuild or ansible-playbook) on commit
🔁 Optional: add webhooks to auto-trigger via Forgejo

View file

@ -0,0 +1,31 @@
{ lib, pkgs, config, ... }:
{
systemd.services.forgejo = {
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
};
services.postgresql = {
enable = true;
ensureDatabases = [ "forgejo" ];
ensureUsers = [
{
name = "forgejo";
ensureDBOwnership = true;
}
];
authentication = pkgs.lib.mkOverride 10 ''
#type database DBuser auth-method
local all all trust
'';
};
services.prometheus.exporters.postgres = {
enable = true;
listenAddress = "0.0.0.0";
port = 9187;
};
networking.firewall.allowedTCPPorts = [ 9187 ];
}

View file

@ -0,0 +1,65 @@
{ lib, pkgs, config, ... }:
let
cfg = config.services.forgejo;
srv = cfg.settings.server;
domain = "git.procopius.dk";
in
{
users.users.plasmagoat.extraGroups = [ "forgejo" ];
services.forgejo = {
enable = true;
user = "forgejo";
group = "forgejo";
stateDir = "/srv/forgejo";
settings = {
# https://forgejo.org/docs/latest/admin/config-cheat-sheet/
server = {
DOMAIN = domain;
ROOT_URL = "https://${srv.DOMAIN}/";
PROTOCOL = "http";
HTTP_PORT = 3000;
};
database = {
DB_TYPE = lib.mkForce "postgres";
HOST = "/run/postgresql";
NAME = "forgejo";
USER = "forgejo";
};
service = {
DISABLE_REGISTRATION = true;
};
metrics = {
ENABLED = true;
ENABLED_ISSUE_BY_REPOSITORY = true;
ENABLED_ISSUE_BY_LABEL = true;
};
# log = {
# ROOT_PATH = "/var/log/forgejo";
# MODE = "file";
# LEVEL = "Info";
# };
security = {
INSTALL_LOCK = true;
SECRET_KEY = "changeme"; # can be another secret
};
};
};
sops.secrets.forgejo-admin-password.owner = "forgejo";
sops.secrets.forgejo-db-password.owner = "forgejo";
systemd.services.forgejo.preStart = let
adminCmd = "${lib.getExe cfg.package} admin user";
user = "plasmagoat"; # Note, Forgejo doesn't allow creation of an account named "admin"
pwd = config.sops.secrets.forgejo-admin-password;
in ''
${adminCmd} create --admin --email "root@localhost" --username ${user} --password "$(tr -d '\n' < ${pwd.path})" || true
## uncomment this line to change an admin user which was already created
# ${adminCmd} change-password --username ${user} --password "$(tr -d '\n' < ${pwd.path})" || true
'';
# Optional: firewall
networking.firewall.allowedTCPPorts = [ 3000 ];
}

View file

@ -0,0 +1,12 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
../../secrets/sops.nix
./networking.nix
./storage.nix
./forgejo.nix
./database.nix
];
}

View file

@ -0,0 +1,6 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "forgejo";
};
}

View file

@ -0,0 +1,29 @@
{
# services.nfs.client = {
# enable = true;
# idmapd.enable = true;
# };
# environment.etc."idmapd.conf".text = ''
# [General]
# Domain = localdomain
# [Mapping]
# Nobody-User = nobody
# Nobody-Group = nogroup
# '';
boot.supportedFilesystems = [ "nfs" ];
services.rpcbind.enable = true;
fileSystems."/srv/forgejo" = {
device = "192.168.1.226:/volume1/data/forgejo";
fsType = "nfs4";
options = [ "x-systemd.automount" "noatime" "_netdev" ];
};
systemd.tmpfiles.rules = [
"d /srv/forgejo 0750 forgejo forgejo -"
];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,744 @@
{
"__inputs": [],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.5.5"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "panel",
"id": "piechart",
"name": "Pie chart v2",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Traefik dashboard prometheus",
"editable": true,
"gnetId": 4475,
"graphTooltip": 0,
"id": null,
"iteration": 1620932097756,
"links": [],
"panels": [
{
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 10,
"title": "$backend stats",
"type": "row"
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value", "percent"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "traefik_service_requests_total{service=\"$service\"}",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{method}} : {{code}}",
"refId": "A"
}
],
"title": "$service return code",
"type": "piechart"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"format": "ms",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 1
},
"id": 4,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "",
"targets": [
{
"exemplar": true,
"expr": "sum(traefik_service_request_duration_seconds_sum{service=\"$service\"}) / sum(traefik_service_requests_total{service=\"$service\"}) * 1000",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "",
"refId": "A"
}
],
"thresholds": "",
"title": "$service response time",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 8
},
"hiddenSeries": false,
"id": 3,
"legend": {
"alignAsTable": true,
"avg": true,
"current": false,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_service_requests_total{service=\"$service\"}[5m]))",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "Total requests $service",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Total requests over 5min $service",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 15
},
"id": 12,
"panels": [],
"title": "Global stats",
"type": "row"
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 16
},
"hiddenSeries": false,
"id": 5,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\",code=\"200\"}[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{method}} : {{code}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Status code 200 over 5min",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 16
},
"hiddenSeries": false,
"id": 6,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"max": true,
"min": true,
"rightSide": true,
"show": true,
"total": false,
"values": true
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.5.5",
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\",code!=\"200\"}[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "{{ method }} : {{code}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Others status code over 5min",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 23
},
"id": 7,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["sum"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_service_requests_total[5m])) by (service) ",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ service }}",
"refId": "A"
}
],
"title": "Requests by service",
"type": "piechart"
},
{
"cacheTimeout": null,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"decimals": 0,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 23
},
"id": 8,
"interval": null,
"links": [],
"maxDataPoints": 3,
"options": {
"displayLabels": [],
"legend": {
"calcs": [],
"displayMode": "table",
"placement": "right",
"values": ["value"]
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["sum"],
"fields": "",
"values": false
},
"text": {}
},
"targets": [
{
"exemplar": true,
"expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint =~ \"$entrypoint\"}[5m])) by (entrypoint) ",
"format": "time_series",
"interval": "",
"intervalFactor": 2,
"legendFormat": "{{ entrypoint }}",
"refId": "A"
}
],
"title": "Requests by protocol",
"type": "piechart"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": ["traefik", "prometheus"],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "Prometheus",
"definition": "label_values(service)",
"description": null,
"error": null,
"hide": 0,
"includeAll": false,
"label": null,
"multi": false,
"name": "service",
"options": [],
"query": {
"query": "label_values(service)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"current": {},
"datasource": "Prometheus",
"definition": "",
"description": null,
"error": null,
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "entrypoint",
"options": [],
"query": {
"query": "label_values(entrypoint)",
"refId": "Prometheus-entrypoint-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Traefik",
"uid": "qPdAviJmz",
"version": 10
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
{ config, pkgs, modulesPath, lib, ... }:
{
services.grafana.enable = true;
services.grafana.settings.server = {
http_port = 3000;
http_addr = "0.0.0.0";
# Grafana needs to know on which domain and URL it's running
# domain = "monitor.local";
# root_url = "https://monitor.local/grafana/"; # Not needed if it is `https://your.domain/`
# serve_from_sub_path = true;
};
networking.firewall.allowedTCPPorts = [ 3000 ];
services.grafana = {
# declarativePlugins = with pkgs.grafanaPlugins; [ ... ];
provision = {
enable = true;
datasources.settings.datasources = [
# "Built-in" datasources can be provisioned - c.f. https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources
{
name = "Prometheus";
type = "prometheus";
url = "http://127.0.0.1:${toString config.services.prometheus.port}";
}
{
name = "Loki";
type = "loki";
url = "http://127.0.0.1:${toString config.services.loki.configuration.server.http_listen_port}";
}
# Some plugins also can - c.f. https://grafana.com/docs/plugins/yesoreyeram-infinity-datasource/latest/setup/provisioning/
# {
# name = "Infinity";
# type = "yesoreyeram-infinity-datasource";
# }
# But not all - c.f. https://github.com/fr-ser/grafana-sqlite-datasource/issues/141
];
# Note: removing attributes from the above `datasources.settings.datasources` is not enough for them to be deleted on `grafana`;
# One needs to use the following option:
# datasources.settings.deleteDatasources = [ { name = "foo"; orgId = 1; } { name = "bar"; orgId = 1; } ];
dashboards.settings.providers = [{
name = "my dashboards";
options.path = "/etc/grafana-dashboards";
}];
};
};
environment.etc."grafana-dashboards/traefik.json" = {
source = ./dashboards/traefik.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/grafana-traefik.json" = {
source = ./dashboards/grafana-traefik.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/node-exporter.json" = {
source = ./dashboards/node-exporter.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/promtail.json" = {
source = ./dashboards/promtail.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/gitea.json" = {
source = ./dashboards/gitea.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
environment.etc."grafana-dashboards/postgres.json" = {
source = ./dashboards/postgres.json;
user = "grafana";
group = "grafana";
mode = "0644";
};
}

View file

@ -0,0 +1,11 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./prometheus.nix
./grafana.nix
./loki.nix
];
}

View file

@ -0,0 +1,37 @@
{
networking.firewall.allowedTCPPorts = [ 3100 ];
services.loki = {
enable = true;
configuration = {
server.http_listen_port = 3100;
auth_enabled = false;
analytics.reporting_enabled = false;
common = {
ring = {
instance_addr = "127.0.0.1";
kvstore.store = "inmemory";
};
replication_factor = 1;
path_prefix = "/tmp/loki";
};
schema_config = {
configs = [
{
from = "2020-05-15";
store = "tsdb";
object_store = "filesystem";
schema = "v13";
index = {
prefix = "index_";
period = "24h";
};
}
];
};
storage_config.filesystem.directory = "/var/lib/loki/chunk";
};
};
}

View file

@ -0,0 +1,17 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "monitor";
# interfaces.eth0 = {
# ipv4.addresses = [{
# address = "192.168.1.171";
# prefixLength = 24;
# }];
# };
# firewall.allowedTCPPorts = [ 80 3000 9090 ];
# defaultGateway = {
# address = "192.168.1.1";
# interface = "eth0";
# };
};
}

View file

@ -0,0 +1,70 @@
{ config, pkgs, modulesPath, lib, ... }:
let
monitor_ip = "monitor.local";
traefik_ip = "traefik.local";
sandbox_ip = "sandbox.local";
forgejo_ip = "forgejo.local";
prometheus_exporter_port = 9100;
promtail_port = 9080;
traefik_monitor_port = 8082;
forgejo_monitor_port = 3000;
in {
networking.firewall.allowedTCPPorts = [ 9090 ];
services.prometheus = {
enable = true;
retentionTime = "7d";
globalConfig = {
scrape_timeout = "10s";
scrape_interval = "30s";
};
scrapeConfigs = [
{
job_name = "node";
static_configs = [
{
targets = [
"${monitor_ip}:${toString prometheus_exporter_port}"
"${traefik_ip}:${toString prometheus_exporter_port}"
"${sandbox_ip}:${toString prometheus_exporter_port}"
"${forgejo_ip}:${toString prometheus_exporter_port}"
];
}
];
}
{
job_name = "traefik";
static_configs = [
{ targets = [ "${traefik_ip}:${toString traefik_monitor_port}" ]; }
];
}
{
job_name = "gitea";
static_configs = [
{ targets = [ "${forgejo_ip}:${toString forgejo_monitor_port}" ]; }
];
}
{
job_name = "postgres";
static_configs = [
{ targets = [ "${forgejo_ip}:9187" ]; }
];
}
{
job_name = "promtail";
static_configs = [
{
targets = [
"${monitor_ip}:${toString promtail_port}"
"${traefik_ip}:${toString promtail_port}"
"${sandbox_ip}:${toString promtail_port}"
"${forgejo_ip}:${toString promtail_port}"
];
}
];
}
];
};
}

View file

@ -0,0 +1,10 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./storage.nix
./sandbox.nix
];
}

View file

@ -0,0 +1,20 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "sandbox";
interfaces.eth0 = {
ipv4.addresses = [{
address = "192.168.1.148";
prefixLength = 24;
}];
ipv6.addresses = [{
address = "fe80::148";
prefixLength = 64;
}];
};
defaultGateway = {
address = "192.168.1.1";
interface = "eth0";
};
};
}

View file

@ -0,0 +1,4 @@
{ config, pkgs, modulesPath, lib, ... }:
{
}

View file

@ -0,0 +1,11 @@
{
boot.supportedFilesystems = [ "nfs" ];
services.rpcbind.enable = true;
fileSystems."/mnt/nas" = {
device = "192.168.1.226:/volume1/docker";
fsType = "nfs";
options = [ "noatime" "vers=4" "rsize=8192" "wsize=8192" ];
};
}

View file

@ -0,0 +1,10 @@
{ config, pkgs, modulesPath, lib, ... }:
{
imports = [
../../templates/base.nix
./networking.nix
./traefik.nix
./promtail.nix
];
}

View file

@ -0,0 +1,18 @@
{ config, lib, pkgs, ... }: {
networking = {
hostName = "traefik";
interfaces.eth0 = {
ipv4.addresses = [{
address = "192.168.1.171";
prefixLength = 24;
}];
};
firewall.allowedTCPPorts = [ 80 443 8080 8082 ];
defaultGateway = {
address = "192.168.1.1";
interface = "eth0";
};
};
}

View file

@ -0,0 +1,27 @@
{ config, lib, pkgs, ... }:
{
# This ensures the directory exists at boot, owned by traefik (writer) and readable by promtail.
systemd.tmpfiles.rules = [
"d /var/log/traefik 0755 traefik promtail -"
];
services.promtail.configuration.scrape_configs = lib.mkAfter [
{
job_name = "traefik";
static_configs = [
{
targets = [ "localhost" ];
labels = {
job = "traefik";
host = config.networking.hostName;
env = "proxmox";
instance = "${config.networking.hostName}.local"; # prometheus scrape target
__path__ = "/var/log/traefik/*.log";
};
}
];
}
];
}

View file

@ -0,0 +1,158 @@
{ config, lib, pkgs, ... }: {
# Traefik reverse proxy setup
services.traefik = {
enable = true;
staticConfigOptions = {
entryPoints = {
web = {
address = ":80";
asDefault = true;
http.redirections.entrypoint = {
to = "websecure";
scheme = "https";
};
};
websecure = {
address = ":443";
asDefault = true;
http.tls.certResolver = "letsencrypt";
};
metrics = {
address = ":8082";
};
};
api.dashboard = true;
api.insecure = true;
# Enable Let's Encrypt
certificatesResolvers = {
letsencrypt = {
acme = {
email = "david.mikael@proton.me"; # Replace with your email
storage = "/var/lib/traefik/acme.json"; # Location to store ACME certificates
httpChallenge = {
entryPoint = "web"; # Uses HTTP challenge (can also use DNS)
};
# Uncomment the following for staging (testing) environment
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory";
};
};
};
# Enable Prometheus metrics
metrics = {
prometheus = {
entryPoint = "metrics";
};
};
log = {
level = "DEBUG";
filePath = "/var/log/traefik/traefik.log";
};
accessLog = {
format = "json";
filePath = "/var/log/traefik/access.log";
};
# Enable access logs (you can customize the log format)
# accessLog = {
# filePath = "/var/log/traefik/access.log"; # Log to a file
# format = "common"; # You can adjust this to `json` or `common`
# };
# tracing = {
# enabled = true;
# provider = "jaeger"; # or zipkin, or other
# jaeger = {
# apiURL = "http://localhost:5775"; # Replace with your Jaeger instance URL
# };
# };
};
dynamicConfigOptions = {
# Add IP whitelisting middleware to restrict access to internal network only
http.middlewares = {
internal-whitelist = {
ipWhiteList = {
sourceRange = ["192.168.1.0/24"]; # Adjust to your internal network range
# Alternatively use `127.0.0.1/32` for localhost access
};
};
};
# Route to Proxmox UI
http.routers.proxmox = {
rule = "Host(`proxmox.procopius.dk`)";
service = "proxmox";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Route to Traefik Dashboard
http.routers.traefik = {
rule = "Host(`traefik.procopius.dk`)";
service = "traefik";
entryPoints = [ "web" "websecure" ];
middlewares = ["internal-whitelist"];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
http.routers.forgejo = {
rule = "Host(`git.procopius.dk`)";
service = "forgejo";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Route to Traefik Dashboard
http.routers.catchAll = {
# rule = "Host(`jellyfin.procopius.dk`)";
rule = "HostRegexp(`.+`)";
# rule = "HostRegexp(`{host:.+}`)";
service = "nginx";
entryPoints = [ "web" "websecure" ];
tls = {
certResolver = "letsencrypt"; # Use Let's Encrypt
};
};
# Define the services
http.services.proxmox.loadBalancer.servers = [
{ url = "https://192.168.1.205:8006"; } # Proxmox
];
http.services.proxmox.loadBalancer.serversTransport = "insecureTransport";
http.services.traefik.loadBalancer.servers = [
{ url = "http://traefik.local:8080"; } # Traefik Dashboard
];
http.services.forgejo.loadBalancer.servers = [
{ url = "http://192.168.1.249:3000"; } # forgejo
];
http.services.nginx.loadBalancer.servers = [
{ url = "https://192.168.1.226:4433"; } # nginx
];
http.services.nginx.loadBalancer.serversTransport = "insecureTransport";
http.serversTransports.insecureTransport.insecureSkipVerify = true;
};
};
# Optionally, you can add Docker support if using Docker Compose
virtualisation.docker.enable = true;
}