{ 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} <