feat(atlas): deploy self-hosted honcho

This commit is contained in:
2026-05-20 20:52:15 +02:00
parent 42acdbc98f
commit 59ada8585f
9 changed files with 316 additions and 1 deletions
@@ -2,6 +2,7 @@
imports = [
./baserow.nix
./ghost.nix
./honcho.nix
./kestra.nix
./littlelink.nix
./matomo.nix
@@ -0,0 +1,209 @@
{
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
'';
}
+2 -1
View File
@@ -28,6 +28,7 @@
host kestra kestra 10.89.0.0/24 scram-sha-256
host netbird netbird 10.89.0.0/24 scram-sha-256
host authentik authentik 10.89.0.0/24 scram-sha-256
host honcho honcho 10.89.0.0/24 scram-sha-256
# Deny all other connections
local all all reject
@@ -38,7 +39,7 @@
services.postgresqlBackup = {
enable = true;
startAt = "03:10:00";
databases = ["baserow" "paperless" "kestra" "authentik" "netbird"];
databases = ["baserow" "paperless" "kestra" "authentik" "netbird" "honcho"];
};
networking.firewall = {
extraCommands = ''