feat: herdr + agent network over netbird
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
{lib, ...}: {
|
||||
imports = [
|
||||
./baserow.nix
|
||||
# ./baserow.nix
|
||||
./ghost.nix
|
||||
./honcho.nix
|
||||
./kestra.nix
|
||||
# ./honcho.nix
|
||||
./littlelink.nix
|
||||
./matomo.nix
|
||||
# ./matomo.nix
|
||||
./netbird.nix
|
||||
# ./n8n.nix
|
||||
# ./pangolin.nix
|
||||
./restreamer.nix
|
||||
./slash.nix
|
||||
./slash-nemoti.nix
|
||||
@@ -16,7 +14,7 @@
|
||||
];
|
||||
system.activationScripts.createPodmanNetworkWeb = lib.mkAfter ''
|
||||
if ! /run/current-system/sw/bin/podman network exists web; then
|
||||
/run/current-system/sw/bin/podman network create web --subnet=10.89.0.0/24 --internal
|
||||
/run/current-system/sw/bin/podman network create web --subnet=10.89.0.0/24 --gateway=10.89.0.1
|
||||
fi
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
environmentFiles = [config.age.secrets.ghost-env.path];
|
||||
ports = ["127.0.0.1:3002:2368"];
|
||||
volumes = ["ghost_data:/var/lib/ghost/content"];
|
||||
extraOptions = ["--add-host=mysql:10.89.0.1" "--ip=10.89.0.11" "--network=web"];
|
||||
extraOptions = ["--add-host=mysql:10.89.0.1" "--ip=10.89.0.11" "--network=web" "--dns=8.8.8.8"];
|
||||
};
|
||||
# Traefik configuration specific to ghost
|
||||
services.traefik.dynamicConfigOptions.http = {
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
serviceName = "honcho";
|
||||
image = "ghcr.io/plastic-labs/honcho:v3.0.6";
|
||||
|
||||
apiIp = "10.89.0.24";
|
||||
deriverIp = "10.89.0.25";
|
||||
redisIp = "10.89.0.26";
|
||||
|
||||
postgresHost = "10.89.0.1";
|
||||
postgresPort = config.m3ta.ports.get "postgres";
|
||||
honchoPort = config.m3ta.ports.get "honcho";
|
||||
|
||||
# m3-atlas Netbird mesh address, discovered from `netbird status -d`.
|
||||
# Binding the host port here keeps self-hosted Honcho off public interfaces.
|
||||
netbirdBindAddress = "100.81.142.56";
|
||||
netbirdRange = "100.64.0.0/16";
|
||||
|
||||
dbName = "honcho";
|
||||
dbUser = "honcho";
|
||||
redisName = "${serviceName}-redis";
|
||||
runtimeDirectory = "/run/${serviceName}";
|
||||
runtimeEnvFile = "${runtimeDirectory}/env";
|
||||
|
||||
# Keep auth disabled for the first deployment because Honcho clients need
|
||||
# generated JWTs. The JWT secret is still provisioned so enabling auth later is
|
||||
# a one-line change here plus client token generation.
|
||||
authUseAuth = false;
|
||||
|
||||
sharedEnvironment = {
|
||||
CACHE_ENABLED = "true";
|
||||
CACHE_URL = "redis://${redisName}:6379/0?suppress=true";
|
||||
LOG_LEVEL = "INFO";
|
||||
TELEMETRY_ENABLED = "false";
|
||||
VECTOR_STORE_MIGRATED = "false";
|
||||
VECTOR_STORE_TYPE = "pgvector";
|
||||
AUTH_USE_AUTH = lib.boolToString authUseAuth;
|
||||
};
|
||||
|
||||
sharedEnvironmentFiles = [
|
||||
runtimeEnvFile
|
||||
config.age.secrets."${serviceName}-selfhost-env".path
|
||||
];
|
||||
|
||||
webNetwork = ip: [
|
||||
"--add-host=postgres:${postgresHost}"
|
||||
"--network=web:ip=${ip}"
|
||||
];
|
||||
|
||||
# The shared web network is intentionally internal. API and deriver also join
|
||||
# this egress-only network so LLM provider calls can leave the host without
|
||||
# exposing any extra inbound ports.
|
||||
networksWithEgress = ip:
|
||||
(webNetwork ip)
|
||||
++ [
|
||||
"--network=${serviceName}-egress"
|
||||
];
|
||||
|
||||
apiHealthCmd = ''/app/.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2).read()"'';
|
||||
in {
|
||||
system.activationScripts.createPodmanNetworkHonchoEgress = lib.mkAfter ''
|
||||
if ! /run/current-system/sw/bin/podman network exists ${serviceName}-egress; then
|
||||
/run/current-system/sw/bin/podman network create ${serviceName}-egress
|
||||
fi
|
||||
'';
|
||||
|
||||
virtualisation.oci-containers.containers = {
|
||||
"${serviceName}-redis" = {
|
||||
image = "docker.io/redis:8.2";
|
||||
autoStart = true;
|
||||
volumes = ["${serviceName}_redis_data:/data"];
|
||||
extraOptions =
|
||||
(webNetwork redisIp)
|
||||
++ [
|
||||
"--health-cmd=redis-cli ping"
|
||||
"--health-interval=5s"
|
||||
"--health-timeout=5s"
|
||||
"--health-retries=5"
|
||||
];
|
||||
};
|
||||
|
||||
"${serviceName}-api" = {
|
||||
inherit image;
|
||||
autoStart = true;
|
||||
entrypoint = "sh";
|
||||
cmd = ["docker/entrypoint.sh"];
|
||||
environment = sharedEnvironment;
|
||||
environmentFiles = sharedEnvironmentFiles;
|
||||
ports = ["${netbirdBindAddress}:${toString honchoPort}:8000"];
|
||||
dependsOn = [redisName];
|
||||
extraOptions =
|
||||
(networksWithEgress apiIp)
|
||||
++ [
|
||||
"--health-cmd=${apiHealthCmd}"
|
||||
"--health-interval=5s"
|
||||
"--health-timeout=5s"
|
||||
"--health-retries=5"
|
||||
"--health-start-period=10s"
|
||||
];
|
||||
};
|
||||
|
||||
"${serviceName}-deriver" = {
|
||||
inherit image;
|
||||
autoStart = true;
|
||||
entrypoint = "/app/.venv/bin/python";
|
||||
cmd = ["-m" "src.deriver"];
|
||||
environment = sharedEnvironment;
|
||||
environmentFiles = sharedEnvironmentFiles;
|
||||
dependsOn = ["${serviceName}-api" redisName];
|
||||
extraOptions = networksWithEgress deriverIp;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = {
|
||||
"${serviceName}-postgres-bootstrap" = {
|
||||
description = "Bootstrap Honcho PostgreSQL role, database, password, and pgvector";
|
||||
after = ["postgresql.service" "agenix.service"];
|
||||
requires = ["postgresql.service" "agenix.service"];
|
||||
before = ["${serviceName}-env.service" "podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
||||
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
||||
path = [
|
||||
config.services.postgresql.package
|
||||
pkgs.coreutils
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "postgres";
|
||||
Group = "postgres";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
test -s ${config.age.secrets."${serviceName}-selfhost-db-password".path}
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --dbname=postgres <<'SQL'
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE ROLE ${dbUser} LOGIN;
|
||||
EXCEPTION WHEN duplicate_object THEN
|
||||
NULL;
|
||||
END
|
||||
$$;
|
||||
|
||||
SELECT 'CREATE DATABASE ${dbName} OWNER ${dbUser}'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${dbName}')\gexec
|
||||
|
||||
ALTER DATABASE ${dbName} OWNER TO ${dbUser};
|
||||
\set honcho_password `cat ${config.age.secrets."${serviceName}-selfhost-db-password".path}`
|
||||
ALTER ROLE ${dbUser} WITH LOGIN PASSWORD :'honcho_password';
|
||||
SQL
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --dbname=${dbName} <<'SQL'
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
GRANT ALL PRIVILEGES ON DATABASE ${dbName} TO ${dbUser};
|
||||
SQL
|
||||
'';
|
||||
};
|
||||
|
||||
"${serviceName}-env" = {
|
||||
description = "Generate Honcho runtime environment file with agenix secrets";
|
||||
after = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
requires = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
before = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
||||
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
||||
path = [
|
||||
pkgs.coreutils
|
||||
pkgs.python3
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
install -d -m 0750 ${runtimeDirectory}
|
||||
|
||||
db_password_encoded=$(
|
||||
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=""))' \
|
||||
< ${config.age.secrets."${serviceName}-selfhost-db-password".path}
|
||||
)
|
||||
jwt_secret=$(tr -d '\r\n' < ${config.age.secrets."${serviceName}-selfhost-jwt-secret".path})
|
||||
|
||||
umask 077
|
||||
cat > ${runtimeEnvFile} <<ENV
|
||||
DB_CONNECTION_URI=postgresql+psycopg://${dbUser}:$db_password_encoded@postgres:${toString postgresPort}/${dbName}
|
||||
AUTH_JWT_SECRET=$jwt_secret
|
||||
ENV
|
||||
'';
|
||||
};
|
||||
|
||||
"podman-${serviceName}-api" = {
|
||||
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
};
|
||||
|
||||
"podman-${serviceName}-deriver" = {
|
||||
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.extraCommands = ''
|
||||
# Self-hosted Honcho API: only Netbird mesh peers may reach ${netbirdBindAddress}:${toString honchoPort}.
|
||||
ip46tables -A nixos-fw -p tcp --dport ${toString honchoPort} -s ${netbirdRange} -j nixos-fw-accept
|
||||
'';
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{config, ...}: {
|
||||
virtualisation.oci-containers.containers."kestra" = {
|
||||
image = "docker.io/kestra/kestra:latest";
|
||||
environmentFiles = [config.age.secrets.kestra-env.path];
|
||||
cmd = ["server" "standalone" "--config" "/etc/config/application.yaml"];
|
||||
ports = ["127.0.0.1:3018:8080"];
|
||||
user = "root";
|
||||
volumes = [
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
"${config.age.secrets.kestra-config.path}:/etc/config/application.yaml"
|
||||
"kestra_data:/app/storage"
|
||||
"/tmp/kestra-wd:/tmp/kestra-wd"
|
||||
];
|
||||
extraOptions = ["--add-host=postgres:10.89.0.1" "--ip=10.89.0.18" "--network=web"];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /tmp/kestra-wd 0750 1000 1000 - -"
|
||||
];
|
||||
|
||||
# Traefik configuration specific to littlelink
|
||||
services.traefik.dynamicConfigOptions.http = {
|
||||
services.kestra.loadBalancer.servers = [{url = "http://localhost:3018/";}];
|
||||
|
||||
routers.kestra = {
|
||||
rule = "Host(`k.m3ta.dev`)";
|
||||
tls = {certResolver = "godaddy";};
|
||||
service = "kestra";
|
||||
entrypoints = "websecure";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
@@ -121,7 +122,7 @@ in {
|
||||
|
||||
virtualisation.oci-containers.containers = {
|
||||
"${serviceName}-dashboard" = {
|
||||
image = "netbirdio/dashboard:latest";
|
||||
image = "docker.io/netbirdio/dashboard:latest";
|
||||
autoStart = true;
|
||||
environmentFiles = [config.age.secrets."${serviceName}-dashboard-env".path];
|
||||
extraOptions = [
|
||||
@@ -131,7 +132,7 @@ in {
|
||||
};
|
||||
|
||||
"${serviceName}-server" = {
|
||||
image = "netbirdio/netbird-server:latest";
|
||||
image = "docker.io/netbirdio/netbird-server:latest";
|
||||
autoStart = true;
|
||||
ports = ["${toString stunPort}:${toString stunPort}/udp"];
|
||||
environmentFiles = [config.age.secrets."${serviceName}-server-env".path];
|
||||
@@ -147,12 +148,15 @@ in {
|
||||
};
|
||||
|
||||
"${serviceName}-proxy" = {
|
||||
image = "netbirdio/reverse-proxy:latest";
|
||||
image = "docker.io/netbirdio/reverse-proxy:latest";
|
||||
autoStart = true;
|
||||
ports = ["${toString wireguardPort}:${toString wireguardPort}/udp"];
|
||||
volumes = [
|
||||
"${serviceName}_proxy_certs:/certs"
|
||||
];
|
||||
environment = {
|
||||
NB_PROXY_PRIVATE = "true";
|
||||
};
|
||||
environmentFiles = [config.age.secrets."${serviceName}-proxy-env".path];
|
||||
cmd = [
|
||||
"--domain=${proxyDomain}"
|
||||
|
||||
Reference in New Issue
Block a user