Add Matrix/Hermes/Omniroute to jeannie pipeline (SOPS-encrypted secrets)
Codify the full Matrix homeserver stack, OmniRoute LLM gateway, and
Hermes agent into the Jeannie deploy pipeline so the repo reproduces
the running homelab from scratch.
New files:
infra/matrix-server/ — Synapse + Postgres + Element docker-compose,
homeserver.yaml.tpl, log_config, element config, bootstrap.sh
infra/omniroute/ — diegosouzapw/omniroute:latest on port 20128
infra/hermes.secret.yaml — SOPS-encrypted bot password + API key
infra/matrix-server/matrix.secret.yaml — SOPS-encrypted Synapse secrets
.sops.yaml — age recipient + encrypted_regex: '.*'
jeannie additions:
deploy_matrix() — renders homeserver.yaml from template, creates
signing key via 'generate' on first boot, delegates to bootstrap.sh
deploy_omniroute() — docker compose up with env-file support
deploy_hermes() — git clone + venv + 'hermes gateway install',
writes ~/.hermes/.env (mode 600), registers non-admin bot via
shared-secret CLI, idempotent
sops_load_secrets() — decrypts *.secret.yaml via age key at deploy
time; fails loudly if the age key is missing
ensure_sops_age_tools() — installs sops from getsops GitHub release
when apt has no sops package (Debian trixie)
DAG: hermes-deploy depends on matrix-deploy + omniroute-deploy.
Root cause: deploy_hermes previously used wrong entrypoint (python
module instead of ./hermes launcher); generate was called with
--generate-config (invalid subcommand); .sops.yaml encrypted_regex
'^password$' left prefixed keys as plaintext — all fixed in this
commit.
This commit is contained in:
parent
d6e33250f4
commit
fe661f9b24
|
|
@ -11,6 +11,10 @@ bootstrap/host/inventory/
|
|||
*.tar.gz
|
||||
*.zip
|
||||
infra/gitea/data/
|
||||
infra/matrix-server/synapse-data/
|
||||
infra/matrix-server/postgres-data/
|
||||
infra/matrix-server/.env
|
||||
infra/omniroute/.env
|
||||
infra/youtube-backup/.env
|
||||
infra/youtube-backup/cookies.txt
|
||||
infra/youtube-backup/archive.txt
|
||||
|
|
@ -21,6 +25,8 @@ infra/youtube-backup/logs/
|
|||
*.decrypted.yaml
|
||||
*.plain.yaml
|
||||
*.secret.local.yaml
|
||||
*.secret.yaml.plain
|
||||
*.secret.plain
|
||||
.age-key.txt
|
||||
sops-age.key
|
||||
.sops.yaml.tmp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# Copy this file to .sops.yaml after replacing the age recipient with the
|
||||
# public key generated on the Debian homelab server.
|
||||
creation_rules:
|
||||
- path_regex: '(^|/).*\.(secret|enc)\.(ya?ml|json)$'
|
||||
encrypted_regex: '.*'
|
||||
age: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||
28
homelab.yml
28
homelab.yml
|
|
@ -12,6 +12,8 @@ domain:
|
|||
subdomains:
|
||||
- demos.lab2025.duckdns.org
|
||||
- heimdall.lab2025.duckdns.org
|
||||
- matrix.lab2025.duckdns.org
|
||||
- element.lab2025.duckdns.org
|
||||
- grafana.lab2025.duckdns.org
|
||||
- prometheus.lab2025.duckdns.org
|
||||
- alertmanager.lab2025.duckdns.org
|
||||
|
|
@ -116,6 +118,32 @@ services:
|
|||
suwayomi_port: 4567
|
||||
maintainerr_port: 6246
|
||||
flaresolverr_port: 8191
|
||||
matrix:
|
||||
host: debian
|
||||
install_dir: /home/jv/matrix-server
|
||||
synapse_port: 8008
|
||||
element_port: 8081
|
||||
server_name: matrix.lab2025.duckdns.org
|
||||
synapse_image: matrixdotorg/synapse:latest
|
||||
element_image: vectorim/element-web:latest
|
||||
postgres_image: postgres:16-alpine
|
||||
postgres_db: synapse
|
||||
postgres_user: synapse
|
||||
bot_user: hermes
|
||||
allowed_users:
|
||||
- "@jv:matrix.lab2025.duckdns.org"
|
||||
omniroute:
|
||||
host: debian
|
||||
install_dir: /home/jv/homelab-omniroute
|
||||
port: 20128
|
||||
image: diegosouzapw/omniroute:latest
|
||||
container_name: omniroute
|
||||
hermes:
|
||||
host: debian
|
||||
home: /home/jv/.hermes
|
||||
install_dir: /home/jv/.hermes/hermes-agent
|
||||
uv_bin: /home/jv/.hermes/bin/uv
|
||||
bot_user: hermes
|
||||
traefik:
|
||||
host: kubernetes
|
||||
load_balancer_ip: 192.168.100.240
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Hermes agent secrets - SOPS-encrypted. Do not commit plaintext.
|
||||
# Decrypted at deploy time by jeannie deploy_hermes.
|
||||
MATRIX_BOT_PASSWORD: ENC[AES256_GCM,data:+21aq+VzaYZLizC2p8oOEf1614M=,iv:qKpVrpE50ByIOXW32RnYy7zwPIbtt3UybAn7Pb8bY5Y=,tag:utUx46AyxK+jk3c7Wdnygw==,type:str]
|
||||
HERMES_OMNIROUTE_API_KEY: ""
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBySEFyREdGOS9HOGtkeVRF
|
||||
M0w4aUcxK2F5NTdSR29CSDY2SHZVbWlXZ0hzCmZOV3VXa3J5eEJLY2pTa2RHTGhV
|
||||
eWpXdXVmVWFiLzZpMmlyUmJIQ3hUdjAKLS0tIG8ybWVNTXNZTHM4dnJ4Y2Z5VXAr
|
||||
TXVIVXVWenlBc0lEZ0VSWmdFQmpPSGsK2FtwcyBFqVzCphxGVV8xg6QZea3SC6wt
|
||||
gX1rp3icFegLisy/m4cUJkfku9zTHoHjsqPjbl+xzs3iaReHSVQRlA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||
encrypted_regex: .*
|
||||
lastmodified: "2026-09-16T16:32:24Z"
|
||||
mac: ENC[AES256_GCM,data:0wKCLSIIDh4+/IHY/UN2KeYEdHBn0ZhCVRcIa3TV4P8CtRbcItQbhj5e95q5FPWJRBASXIxj/WwOkyd5JFUmxH5XhaeKCHvxbgNMClPb8Ic5KSnNMRgSZtrCgKlACySFEDL5aD9mxqkd3f5bVd+JY04HGHkSJUWRfdGnNdwGfnw=,iv:BPyQLTty1qjSldlxeUEC/Uo80H1R8Vlfey9ie26JZ30=,tag:oYZ2TVwKUgf+oPl8nvqeLA==,type:str]
|
||||
version: 3.13.3
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Matrix stack — non-secret defaults.
|
||||
# Copy to a sibling .env (gitignored) or export these as env vars when running
|
||||
# deploy_matrix. NEVER put real secrets in this committed example.
|
||||
MATRIX_SERVER_NAME=matrix.lab2025.duckdns.org
|
||||
MATRIX_SYNAPSE_PORT=8008
|
||||
MATRIX_ELEMENT_PORT=8081
|
||||
MATRIX_SYNAPSE_CONTAINER_NAME=matrix-synapse
|
||||
MATRIX_POSTGRES_CONTAINER_NAME=matrix-postgres
|
||||
MATRIX_ELEMENT_CONTAINER_NAME=matrix-element
|
||||
MATRIX_POSTGRES_DB=synapse
|
||||
MATRIX_POSTGRES_USER=synapse
|
||||
|
||||
# SECRETS (must be supplied at deploy time, not committed):
|
||||
# MATRIX_POSTGRES_PASSWORD postgres/synapse db password
|
||||
# MATRIX_REGISTRATION_SHARED_SECRET shared registration secret for register_new_matrix_user
|
||||
# MATRIX_MACAROON_SECRET_KEY macaroon secret
|
||||
# MATRIX_FORM_SECRET form secret
|
||||
# MATRIX_BOT_PASSWORD hermes bot account password
|
||||
# (The synapse signing key is generated by Synapse on first boot — not a secret you set.)
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy/update the self-hosted Matrix stack (Synapse + Postgres + Element).
|
||||
# Idempotent: renders homeserver.yaml from the committed template, ensures
|
||||
# correct data-dir ownership, and starts containers.
|
||||
#
|
||||
# Secrets are read from an install-dir .env (written by jeannie deploy_matrix),
|
||||
# NOT from the repo. The signing key is created by Synapse on first boot.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${MATRIX_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
echo "Missing secret env file ${ENV_FILE}. Run jeannie deploy-matrix first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${ENV_FILE}"
|
||||
set +a
|
||||
|
||||
MATRIX_SERVER_NAME="${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||
MATRIX_POSTGRES_DB="${MATRIX_POSTGRES_DB:-synapse}"
|
||||
MATRIX_POSTGRES_USER="${MATRIX_POSTGRES_USER:-synapse}"
|
||||
SYNAPSE_UID="${MATRIX_SYNAPSE_UID:-991}"
|
||||
POSTGRES_UID="${MATRIX_POSTGRES_UID:-70}"
|
||||
|
||||
render_homeserver() {
|
||||
local tpl="${SCRIPT_DIR}/homeserver.yaml.tpl"
|
||||
local out="${SCRIPT_DIR}/synapse-data/homeserver.yaml"
|
||||
for var in MATRIX_SERVER_NAME MATRIX_POSTGRES_DB MATRIX_POSTGRES_USER \
|
||||
MATRIX_POSTGRES_PASSWORD MATRIX_REGISTRATION_SHARED_SECRET \
|
||||
MATRIX_MACAROON_SECRET_KEY MATRIX_FORM_SECRET; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
echo "Missing required secret/env ${var}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# shellcheck disable=SC2016
|
||||
envsubst \
|
||||
'${MATRIX_SERVER_NAME} ${MATRIX_POSTGRES_DB} ${MATRIX_POSTGRES_USER} ${MATRIX_POSTGRES_PASSWORD} ${MATRIX_REGISTRATION_SHARED_SECRET} ${MATRIX_MACAROON_SECRET_KEY} ${MATRIX_FORM_SECRET}' \
|
||||
<"${tpl}" >"${out}"
|
||||
cp "${SCRIPT_DIR}/log_config.yaml" "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.log.config"
|
||||
}
|
||||
|
||||
sudo mkdir -p "${SCRIPT_DIR}/synapse-data" "${SCRIPT_DIR}/postgres-data"
|
||||
|
||||
# First boot: generate signing key + log config (the files a hand-written
|
||||
# homeserver.yaml would not provide). Without this Synapse fails to boot on a
|
||||
# missing signing key. Only touches the data dir (not postgres).
|
||||
if [[ ! -f "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.signing.key" ]]; then
|
||||
echo "First boot: generating Synapse signing key + log config..."
|
||||
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" \
|
||||
run --rm --no-deps synapse generate --config-path /data/homeserver.yaml
|
||||
fi
|
||||
|
||||
render_homeserver
|
||||
|
||||
# Ownership foot-gun: wrong uid prevents boot (synapse 991, postgres 70).
|
||||
sudo chown -R "${SYNAPSE_UID}:${SYNAPSE_UID}" "${SCRIPT_DIR}/synapse-data"
|
||||
sudo chown -R "${POSTGRES_UID}:${POSTGRES_UID}" "${SCRIPT_DIR}/postgres-data"
|
||||
|
||||
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||
|
||||
echo "Matrix stack up:"
|
||||
echo " Synapse http://127.0.0.1:${MATRIX_SYNAPSE_PORT:-8008}"
|
||||
echo " Element http://127.0.0.1:${MATRIX_ELEMENT_PORT:-8081}"
|
||||
echo " server_name ${MATRIX_SERVER_NAME}"
|
||||
|
||||
cat <<'EOF'
|
||||
Next: register users (hermes bot, and any humans) via the shared-secret CLI:
|
||||
docker exec -u 991 matrix-synapse register_new_matrix_user -c /data/homeserver.yaml -u NAME -p PASS [--no-admin]
|
||||
EOF
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
services:
|
||||
synapse:
|
||||
image: matrixdotorg/synapse:latest
|
||||
container_name: ${MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
SYNAPSE_SERVER_NAME: ${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||
SYNAPSE_REPORT_STATS: "no"
|
||||
# federation disabled (user choice): client-only listener on 8008
|
||||
ports:
|
||||
- "${MATRIX_SYNAPSE_PORT:-8008}:8008"
|
||||
volumes:
|
||||
- ./synapse-data:/data
|
||||
networks:
|
||||
- matrix
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: ${MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
||||
POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
||||
POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- matrix
|
||||
|
||||
element:
|
||||
image: vectorim/element-web:latest
|
||||
container_name: ${MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- synapse
|
||||
ports:
|
||||
- "${MATRIX_ELEMENT_PORT:-8081}:80"
|
||||
volumes:
|
||||
# Must mount the FILE, not a directory (element-web reads /app/config.json)
|
||||
- ./element-config/config.json:/app/config.json
|
||||
networks:
|
||||
- matrix
|
||||
|
||||
networks:
|
||||
matrix:
|
||||
driver: bridge
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"default_server_name": "matrix.lab2025.duckdns.org",
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://matrix.lab2025.duckdns.org",
|
||||
"server_name": "matrix.lab2025.duckdns.org"
|
||||
}
|
||||
},
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": true,
|
||||
"disable_login_language_selector": false,
|
||||
"disable_3pid_login": false,
|
||||
"force_verification": false,
|
||||
"brand": "Matrix",
|
||||
"default_federate": false,
|
||||
"show_labs_settings": true,
|
||||
"default_theme": "dark",
|
||||
"room_directory": {
|
||||
"servers": ["matrix.lab2025.duckdns.org"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Configuration file for Synapse.
|
||||
# This is generated by Jeannie from infra/matrix-server/homeserver.yaml.tpl.
|
||||
# Secrets (registration_shared_secret, macaroon_secret_key, form_secret, postgres
|
||||
# password, signing key) are injected by the deploy_matrix() bootstrap — never store
|
||||
# them in this template.
|
||||
|
||||
server_name: "${MATRIX_SERVER_NAME}"
|
||||
pid_file: /data/homeserver.pid
|
||||
listeners:
|
||||
- port: 8008
|
||||
resources:
|
||||
- compress: false
|
||||
names:
|
||||
- client
|
||||
- federation
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
database:
|
||||
name: psycopg2
|
||||
args:
|
||||
user: "${MATRIX_POSTGRES_USER}"
|
||||
password: "${MATRIX_POSTGRES_PASSWORD}"
|
||||
database: "${MATRIX_POSTGRES_DB}"
|
||||
host: postgres
|
||||
port: 5432
|
||||
cp_min: 5
|
||||
cp_max: 10
|
||||
log_config: "/data/${MATRIX_SERVER_NAME}.log.config"
|
||||
media_store_path: /data/media_store
|
||||
registration_shared_secret: "${MATRIX_REGISTRATION_SHARED_SECRET}"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "${MATRIX_MACAROON_SECRET_KEY}"
|
||||
form_secret: "${MATRIX_FORM_SECRET}"
|
||||
signing_key_path: "/data/${MATRIX_SERVER_NAME}.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
|
||||
# vim:ft=yaml
|
||||
user_directory:
|
||||
enabled: true
|
||||
search_all_users: true
|
||||
prefer_local_users: true
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
_placeholder:
|
||||
level: "INFO"
|
||||
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
|
||||
disable_existing_loggers: false
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Matrix homeserver secrets - SOPS-encrypted. Do not commit plaintext.
|
||||
# Decrypted at deploy time by jeannie deploy_matrix.
|
||||
MATRIX_POSTGRES_PASSWORD: ENC[AES256_GCM,data:HYti676SrsKpr5UuYX7wrg==,iv:pggYfnjqZAGulrCNVBJ/vcICcsn9hic150hmRb1D5uE=,tag:rs4jSLwMZeoaUgjqCYIkTA==,type:str]
|
||||
MATRIX_REGISTRATION_SHARED_SECRET: ENC[AES256_GCM,data:+OChmnoaVrWhLCQHPe3sSbfuEs2H6nJuPhFPelRM+hOPCADXYZPiUPxH3RJfLY9niCg=,iv:b2wVg4Xg/qTTT7ghyNJJNOfnafESE7EZAZoVUKFJxJc=,tag:ewdDXpFoeb2/+HlLzYJySw==,type:str]
|
||||
MATRIX_MACAROON_SECRET_KEY: ENC[AES256_GCM,data:0IY3J7ijJhYiqd10A2D/xo0Utok0llFFjUOOqY57QjnCA8tgeJOH4lNuiPTScKPMnnI=,iv:PkqyW3RaQpA8+TQtKI6ibo11CJVUiod/OO2QLBa4Lrs=,tag:Z8j5xqlcysgkrmbmiMVNEA==,type:str]
|
||||
MATRIX_FORM_SECRET: ENC[AES256_GCM,data:WfaToWF7NO5VgpeJcuONlhHK+GopNjPnVAjFLorrronMNUzTKIICr2xjrVBq3wvPuf4=,iv:Qgf7fdO9boSV136LL7/IxNcYz33gNP7YbIQohJFEAAI=,tag:FXtUqNj/JtsrWLx/ntypag==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1THJOdkhOb21UeFk2NHdx
|
||||
bUNSMTZUVDlOeUh1T1N4WnA5akpVVWVEVFRNCk02RG9kNENCVHJUSVVXNkE0ZUFh
|
||||
ek8vU2NzRUwvMm1EMDNwdnc1YjhzZ2cKLS0tIFpmZU5Pb0FJRTl1ZktVVXA1RTAw
|
||||
MjBHcmZQTFJqZUdHWVgxdURhQk5wRW8Ka0TXyBnr7Puq+dYVizMK+HIAEncAD2bl
|
||||
5JGP5TU1Dp0I9njHJYmDAicx5iaMhfwQe4iL9/73FOy1vnfYPlv9gQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||
encrypted_regex: .*
|
||||
lastmodified: "2026-09-16T16:32:24Z"
|
||||
mac: ENC[AES256_GCM,data:lyhzE48mseEWnf2+UoYy4lkBPFtvD3PuB4BR/LbTEkz4xrC0KYHqlVma5dEhLoOTs+k5rhFy3ko3lZoZ396kkrOMftd9zyYTK7Nk/ZxPRrg3gPqr/Dd4bTCGDstznNMgbsCgzklhB35IYWgP/PL5ccZiP/OnqWXoTMPRYy/EPC8=,iv:FVhAK3zGSROkI0iyhRo1+PMlXLbZQ+74A4pnlSR6Y80=,tag:x3/mNU6l8PqPBb0cRbPAuQ==,type:str]
|
||||
version: 3.13.3
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# OmniRoute — non-secret defaults.
|
||||
# Copy to a sibling .env or export when running deploy_omniroute.
|
||||
# Secrets (JWT_SECRET, API_KEY_SECRET, STORAGE_ENCRYPTION_*) are auto-generated
|
||||
# by OmniRoute into its data volume (server.env) on first boot — not stored here.
|
||||
OMNIROUTE_IMAGE=diegosouzapw/omniroute:latest
|
||||
OMNIROUTE_CONTAINER_NAME=omniroute
|
||||
OMNIROUTE_PORT=20128
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy/update the OmniRoute LLM-routing gateway that Hermes uses as its model
|
||||
# provider (http://localhost:20128/v1). Idempotent. Secrets live in the data
|
||||
# volume (server.env) generated by OmniRoute on first boot.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${OMNIROUTE_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
echo "Missing env file ${ENV_FILE}. Run jeannie deploy-omniroute first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${ENV_FILE}"
|
||||
set +a
|
||||
|
||||
sudo mkdir -p "${SCRIPT_DIR}"
|
||||
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||
|
||||
echo "OmniRoute up at http://127.0.0.1:${OMNIROUTE_PORT:-20128}"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
omniroute:
|
||||
image: ${OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||
container_name: ${OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||
restart: unless-stopped
|
||||
stop_timeout: 40
|
||||
ports:
|
||||
- "${OMNIROUTE_PORT:-20128}:20128"
|
||||
volumes:
|
||||
- omniroute-data:/app/data
|
||||
|
||||
volumes:
|
||||
omniroute-data:
|
||||
336
jeannie
336
jeannie
|
|
@ -4117,6 +4117,233 @@ ENV_EOT
|
|||
echo "Heimdall is reachable at http://127.0.0.1:${http_port}/ and ${public_url%/}/."
|
||||
}
|
||||
|
||||
deploy_matrix() {
|
||||
local mode="${LAB_MATRIX_DEPLOY:-true}"
|
||||
local install_dir="${LAB_MATRIX_INSTALL_DIR:-/home/jv/matrix-server}"
|
||||
local image=""
|
||||
local container_name=""
|
||||
local http_port=""
|
||||
local ssh_port=""
|
||||
local domain=""
|
||||
local install_docker="false"
|
||||
local source_dir="${REPO_ROOT}/infra/matrix-server"
|
||||
local value_name
|
||||
local value
|
||||
|
||||
require_debian_server "deploy-matrix"
|
||||
|
||||
if disabled_value "${mode}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for value_name in install_dir; do
|
||||
value="${!value_name}"
|
||||
if [[ "${value}" == *$'\n'* ]]; then
|
||||
echo "${value_name} cannot contain a newline." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/homeserver.yaml.tpl" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||
echo "Missing Matrix source files in ${source_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying Matrix stack on the Debian host at ${install_dir}..."
|
||||
|
||||
ensure_local_docker_compose "MATRIX" "${install_docker}"
|
||||
repair_local_docker_iptables
|
||||
|
||||
sudo mkdir -p "${install_dir}"
|
||||
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||
sudo cp "${source_dir}/homeserver.yaml.tpl" "${install_dir}/homeserver.yaml.tpl"
|
||||
sudo cp "${source_dir}/log_config.yaml" "${install_dir}/log_config.yaml"
|
||||
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json" 2>/dev/null || {
|
||||
sudo mkdir -p "${install_dir}/element-config"
|
||||
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json"
|
||||
}
|
||||
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||
|
||||
# Secrets come from the SOPS-encrypted repo file, decrypted at deploy time.
|
||||
# (Env vars override the file, for manual/ops overrides.)
|
||||
if ! sops_load_secrets "${REPO_ROOT}/infra/matrix-server/matrix.secret.yaml"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${MATRIX_POSTGRES_PASSWORD:-}" || -z "${MATRIX_REGISTRATION_SHARED_SECRET:-}" ]]; then
|
||||
echo "MATRIX_POSTGRES_PASSWORD and MATRIX_REGISTRATION_SHARED_SECRET missing after decrypt." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local matrix_server_name="${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||
local matrix_synapse_port="${LAB_MATRIX_SYNAPSE_PORT:-8008}"
|
||||
local matrix_element_port="${LAB_MATRIX_ELEMENT_PORT:-8081}"
|
||||
local postgres_db="${LAB_MATRIX_POSTGRES_DB:-synapse}"
|
||||
local postgres_user="${LAB_MATRIX_POSTGRES_USER:-synapse}"
|
||||
|
||||
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||
MATRIX_SERVER_NAME=${matrix_server_name}
|
||||
MATRIX_SYNAPSE_PORT=${matrix_synapse_port}
|
||||
MATRIX_ELEMENT_PORT=${matrix_element_port}
|
||||
MATRIX_SYNAPSE_CONTAINER_NAME=${LAB_MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||
MATRIX_POSTGRES_CONTAINER_NAME=${LAB_MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||
MATRIX_ELEMENT_CONTAINER_NAME=${LAB_MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||
MATRIX_POSTGRES_DB=${postgres_db}
|
||||
MATRIX_POSTGRES_USER=${postgres_user}
|
||||
MATRIX_POSTGRES_PASSWORD=${MATRIX_POSTGRES_PASSWORD}
|
||||
MATRIX_REGISTRATION_SHARED_SECRET=${MATRIX_REGISTRATION_SHARED_SECRET}
|
||||
MATRIX_MACAROON_SECRET_KEY=${MATRIX_MACAROON_SECRET_KEY:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||
MATRIX_FORM_SECRET=${MATRIX_FORM_SECRET:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||
ENV_EOT
|
||||
|
||||
# Pull + render + chown + up. bootstrap.sh is the single source of truth.
|
||||
# sudo the docker compose commands; data dirs are chown'd inside bootstrap.
|
||||
(
|
||||
cd "${install_dir}"
|
||||
sudo docker compose --env-file "${install_dir}/.env" -f "${install_dir}/docker-compose.yml" pull
|
||||
sudo MATRIX_ENV_FILE="${install_dir}/.env" bash "${install_dir}/bootstrap.sh"
|
||||
)
|
||||
|
||||
echo "Matrix stack is reachable at http://127.0.0.1:${matrix_synapse_port}/ and Element at http://127.0.0.1:${matrix_element_port}/."
|
||||
}
|
||||
|
||||
deploy_omniroute() {
|
||||
local mode="${LAB_OMNIROUTE_DEPLOY:-true}"
|
||||
local install_dir="${LAB_OMNIROUTE_INSTALL_DIR:-/home/jv/homelab-omniroute}"
|
||||
local install_docker="false"
|
||||
local source_dir="${REPO_ROOT}/infra/omniroute"
|
||||
|
||||
require_debian_server "deploy-omniroute"
|
||||
|
||||
if disabled_value "${mode}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||
echo "Missing OmniRoute source files in ${source_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying OmniRoute LLM gateway on the Debian host at ${install_dir}..."
|
||||
|
||||
ensure_local_docker_compose "OMNIROUTE" "${install_docker}"
|
||||
repair_local_docker_iptables
|
||||
|
||||
sudo mkdir -p "${install_dir}"
|
||||
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||
|
||||
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||
OMNIROUTE_IMAGE=${LAB_OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||
OMNIROUTE_CONTAINER_NAME=${LAB_OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||
OMNIROUTE_PORT=${LAB_OMNIROUTE_PORT:-20128}
|
||||
ENV_EOT
|
||||
|
||||
(
|
||||
cd "${install_dir}"
|
||||
sudo docker compose pull
|
||||
sudo bash "${install_dir}/bootstrap.sh"
|
||||
)
|
||||
|
||||
echo "OmniRoute is reachable at http://127.0.0.1:${LAB_OMNIROUTE_PORT:-20128}."
|
||||
}
|
||||
|
||||
deploy_hermes() {
|
||||
local mode="${LAB_HERMES_DEPLOY:-true}"
|
||||
local hermes_home="${LAB_HERMES_HOME:-/home/jv/.hermes}"
|
||||
local install_dir="${LAB_HERMES_INSTALL_DIR:-/home/jv/.hermes/hermes-agent}"
|
||||
local uv_bin="${LAB_HERMES_UV_BIN:-/home/jv/.hermes/bin/uv}"
|
||||
local install_docker="${LAB_HERMES_INSTALL_DOCKER:-false}"
|
||||
local value_name
|
||||
local value
|
||||
|
||||
require_debian_server "deploy-hermes"
|
||||
|
||||
if disabled_value "${mode}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for value_name in hermes_home install_dir uv_bin; do
|
||||
value="${!value_name}"
|
||||
if [[ "${value}" == *$'\n'* ]]; then
|
||||
echo "${value_name} cannot contain a newline." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deploying Hermes agent on the Debian host at ${install_dir}..."
|
||||
|
||||
ensure_local_docker_compose "HERMES" "${install_docker}"
|
||||
|
||||
# Install uv if missing.
|
||||
if [[ ! -x "${uv_bin}" ]]; then
|
||||
mkdir -p "$(dirname "${uv_bin}")"
|
||||
echo "Installing uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | bash
|
||||
# uv installs to ~/.local/bin; copy/alias into HERMES bin if that path differs.
|
||||
if command -v uv >/dev/null 2>&1 && [[ ! -x "${uv_bin}" ]]; then
|
||||
cp "$(command -v uv)" "${uv_bin}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Hermes via its official installer (git method, managed venv layout).
|
||||
if [[ ! -x "${hermes_home}/hermes-agent/venv/bin/python" ]]; then
|
||||
echo "Installing Hermes agent..."
|
||||
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
||||
fi
|
||||
|
||||
# Ensure the Matrix platform dependencies (E2EE) are present in the venv.
|
||||
if [[ -x "${uv_bin}" && -f "${install_dir}/pyproject.toml" ]]; then
|
||||
cd "${install_dir}"
|
||||
"${uv_bin}" pip install 'mautrix[encryption]' -p venv/bin/python
|
||||
fi
|
||||
|
||||
# Secret env for the gateway (.env under HERMES_HOME, mode 600, never committed).
|
||||
# Pull the hermes secrets from the SOPS-encrypted repo file; env vars override.
|
||||
if ! sops_load_secrets "${REPO_ROOT}/infra/hermes.secret.yaml"; then
|
||||
echo "WARNING: could not load hermes secrets; MATRIX_PASSWORD may be blank." >&2
|
||||
fi
|
||||
|
||||
local secrets_file="${hermes_home}/.env"
|
||||
sudo install -d -m 0700 -o "$(id -u)" -g "$(id -g)" "${hermes_home}"
|
||||
if [[ -f "${secrets_file}" ]]; then
|
||||
sudo cp "${secrets_file}" "${secrets_file}.bak-pre-jeannie"
|
||||
fi
|
||||
sudo tee "${secrets_file}" >/dev/null <<ENV_EOT
|
||||
OPENAI_BASE_URL=${HERMES_OPENAI_BASE_URL:-http://localhost:${LAB_OMNIROUTE_PORT:-20128}/v1}
|
||||
HERMES_CUSTOM_LOCALHOST_${LAB_OMNIROUTE_PORT:-20128}_API_KEY=${HERMES_OMNIROUTE_API_KEY:-}
|
||||
MATRIX_HOMESERVER=${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||
MATRIX_USER_ID=${HERMES_MATRIX_USER_ID:-@hermes:matrix.lab2025.duckdns.org}
|
||||
MATRIX_PASSWORD=${MATRIX_BOT_PASSWORD:-}
|
||||
MATRIX_ALLOWED_USERS=${HERMES_MATRIX_ALLOWED_USERS:-@jv:matrix.lab2025.duckdns.org}
|
||||
MATRIX_E2EE_MODE=required
|
||||
ENV_EOT
|
||||
sudo chmod 600 "${secrets_file}"
|
||||
|
||||
# Create the Matrix bot account (idempotent) if the homeserver is up.
|
||||
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^matrix-synapse$'; then
|
||||
if [[ -n "${MATRIX_BOT_PASSWORD:-}" ]]; then
|
||||
if ! docker exec matrix-synapse register_new_matrix_user -c /data/homeserver.yaml \
|
||||
-u hermes -p "${MATRIX_BOT_PASSWORD}" --no-admin >/dev/null 2>&1; then
|
||||
echo "Hermes bot account already exists (expected on re-run) or auto-registration skipped." >&2
|
||||
else
|
||||
echo "Hermes Matrix bot account ready."
|
||||
fi
|
||||
else
|
||||
echo "MATRIX_BOT_PASSWORD not set; skipping Hermes bot account registration (needed only on fresh install)." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install/start the gateway systemd user service (Hermes manages the unit).
|
||||
"${hermes_home}/hermes-agent/venv/bin/python" "${hermes_home}/hermes-agent/hermes" gateway install
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now hermes-gateway 2>/dev/null || true
|
||||
|
||||
echo "Hermes agent deployed. Run 'hermes' to configure the model, or check gateway logs at ${hermes_home}/logs/gateway.log."
|
||||
}
|
||||
|
||||
deploy_rpi_services() {
|
||||
local mode="${LAB_RPI_SERVICES_DEPLOY:-true}"
|
||||
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
|
||||
|
|
@ -5609,6 +5836,22 @@ spec = {
|
|||
"title": "Heimdall deploy",
|
||||
"command": [jeannie, "__up-task", "heimdall-deploy"],
|
||||
},
|
||||
{
|
||||
"id": "matrix-deploy",
|
||||
"title": "Matrix deploy",
|
||||
"command": [jeannie, "__up-task", "matrix-deploy"],
|
||||
},
|
||||
{
|
||||
"id": "omniroute-deploy",
|
||||
"title": "OmniRoute deploy",
|
||||
"command": [jeannie, "__up-task", "omniroute-deploy"],
|
||||
},
|
||||
{
|
||||
"id": "hermes-deploy",
|
||||
"title": "Hermes deploy",
|
||||
"needs": ["omniroute-deploy", "matrix-deploy"],
|
||||
"command": [jeannie, "__up-task", "hermes-deploy"],
|
||||
},
|
||||
{
|
||||
"id": "rpi-services",
|
||||
"title": "RPi services",
|
||||
|
|
@ -5656,6 +5899,15 @@ up_task() {
|
|||
heimdall-deploy)
|
||||
deploy_heimdall
|
||||
;;
|
||||
matrix-deploy)
|
||||
deploy_matrix
|
||||
;;
|
||||
omniroute-deploy)
|
||||
deploy_omniroute
|
||||
;;
|
||||
hermes-deploy)
|
||||
deploy_hermes
|
||||
;;
|
||||
rpi-services)
|
||||
deploy_rpi_services
|
||||
;;
|
||||
|
|
@ -7579,9 +7831,6 @@ ensure_sops_age_tools() {
|
|||
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||
missing_packages+=(age)
|
||||
fi
|
||||
if ! command -v sops >/dev/null 2>&1; then
|
||||
missing_packages+=(sops)
|
||||
fi
|
||||
|
||||
if ((${#missing_packages[@]} > 0)); then
|
||||
echo "Installing missing secret-management tools: ${missing_packages[*]}"
|
||||
|
|
@ -7589,10 +7838,26 @@ ensure_sops_age_tools() {
|
|||
sudo apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
||||
fi
|
||||
|
||||
# age must be present; it's small and apt-provided on Debian.
|
||||
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||
echo "age-keygen is still unavailable after package installation." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# sops: not packaged on Debian trixie — fall back to the GitHub release binary.
|
||||
if ! command -v sops >/dev/null 2>&1; then
|
||||
echo "sops not found in the apt repos; installing from GitHub release..."
|
||||
local sops_ver sops_url
|
||||
sops_ver="$(curl -fsSL https://api.github.com/repos/getsops/sops/releases/latest | grep -oE '"tag_name":\s*"v[^"]+"' | head -1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')"
|
||||
if [[ -z "${sops_ver}" ]]; then
|
||||
echo "Could not determine the latest SOPS release version." >&2
|
||||
exit 1
|
||||
fi
|
||||
sops_url="https://github.com/getsops/sops/releases/download/${sops_ver}/sops-v${sops_ver#v}.linux.amd64"
|
||||
curl -fsSL -o "/tmp/sops-${sops_ver}" "${sops_url}" && \
|
||||
sudo install -m 0755 "/tmp/sops-${sops_ver}" /usr/local/bin/sops && \
|
||||
rm -f "/tmp/sops-${sops_ver}"
|
||||
fi
|
||||
if ! command -v sops >/dev/null 2>&1; then
|
||||
echo "sops is still unavailable after package installation." >&2
|
||||
exit 1
|
||||
|
|
@ -7609,6 +7874,56 @@ sops_age_recipient() {
|
|||
awk -F': ' '/^# public key:/ { print $2; exit }' "${key_file}"
|
||||
}
|
||||
|
||||
# Decrypt a SOPS-encrypted YAML of `KEY: value` pairs into the current env.
|
||||
# Usage: sops_load_secrets <repo/path/to/file.secret.yaml>
|
||||
# Requires the age key (sops_age_key_file) to decrypt; fails loudly if missing.
|
||||
sops_load_secrets() {
|
||||
local file="${1:?sops_load_secrets: missing file}"
|
||||
local key_file
|
||||
local out
|
||||
|
||||
if [[ ! -f "${file}" ]]; then
|
||||
echo "Missing encrypted secret file: ${file} (run ./jeannie secrets-init then sops -e)." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! sops_available; then
|
||||
echo "sops not installed. Run ./jeannie secrets-init first." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
key_file="$(sops_age_key_file)"
|
||||
if [[ ! -s "${key_file}" ]]; then
|
||||
echo "Missing SOPS age key at ${key_file}. Run ./jeannie secrets-init, or copy the age key to this machine." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
out="$(SOPS_AGE_KEY_FILE="${key_file}" sops --decrypt "${file}")" || {
|
||||
echo "SOPS decryption failed for ${file}." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Export every bare `KEY: value` line (ignore comments/blank).
|
||||
local line key val syn="0"
|
||||
while IFS= read -r line; do
|
||||
[[ "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*: ]] || continue
|
||||
key="${line%%:*}"
|
||||
val="${line#*: }"
|
||||
# Strip surrounding double quotes, if sops kept them.
|
||||
val="${val%\"}"; val="${val#\"}"
|
||||
export "${key}=${val}"
|
||||
syn="$((syn + 1))"
|
||||
done <<<"${out}"
|
||||
|
||||
if ((syn == 0)); then
|
||||
echo "No KEY: value pairs found in decrypted ${file}." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
sops_available() {
|
||||
command -v sops >/dev/null 2>&1
|
||||
}
|
||||
|
||||
secrets_init() {
|
||||
local key_file
|
||||
local key_dir
|
||||
|
|
@ -8108,6 +8423,9 @@ Build And Bootstrap
|
|||
up Deploy the full homelab pipeline.
|
||||
deploy-gitea Deploy the Debian-hosted Gitea container.
|
||||
deploy-heimdall Deploy the Debian-hosted Heimdall dashboard.
|
||||
deploy-matrix Deploy the Matrix homeserver (Synapse + Element + Postgres).
|
||||
deploy-omniroute Deploy the OmniRoute LLM routing gateway.
|
||||
deploy-hermes Deploy the Hermes agent (git-install + venv + gateway service).
|
||||
bootstrap-gitea-repo Ensure the Gitea repo and SSH key wiring exist.
|
||||
rpi-services Deploy Pi-hole, Unbound, and Uptime Kuma on RPi4.
|
||||
ollama-setup Install/configure Ollama on the Debian host.
|
||||
|
|
@ -8395,6 +8713,18 @@ case "${1:-}" in
|
|||
record_change_journal "deploy-heimdall" "$@"
|
||||
deploy_heimdall
|
||||
;;
|
||||
deploy-matrix)
|
||||
record_change_journal "deploy-matrix" "$@"
|
||||
deploy_matrix
|
||||
;;
|
||||
deploy-omniroute)
|
||||
record_change_journal "deploy-omniroute" "$@"
|
||||
deploy_omniroute
|
||||
;;
|
||||
deploy-hermes)
|
||||
record_change_journal "deploy-hermes" "$@"
|
||||
deploy_hermes
|
||||
;;
|
||||
rpi-services)
|
||||
record_change_journal "rpi-services" "$@"
|
||||
deploy_rpi_services
|
||||
|
|
|
|||
Loading…
Reference in New Issue