Initial commit: PFI fleet inventory, stacks, tooling, and backup pipeline
Captures the full workspace state built up to this point:
- CLAUDE.md + README.md describing conventions and the four-host fleet
(ana-ml2, ana-docker, nh3-docker, esh-docker-vm).
- Per-host notes under servers/<host>/ with ssh-target fallback files
and latest system-details snapshots (two in-compose credential leaks
scrubbed; the upstream compose files still need to move those to .env).
- scripts/: server_inspect.sh (read-only remote diagnostic),
refresh-server-info.sh (dir-driven discovery + snapshot capture with
validation warnings), add-host.sh, sync-stacks.sh (pull
compose/conf trees), deploy-stack.sh (push with per-file diff + prompt).
- stacks/: canonical compose for backrest, beszel, dozzle, llama-swap,
rest-server-ana, rest-server-nh3, vllm-qwen3, plus the retired
infinity reference. All use the .env-driven + traefik-net + homepage
label pattern.
- configs/restic/ana-docker/: first resticprofile config + pre-backup
hook (Synapse pg_dump, Seafile mysqldump, Vaultwarden SQLite); templates
for the other three hosts to come.
- docs/pfi/: general infrastructure reference carried over.
- .gitignore excludes .env, stacks-mirror/, and assorted secret/state
filenames to prevent re-leaks on later commits.
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Secrets — real .env files must never land here, only .env.example templates.
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
**/.env
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
|
# Live mirror of server /opt/docker/{compose,conf}/ trees pulled by
|
||||||
|
# sync-stacks.sh. Contains upstream compose files that can carry embedded
|
||||||
|
# plaintext credentials (e.g. legacy seafile/paperless configs), so we
|
||||||
|
# don't track them in git. Audited, hand-curated copies live under
|
||||||
|
# stacks/<name>/ and are the source of truth.
|
||||||
|
stacks-mirror/
|
||||||
|
|
||||||
|
# Staged htpasswd / secrets files that might get written to /tmp during
|
||||||
|
# helper scripts.
|
||||||
|
htpasswd-new
|
||||||
|
*.netrc
|
||||||
|
|
||||||
|
# Editor / OS cruft
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
*.log.*
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This workspace is for managing PFI infrastructure — servers, Docker stacks, and related configs. Spawn a dedicated Claude Code session here when working on infra so it doesn't clutter AIPA-MCP development context.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Inventory of servers and their state
|
||||||
|
- Canonical copies of Docker Compose stacks deployed on those servers
|
||||||
|
- Scripts for inspecting and managing the infrastructure
|
||||||
|
- Conventions so all stacks look the same
|
||||||
|
|
||||||
|
This is a **reference workspace** — the authoritative copies of compose files and configs live **on the servers** under `/opt/docker/compose/<stack>/` and `/opt/docker/conf/<stack>/`. This workspace mirrors them for version control, editing, and planning.
|
||||||
|
|
||||||
|
## Conventions (enforce for every new stack)
|
||||||
|
|
||||||
|
Observed and standardized across servers:
|
||||||
|
|
||||||
|
- **Compose location on server:** `/opt/docker/compose/<stack>/compose.yaml`
|
||||||
|
- **Config mounts on server:** `/opt/docker/conf/<stack>/...`
|
||||||
|
- **Networks:** external `traefik-net`, aliased as `tnet` in compose
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
- **GPU reservation:** prefer `deploy.resources.reservations.devices` with explicit `device_ids` for pinning
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
device_ids: ["1"]
|
||||||
|
capabilities: [gpu]
|
||||||
|
```
|
||||||
|
- **Tunables:** `.env` in the same directory as `compose.yaml` — keep the compose file constant, edit the `.env`
|
||||||
|
- **Named volumes** for service state (pattern: `<stack>_<name>`)
|
||||||
|
- **Bind mounts** only for: model files (`/tank/aimodels/...`), config files (`/opt/docker/conf/...`), docker socket where required
|
||||||
|
- **Restart policy:** `restart: unless-stopped` for daemons
|
||||||
|
- **Homepage labels** on user-facing services:
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- homepage.group=AI Systems
|
||||||
|
- homepage.name=<ServiceName>
|
||||||
|
- homepage.icon=mdi-<icon>
|
||||||
|
- homepage.description=<short>
|
||||||
|
- homepage.href=http://<host-ip>:<port>
|
||||||
|
```
|
||||||
|
- **Healthchecks** on services that expose HTTP
|
||||||
|
|
||||||
|
## Servers
|
||||||
|
|
||||||
|
| Name | IP | Site | Role | Details |
|
||||||
|
|------|-----|------|------|---------|
|
||||||
|
| ana-ml2 | 10.250.50.54 | Anaheim (`10.250.0.0/16`) | GPU / AI inference | `servers/ana-ml2/README.md` |
|
||||||
|
| ana-docker | 10.250.50.70 | Anaheim (`10.250.0.0/16`) | General-purpose Docker host (non-GPU) | `servers/ana-docker/README.md` |
|
||||||
|
| nh3-docker | 10.100.50.40 | NH3 (`10.100.0.0/16`) | General-purpose Docker host (non-GPU) | `servers/nh3-docker/README.md` |
|
||||||
|
| esh-docker-vm | 10.0.50.45 | ESH home lab (`esteban.net`, `10.0.50.0/24`) | Home-lab Docker host (non-PFI scope) | `servers/esh-docker-vm/README.md` |
|
||||||
|
|
||||||
|
**Placement rules:**
|
||||||
|
- GPU-required stacks → `ana-ml2`.
|
||||||
|
- Anaheim non-GPU services → `ana-docker`.
|
||||||
|
- NH-site non-GPU services → `nh3-docker`.
|
||||||
|
- ESH home-lab workloads (`esteban.net`) → `esh-docker-vm`. Not part of the PFI colo topology, but shares monitoring/backup tooling.
|
||||||
|
- Cross-site services (e.g. Beszel hub, Dozzle hub) live on `ana-docker` and pull from agents on the other hosts.
|
||||||
|
|
||||||
|
## How to refresh a server's state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help (no args)
|
||||||
|
scripts/refresh-server-info.sh
|
||||||
|
|
||||||
|
# Refresh every host discovered under servers/*/
|
||||||
|
scripts/refresh-server-info.sh all
|
||||||
|
|
||||||
|
# Refresh a specific host (must match a servers/<name>/ dir; ssh_config
|
||||||
|
# entry or servers/<name>/ssh-target handles how to reach it)
|
||||||
|
scripts/refresh-server-info.sh ana-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Fleet-wide runs require the literal `all` keyword — no-args prints help so you can't accidentally hit every host by forgetting a name.
|
||||||
|
|
||||||
|
The script pipes `server_inspect.sh` over SSH via stdin (no scp, no remote cleanup) and writes each `servers/<host>/system-details.txt` atomically — a failed run never clobbers the previous snapshot. The inspect script itself is read-only.
|
||||||
|
|
||||||
|
Each server dir can hold an `ssh-target` file (one line, `<ip>` or `<user>@<ip>`) as a fallback for when the dir name doesn't resolve via DNS or `~/.ssh/config`. The script prefers whatever ssh would resolve normally and only consults the file when that fails.
|
||||||
|
|
||||||
|
To register a new server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/add-host.sh <name> <ip-or-user@ip>
|
||||||
|
scripts/refresh-server-info.sh <name> # pull the first snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
To audit discovery without touching the network (checks permissions, unresolvable names with no fallback, missing README / system-details, malformed `ssh-target`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh --validate-only all
|
||||||
|
scripts/refresh-server-info.sh --validate-only <host>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack mirror (pull / push)
|
||||||
|
|
||||||
|
Compose and config trees are mirrored into `stacks-mirror/<host>/<stack>/` so they can be diffed and version-controlled. Pull is fleet-wide and safe; push is one stack at a time with a diff + prompt.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull compose + conf from every host into stacks-mirror/
|
||||||
|
scripts/sync-stacks.sh
|
||||||
|
scripts/sync-stacks.sh --dry-run # see what would change
|
||||||
|
scripts/sync-stacks.sh ana-docker # one host
|
||||||
|
|
||||||
|
# Push a local stack back to the server (diffs each file, prompts y/N)
|
||||||
|
scripts/deploy-stack.sh <host> <stack>
|
||||||
|
scripts/deploy-stack.sh <host> <stack> --compose # skip conf
|
||||||
|
scripts/deploy-stack.sh <host> <stack> --conf # skip compose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Opt-out per stack:** create `stacks-mirror/<host>/<stack>/.no-sync` (skip both sides) or `stacks-mirror/<host>/<stack>/conf/.no-sync` (skip conf only).
|
||||||
|
|
||||||
|
**Always excluded in both directions** (secrets / runtime state): `.env`, `.env.*`, `acme.json`, `client_secrets.json`, `*.pem`, `*.key`, `*.crt`, `*.pfx`, `*.sqlite`, `*.sqlite3`, `*.db`, `*.log`, `*.log.*`, `*.pid`, `hub/`, `logs/`.
|
||||||
|
|
||||||
|
Requires `rsync` installed on this workstation and every host you sync against (`apt install rsync`).
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
eshpfi-management/
|
||||||
|
├── CLAUDE.md # this file
|
||||||
|
├── README.md # human-facing overview
|
||||||
|
├── scripts/
|
||||||
|
│ └── server_inspect.sh # gather server state for compose planning
|
||||||
|
├── servers/
|
||||||
|
│ └── <name>/
|
||||||
|
│ ├── README.md
|
||||||
|
│ └── system-details.txt # latest server_inspect output
|
||||||
|
├── stacks/
|
||||||
|
│ └── <stack>/
|
||||||
|
│ ├── compose.yaml # deployed to /opt/docker/compose/<stack>/
|
||||||
|
│ ├── .env.example # template; real .env lives on server
|
||||||
|
│ └── README.md # what this stack does, how to deploy
|
||||||
|
└── docs/
|
||||||
|
└── pfi/ # general PFI infrastructure reference
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working rules
|
||||||
|
|
||||||
|
- **Copies, not symlinks.** Files here reflect what's on the server at the time of the last sync. When you edit here, the server doesn't change until you deploy.
|
||||||
|
- **Never commit secrets.** Use `.env.example` templates; real `.env` files (with tokens, passwords) live on the server and are gitignored if/when this becomes a git repo.
|
||||||
|
- **Surgical edits.** When fixing one stack, don't touch unrelated ones. Follow AIPA-MCP's CLAUDE.md rules about scope discipline.
|
||||||
|
- **Sanity-check before deploying.** Run `docker compose config` (dry parse) before `docker compose up -d` on the server.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# eshpfi-management
|
||||||
|
|
||||||
|
Infrastructure management workspace for the PFI fleet (plus the ESH home-lab host). Tracks server state, canonical Docker Compose stacks, per-host configs, and the tooling that moves them around.
|
||||||
|
|
||||||
|
See **[CLAUDE.md](CLAUDE.md)** for the full set of conventions and the rules Claude Code sessions follow when working here.
|
||||||
|
|
||||||
|
## The fleet
|
||||||
|
|
||||||
|
| Host | IP | Site | Role |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ana-ml2 | `10.250.50.54` | Anaheim (`10.250.0.0/16`) | GPU / AI inference |
|
||||||
|
| ana-docker | `10.250.50.70` | Anaheim | General-purpose Docker + cross-site hubs |
|
||||||
|
| nh3-docker | `10.100.50.40` | NH3 (`10.100.0.0/16`) | General-purpose Docker (NH site) |
|
||||||
|
| esh-docker-vm | `10.0.50.45` | ESH home lab (`esteban.net`) | Home-lab Docker (non-PFI scope) |
|
||||||
|
|
||||||
|
Per-host snapshots of the running system live under `servers/<host>/system-details.txt`, refreshed via `scripts/refresh-server-info.sh`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── CLAUDE.md # conventions; loaded by Claude Code sessions
|
||||||
|
├── README.md # this file
|
||||||
|
├── scripts/ # workstation tooling
|
||||||
|
│ ├── server_inspect.sh # read-only diagnostic, runs on remote via stdin
|
||||||
|
│ ├── refresh-server-info.sh # pull fresh system-details.txt for one/all hosts
|
||||||
|
│ ├── add-host.sh # register a new server (writes servers/<name>/ssh-target)
|
||||||
|
│ ├── sync-stacks.sh # pull /opt/docker/{compose,conf}/ → stacks-mirror/
|
||||||
|
│ └── deploy-stack.sh # push stacks-mirror/<host>/<stack>/ with diff + prompt
|
||||||
|
├── servers/ # per-host notes + latest snapshot + ssh-target fallback
|
||||||
|
│ └── <host>/
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── system-details.txt # regenerate on demand
|
||||||
|
│ └── ssh-target # <ip> or <user>@<ip>, used when DNS fails
|
||||||
|
├── stacks/ # canonical compose files (source of truth)
|
||||||
|
│ └── <stack>/
|
||||||
|
│ ├── compose.yaml
|
||||||
|
│ ├── .env.example
|
||||||
|
│ └── README.md
|
||||||
|
├── stacks-mirror/ # gitignored — live mirror from sync-stacks.sh
|
||||||
|
├── configs/ # host-level config files that aren't docker-compose
|
||||||
|
│ └── restic/<host>/ # resticprofile configs + pre-backup hooks
|
||||||
|
└── docs/ # general reference (network, models, proxmox, etc.)
|
||||||
|
└── pfi/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current stacks
|
||||||
|
|
||||||
|
**GPU (ana-ml2):**
|
||||||
|
- `llama-swap` — GGUF model swapper via llama.cpp (port 9292)
|
||||||
|
- `vllm-qwen3` — embeddings (8001) + reranker (8002) via vLLM
|
||||||
|
|
||||||
|
**Anaheim non-GPU (ana-docker):**
|
||||||
|
- `traefik`, `crowdsec`, `gitea`, `vaultwarden`, `synapse`, `seafile`, `searxng`, `openwebui`, `sillytavern`, `mailrise`, `rustdesk`, `dockge`, `it-tools`
|
||||||
|
- Fleet services: `beszel` (metrics hub, port 8090), `dozzle-hub` (log viewer, 8088), `backrest` (restic UI, 9898)
|
||||||
|
- Backup target: `rest-server-ana` on port 8000
|
||||||
|
|
||||||
|
**NH3 (nh3-docker):**
|
||||||
|
- `adguard`, `dockge`, plus Beszel/Dozzle agents
|
||||||
|
|
||||||
|
**NH3 (Synology `10.100.50.50`):**
|
||||||
|
- `rest-server-nh3` — restic backup target (port 8000)
|
||||||
|
|
||||||
|
**ESH home lab (esh-docker-vm):**
|
||||||
|
- `adguard`, `homeassistant` (macvlan), `esphome`, `mosquitto`, `paperless-ngx`, `pgadmin`, `calibre`, `calibre-web`, `drawio`, `traefik`, `homepage`, `uptime-kuma`, plus Beszel/Dozzle agents
|
||||||
|
|
||||||
|
## Common tasks
|
||||||
|
|
||||||
|
**Refresh one host's snapshot:**
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh ana-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refresh all hosts:**
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add a new host:**
|
||||||
|
```bash
|
||||||
|
scripts/add-host.sh <name> <ip-or-user@ip>
|
||||||
|
scripts/refresh-server-info.sh <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validate discovery (without hitting the network):**
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh --validate-only all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Push a stack to a host (with diff + confirm):**
|
||||||
|
```bash
|
||||||
|
scripts/deploy-stack.sh <host> <stack>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pull every server's compose/conf trees into stacks-mirror/ (not committed — see `.gitignore`):**
|
||||||
|
```bash
|
||||||
|
scripts/sync-stacks.sh all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup pipeline
|
||||||
|
|
||||||
|
Backups are driven by per-host `resticprofile` configs under `configs/restic/<host>/`, scheduled via systemd timers on each host:
|
||||||
|
|
||||||
|
- **Writes**: each host backs up to its site-local rest-server (`rest-server-ana` or `rest-server-nh3`), over HTTP basic-auth.
|
||||||
|
- **Authentication**: shared `.htpasswd` file on both rest-servers, one entry per host; credentials stored in `/etc/restic/restic.env` on each client host.
|
||||||
|
- **Encryption**: per-host client-side passphrase in `/etc/restic/password` (unique per repo; losing it = losing that host's backups).
|
||||||
|
- **Visibility**: Backrest (`http://10.250.50.70:9898`) shows every repo for browsing/restore.
|
||||||
|
- **Schedule**: backup at 01:00 daily, `forget` at 03:00 daily, weekly `check --read-data-subset 10%` on Sundays.
|
||||||
|
- **Prune**: manual ceremony (rest-server runs with `--append-only`, which blocks destructive prune ops).
|
||||||
|
- **Off-site**: cross-site rsync between the two rest-server data dirs is planned (not yet implemented).
|
||||||
|
|
||||||
|
## Authoritative vs. mirror
|
||||||
|
|
||||||
|
- **Authoritative:** files on each server under `/opt/docker/compose/<stack>/` and `/opt/docker/conf/<stack>/`.
|
||||||
|
- **This workspace:** source-of-truth copies under `stacks/<name>/` (hand-curated), and a gitignored mirror under `stacks-mirror/` pulled by `sync-stacks.sh`.
|
||||||
|
|
||||||
|
Edit in `stacks/`, push with `deploy-stack.sh`. Never commit `stacks-mirror/` — it can contain embedded plaintext secrets from upstream compose files that haven't been audited yet.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# configs/restic
|
||||||
|
|
||||||
|
Per-host restic backup configs, deployed into `/etc/restic/` on each server and driven by `resticprofile` + `systemd` timers.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
configs/restic/
|
||||||
|
├── README.md # this file
|
||||||
|
└── <host>/
|
||||||
|
├── profiles.yaml # committed, zero secrets
|
||||||
|
├── pre-backup.sh # committed, zero secrets
|
||||||
|
└── README.md # per-host notes (paths, containers, quirks)
|
||||||
|
```
|
||||||
|
|
||||||
|
On each server, deployed to `/etc/restic/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/etc/restic/
|
||||||
|
├── profiles.yaml # scp'd from configs/restic/<host>/profiles.yaml
|
||||||
|
├── pre-backup.sh # scp'd, 0755, root:root
|
||||||
|
├── password # 0400 root:root — client-side encryption passphrase
|
||||||
|
└── restic.env # 0600 root:root — RESTIC_REPOSITORY=rest:http://user:pw@host:port/path/
|
||||||
|
/var/lib/restic/
|
||||||
|
├── stage/ # temp staging for DB dumps; owned by root, 0700
|
||||||
|
└── last-success # unix timestamp of the last successful run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
- **No secrets in committed config.** `profiles.yaml` references `RESTIC_PASSWORD_FILE=/etc/restic/password` and loads `RESTIC_REPOSITORY` from `restic.env`. Both files live only on the host, 0400/0600 root-owned.
|
||||||
|
- **Creds go in the URL, not netrc.** restic's rest backend doesn't consult `~/.netrc` — HTTP basic-auth has to be embedded in the repository URL. Keeping that URL in an env-file (not the committed YAML) means the secret stays on the host.
|
||||||
|
- **Pre-hook runs DB dumps into a staging dir**, then `restic backup` includes that dir alongside the regular paths. One snapshot = one point-in-time.
|
||||||
|
- **`restic forget` is scheduled; `restic prune` is not.** Rest-server's `--append-only` blocks prune from the client side by design. Prune is a manual ceremony (flip the flag, run prune, flip back).
|
||||||
|
|
||||||
|
## Install restic + resticprofile on each host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Current-enough restic. Debian 12 ships 0.14 (too old for some flags);
|
||||||
|
# Debian 13 ships 0.18. If you're on 12, grab the .deb from the upstream
|
||||||
|
# release page instead.
|
||||||
|
sudo apt install -y restic # or install 0.18+ from github.com/restic/restic/releases
|
||||||
|
|
||||||
|
# resticprofile is not in Debian. Download the .deb from its release page.
|
||||||
|
v=$(curl -sI https://github.com/creativeprojects/resticprofile/releases/latest \
|
||||||
|
| awk -F'/' '/^location:/{sub(/\r/,""); print $NF}')
|
||||||
|
v=${v#v}
|
||||||
|
url="https://github.com/creativeprojects/resticprofile/releases/download/v${v}/resticprofile_${v}_linux_amd64.deb"
|
||||||
|
curl -sL -o /tmp/resticprofile.deb "$url"
|
||||||
|
sudo dpkg -i /tmp/resticprofile.deb
|
||||||
|
rm /tmp/resticprofile.deb
|
||||||
|
resticprofile version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-host deploy flow (done once per host)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HOST=ana-docker # or ana-ml2, nh3-docker, esh-docker-vm
|
||||||
|
|
||||||
|
# 1. Create the target dir (one-time)
|
||||||
|
ssh -t "$HOST" 'sudo install -d -o root -g root -m 0755 /etc/restic'
|
||||||
|
ssh -t "$HOST" 'sudo install -d -o root -g root -m 0700 /var/lib/restic/stage'
|
||||||
|
|
||||||
|
# 2. Seed the two secret files on the host (never in this repo):
|
||||||
|
# - the client-side encryption passphrase (the one used at `restic init`)
|
||||||
|
# - an env-file with the full RESTIC_REPOSITORY URL including HTTP creds
|
||||||
|
ssh -t "$HOST" 'sudo install -o root -g root -m 0400 /dev/null /etc/restic/password'
|
||||||
|
ssh -t "$HOST" 'sudo install -o root -g root -m 0600 /dev/null /etc/restic/restic.env'
|
||||||
|
|
||||||
|
# Seed content (replace <…> with real values from your vault):
|
||||||
|
ssh -t "$HOST" "echo '<encryption-passphrase>' | sudo tee /etc/restic/password >/dev/null"
|
||||||
|
ssh -t "$HOST" "echo 'RESTIC_REPOSITORY=rest:http://<user>:<http-pw>@<rest-server>:8000/<user>/' | sudo tee /etc/restic/restic.env >/dev/null"
|
||||||
|
|
||||||
|
# 3. Push the committed config + pre-hook
|
||||||
|
scp "configs/restic/$HOST/profiles.yaml" "$HOST:/tmp/profiles.yaml"
|
||||||
|
scp "configs/restic/$HOST/pre-backup.sh" "$HOST:/tmp/pre-backup.sh"
|
||||||
|
ssh -t "$HOST" "
|
||||||
|
sudo install -o root -g root -m 0644 /tmp/profiles.yaml /etc/restic/profiles.yaml
|
||||||
|
sudo install -o root -g root -m 0755 /tmp/pre-backup.sh /etc/restic/pre-backup.sh
|
||||||
|
rm -f /tmp/profiles.yaml /tmp/pre-backup.sh
|
||||||
|
"
|
||||||
|
|
||||||
|
# 4. Test the profile before scheduling
|
||||||
|
ssh -t "$HOST" 'sudo resticprofile --config /etc/restic/profiles.yaml --name default backup --dry-run'
|
||||||
|
|
||||||
|
# 5. When dry-run looks clean, wire up systemd timers
|
||||||
|
ssh -t "$HOST" 'sudo resticprofile --config /etc/restic/profiles.yaml schedule'
|
||||||
|
# → installs restic-backup@<profile>.{service,timer} units for backup/forget/check
|
||||||
|
|
||||||
|
# 6. Verify
|
||||||
|
ssh -t "$HOST" 'sudo systemctl list-timers | grep restic'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prune ceremony (quarterly or as needed)
|
||||||
|
|
||||||
|
Per-repo, when enough forgotten-but-still-on-disk snapshots accumulate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the rest-server host (ana-docker or the Synology):
|
||||||
|
# 1. Stop the append-only rest-server, start one without the flag
|
||||||
|
# (simplest: edit the stack's .env, remove --append-only from OPTIONS,
|
||||||
|
# docker compose up -d)
|
||||||
|
|
||||||
|
# 2. From the client host, run prune
|
||||||
|
ssh -t <host> 'sudo resticprofile --config /etc/restic/profiles.yaml --name default prune'
|
||||||
|
|
||||||
|
# 3. Put --append-only back on the rest-server and docker compose up -d.
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively stand up a second rest-server stack on port 8001 without `--append-only` and point prune runs at that endpoint; keep the append-only one for daily writes.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Each successful run writes `/var/lib/restic/last-success` with the current unix timestamp (this is the `post-backup` hook in `profiles.yaml`). Beszel can be configured to alert when that file's mtime exceeds ~36 hours.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh <host> 'stat -c "%y %n" /var/lib/restic/last-success 2>/dev/null || echo no successful run yet'
|
||||||
|
```
|
||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# pre-backup.sh — ana-docker.
|
||||||
|
# Runs as root from resticprofile's `run-before`, before `restic backup`.
|
||||||
|
#
|
||||||
|
# Produces DB dumps in /var/lib/restic/stage/ so the nightly restic
|
||||||
|
# snapshot captures consistent point-in-time data for services whose
|
||||||
|
# raw volume files are not safe to back up live.
|
||||||
|
#
|
||||||
|
# Containers handled here:
|
||||||
|
# - synapse-db (Postgres 16)
|
||||||
|
# - seafile-mysql (MariaDB 10.6)
|
||||||
|
# - vaultwarden (SQLite w/ WAL; online .backup via sqlite3 if available)
|
||||||
|
#
|
||||||
|
# Gitea's DB is external (hosted elsewhere in the LAN) so we only back
|
||||||
|
# up its data volume; whoever owns the gitea Postgres backs it up
|
||||||
|
# separately.
|
||||||
|
#
|
||||||
|
# Idempotent: clears and recreates its staging files each run.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STAGE=/var/lib/restic/stage
|
||||||
|
install -d -o root -g root -m 0700 "$STAGE"
|
||||||
|
|
||||||
|
log() { printf '%s pre-backup(ana-docker): %s\n' "$(date -Is)" "$*"; }
|
||||||
|
|
||||||
|
# Purge previous stage so stale dumps don't pile up and end up in the snapshot.
|
||||||
|
find "$STAGE" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||||
|
|
||||||
|
# ---------- synapse-db (Postgres) ---------------------------------------------
|
||||||
|
if docker inspect synapse-db >/dev/null 2>&1; then
|
||||||
|
log "dumping synapse postgres"
|
||||||
|
# -Fc custom format, internally compressed + restore-to-subset friendly
|
||||||
|
docker exec synapse-db \
|
||||||
|
pg_dump -U synapse -d synapse -Fc --clean --if-exists \
|
||||||
|
> "$STAGE/synapse.pg_dump"
|
||||||
|
else
|
||||||
|
log "skip synapse: container not present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- seafile-mysql (MariaDB) -------------------------------------------
|
||||||
|
if docker inspect seafile-mysql >/dev/null 2>&1; then
|
||||||
|
log "dumping seafile mariadb"
|
||||||
|
# The root password lives in the container's own env (MYSQL_ROOT_PASSWORD);
|
||||||
|
# expand it inside the container so it never lands in the host's process list.
|
||||||
|
docker exec seafile-mysql sh -c \
|
||||||
|
'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases --single-transaction --quick 2>/dev/null' \
|
||||||
|
| gzip -c > "$STAGE/seafile.sql.gz"
|
||||||
|
else
|
||||||
|
log "skip seafile: container not present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- vaultwarden (SQLite + WAL) ----------------------------------------
|
||||||
|
# Vaultwarden uses SQLite in WAL mode. A live copy of db.sqlite3 + -wal is
|
||||||
|
# usually recoverable, but sqlite3's own .backup pragma is the correct way
|
||||||
|
# to get a consistent snapshot. If the vaultwarden image has sqlite3
|
||||||
|
# available, use it; otherwise rely on restic backing up the raw volume.
|
||||||
|
if docker inspect vaultwarden >/dev/null 2>&1; then
|
||||||
|
if docker exec vaultwarden sh -c 'command -v sqlite3 >/dev/null 2>&1'; then
|
||||||
|
log "dumping vaultwarden sqlite via .backup"
|
||||||
|
docker exec vaultwarden sqlite3 /data/db.sqlite3 \
|
||||||
|
".backup /tmp/vaultwarden.sqlite3"
|
||||||
|
docker cp vaultwarden:/tmp/vaultwarden.sqlite3 "$STAGE/vaultwarden.sqlite3"
|
||||||
|
docker exec vaultwarden rm -f /tmp/vaultwarden.sqlite3
|
||||||
|
else
|
||||||
|
log "skip vaultwarden .backup: sqlite3 not in container (raw volume still included via restic)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "skip vaultwarden: container not present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- summary -----------------------------------------------------------
|
||||||
|
size=$(du -sh "$STAGE" 2>/dev/null | awk '{print $1}')
|
||||||
|
count=$(find "$STAGE" -type f | wc -l)
|
||||||
|
log "stage ready: $count files, $size total"
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# resticprofile config for ana-docker.
|
||||||
|
#
|
||||||
|
# Writes to the Anaheim-side rest-server at 10.250.50.70 as user
|
||||||
|
# `ana-docker`. The full REST URL (including HTTP basic-auth creds)
|
||||||
|
# lives in /etc/restic/restic.env — loaded via env-file so this YAML
|
||||||
|
# carries zero secrets and is safe to version-control.
|
||||||
|
#
|
||||||
|
# The client-side encryption passphrase lives in /etc/restic/password.
|
||||||
|
|
||||||
|
version: "1"
|
||||||
|
|
||||||
|
global:
|
||||||
|
priority: low
|
||||||
|
ionice: true
|
||||||
|
ionice-class: 2
|
||||||
|
ionice-level: 7
|
||||||
|
min-memory: 100
|
||||||
|
|
||||||
|
default:
|
||||||
|
env-file: /etc/restic/restic.env # provides RESTIC_REPOSITORY=rest:http://user:pw@…
|
||||||
|
env:
|
||||||
|
RESTIC_PASSWORD_FILE: /etc/restic/password
|
||||||
|
initialize: false # repo was created by `restic init`
|
||||||
|
lock: /var/lock/restic-ana-docker.lock
|
||||||
|
|
||||||
|
backup:
|
||||||
|
verbose: 1
|
||||||
|
run-before:
|
||||||
|
- /etc/restic/pre-backup.sh
|
||||||
|
run-after:
|
||||||
|
- date +%s > /var/lib/restic/last-success
|
||||||
|
source:
|
||||||
|
- /opt/docker
|
||||||
|
- /var/lib/docker/volumes
|
||||||
|
- /var/lib/restic/stage
|
||||||
|
exclude:
|
||||||
|
# Docker internals we never want in a backup
|
||||||
|
- /var/lib/docker/volumes/backingFsBlockDev
|
||||||
|
- /var/lib/docker/volumes/metadata.db
|
||||||
|
# Raw DB files — we dump them via pre-backup.sh into /var/lib/restic/stage
|
||||||
|
- /var/lib/docker/volumes/synapse-db-data
|
||||||
|
- /var/lib/docker/volumes/synapse_synapse-db-data
|
||||||
|
- /var/lib/docker/volumes/seafile_db
|
||||||
|
# Ephemeral / regenerable junk
|
||||||
|
- /opt/docker/compose/*/logs
|
||||||
|
- /opt/docker/conf/traefik-ana/acme.json # secret material; excluded everywhere
|
||||||
|
- /opt/docker/conf/crowdsec/hub # upstream-managed, regenerable
|
||||||
|
- "**/*.log"
|
||||||
|
- "**/*.log.*"
|
||||||
|
- "**/*.pid"
|
||||||
|
tag:
|
||||||
|
- host:ana-docker
|
||||||
|
- site:ana
|
||||||
|
- fleet:pfi
|
||||||
|
schedule: "*-*-* 01:00:00"
|
||||||
|
schedule-permission: system
|
||||||
|
schedule-log: /var/log/restic-backup.log
|
||||||
|
|
||||||
|
forget:
|
||||||
|
keep-daily: 7
|
||||||
|
keep-weekly: 4
|
||||||
|
keep-monthly: 12
|
||||||
|
keep-yearly: 3
|
||||||
|
# NOTE: no `prune: true` — rest-server runs with --append-only, which
|
||||||
|
# blocks the destructive half of prune. See README.md "Prune ceremony".
|
||||||
|
tag:
|
||||||
|
- host:ana-docker
|
||||||
|
schedule: "*-*-* 03:00:00"
|
||||||
|
schedule-permission: system
|
||||||
|
schedule-log: /var/log/restic-forget.log
|
||||||
|
|
||||||
|
check:
|
||||||
|
read-data-subset: 10%
|
||||||
|
schedule: "Sun *-*-* 05:00:00"
|
||||||
|
schedule-permission: system
|
||||||
|
schedule-log: /var/log/restic-check.log
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# ChromaDB Setup Documentation
|
||||||
|
|
||||||
|
**Project**: Infrastructure-PFI
|
||||||
|
**Target Server**: PFI-ANA-Docker (VM 102)
|
||||||
|
**IP Address**: 10.250.50.x (VLAN 50)
|
||||||
|
**Status**: Ready for deployment
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ChromaDB is an embedded vector database optimized for AI/ML applications. This deployment provides:
|
||||||
|
|
||||||
|
- Persistent vector storage on `/tank/chromadb/`
|
||||||
|
- REST API on port 8000 (internal + Traefik-routed)
|
||||||
|
- Token-based authentication
|
||||||
|
- Automated backup and health monitoring
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ PFI-ANA-Docker (VM 102) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
|
||||||
|
│ │ Traefik │───▶│ ChromaDB │ │ Dockge │ │
|
||||||
|
│ │ Reverse │ │ (Port 8000) │ │ Manager │ │
|
||||||
|
│ │ Proxy │ │ │ │ │ │
|
||||||
|
│ └──────────────┘ └──────┬───────┘ └───────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────▼────────┐ │
|
||||||
|
│ │ /tank/chromadb │ │
|
||||||
|
│ │ (bind mount) │ │
|
||||||
|
│ └─────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Locations (on VM 102)
|
||||||
|
|
||||||
|
| Host Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `/opt/docker/conf/chromadb/` | Compose file, auth token, config |
|
||||||
|
| `/opt/docker/conf/chromadb/docker-compose.yml` | Main compose file |
|
||||||
|
| `/opt/docker/conf/chromadb/auth_token` | Token for API authentication |
|
||||||
|
| `/tank/chromadb/` | Persistent vector data (bind mount) |
|
||||||
|
| `/opt/docker/backups/chromadb/` | Backup archives |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
This deployment uses **ChromaDB's native token auth**:
|
||||||
|
|
||||||
|
- A random 64-hex-char token is generated during setup (`openssl rand -hex 32`)
|
||||||
|
- The token is stored at `/opt/docker/conf/chromadb/auth_token` (mode 600)
|
||||||
|
- Clients must supply the token via `Settings`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import chromadb
|
||||||
|
from chromadb.config import Settings
|
||||||
|
|
||||||
|
client = chromadb.HttpClient(
|
||||||
|
host="10.250.50.x", # or chromadb.pfi.local via Traefik
|
||||||
|
port=8000,
|
||||||
|
settings=Settings(
|
||||||
|
chroma_client_auth_provider="chromadb.auth.token.TokenAuthClientProvider",
|
||||||
|
chroma_client_auth_credentials="YOUR_TOKEN_HERE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
print(client.heartbeat())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
### 1. Copy compose file to VM 102
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp configs/pfi-ana/docker/compose-examples/chromadb/docker-compose.yml \
|
||||||
|
root@10.250.50.x:/opt/docker/conf/chromadb/docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run the setup script (on VM 102)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy scripts to VM 102
|
||||||
|
scp scripts/setup-chromadb.sh root@10.250.50.x:/opt/docker/conf/chromadb/
|
||||||
|
ssh root@10.250.50.x
|
||||||
|
|
||||||
|
# Run setup
|
||||||
|
cd /opt/docker/conf/chromadb
|
||||||
|
chmod +x setup-chromadb.sh
|
||||||
|
./setup-chromadb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. (Optional) Run the demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy demo files
|
||||||
|
scp -r configs/pfi-ana/docker/compose-examples/chromadb/ root@10.250.50.x:/tmp/chromadb-demo/
|
||||||
|
|
||||||
|
# On VM 102, edit CHROMA_TOKEN in docker-compose.demo.yml
|
||||||
|
cd /tmp/chromadb-demo
|
||||||
|
# Set CHROMA_TOKEN in docker-compose.demo.yml to match auth_token
|
||||||
|
docker compose -f docker-compose.demo.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring & Maintenance
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|---|---|
|
||||||
|
| Health check | `./scripts/health-check-chromadb.sh` |
|
||||||
|
| View logs | `docker logs -f chromadb` |
|
||||||
|
| Backup | `./scripts/backup-chromadb.sh` |
|
||||||
|
| Restart | `docker compose restart` |
|
||||||
|
| Stop | `docker compose down` |
|
||||||
|
|
||||||
|
### Cron (daily backup at 2 AM)
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 2 * * * /opt/docker/conf/chromadb/backup-chromadb.sh >> /var/log/chromadb-backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
| Aspect | Value |
|
||||||
|
|---|---|
|
||||||
|
| Docker network | `traefik-net` (aliased as `tnet`) |
|
||||||
|
| Internal port | 8000 |
|
||||||
|
| Traefik host rule | `chromadb.pfi.local` |
|
||||||
|
| Traefik entrypoint | `websecure` (HTTPS) |
|
||||||
|
| TLS | Enabled via Traefik |
|
||||||
|
|
||||||
|
## Project Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `configs/pfi-ana/docker/compose-examples/chromadb/docker-compose.yml` | Production compose (for Dockge) |
|
||||||
|
| `configs/pfi-ana/docker/compose-examples/chromadb/docker-compose.demo.yml` | Demo client |
|
||||||
|
| `configs/pfi-ana/docker/compose-examples/chromadb/Dockerfile.demo` | Demo image |
|
||||||
|
| `configs/pfi-ana/docker/compose-examples/chromadb/scripts/demo.py` | Demo test script |
|
||||||
|
| `scripts/setup-chromadb.sh` | Deployment script (run on VM 102) |
|
||||||
|
| `scripts/backup-chromadb.sh` | Backup script (run on VM 102) |
|
||||||
|
| `scripts/health-check-chromadb.sh` | Health monitoring (run on VM 102) |
|
||||||
|
| `scripts/quickstart-chromadb.sh` | Convenience wrapper for setup |
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
# PFI-ANA Docker Stack
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PFI-ANA (the Colo) runs Docker services managed through **Dockge**, a compose-aware Docker management UI. All services that require inbound HTTP/HTTPS routing join a shared external Docker network called `traefik-net`, allowing **Traefik** to act as a reverse proxy and handle TLS termination and routing.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
| Network | Docker Name | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| Traefik network | `traefik-net` | Shared external network. Services join as `tnet` so Traefik can discover them. |
|
||||||
|
|
||||||
|
Every compose file that needs to be reachable through Traefik **must** include:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
And the service must list `tnet` under its `networks` key.
|
||||||
|
|
||||||
|
### Storage Paths (Host)
|
||||||
|
|
||||||
|
| Host Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `/opt/docker/compose/<service>/` | Per-service compose files (managed by Dockge) |
|
||||||
|
| `/opt/docker/conf/<service>/` | Per-service configuration files (bind-mounted into containers) |
|
||||||
|
| `/tank/` | Large / persistent data storage (e.g., AI models, generated images, voice data) |
|
||||||
|
|
||||||
|
### GPU Support
|
||||||
|
|
||||||
|
Services requiring GPU access use the **NVIDIA Container Toolkit**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
runtime: nvidia
|
||||||
|
```
|
||||||
|
|
||||||
|
or the more explicit device reservation:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Homepage Dashboard Labels
|
||||||
|
|
||||||
|
Several services include Docker labels for **Homepage** (a dashboard UI). The convention is:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- homepage.group=<Group Name>
|
||||||
|
- homepage.name=<Display Name>
|
||||||
|
- homepage.icon=<icon identifier>
|
||||||
|
- homepage.description=<brief description>
|
||||||
|
- homepage.href=http://<host>:<port>
|
||||||
|
```
|
||||||
|
|
||||||
|
All services reference the VM 102 host IP `10.250.50.70` (PFI-ANA_DOCKER).
|
||||||
|
|
||||||
|
### Compose File Location
|
||||||
|
|
||||||
|
Dockge expects compose files under `/opt/docker/compose/` on the Docker host. The live compose files are version-controlled in this project under `configs/pfi-ana/docker/compose/`. Configuration files that containers bind-mount live under `configs/pfi-ana/docker/conf/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live Services Index
|
||||||
|
|
||||||
|
| Service | Host Port | Container Port | GPU | Homepage Group | Status | Compose File | Config File |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **Dockge** | 5001 | 5001 | No | PFI-ANA | ✅ Live | `compose/dockge/compose.yaml` | — |
|
||||||
|
| **llama-swap** | 9292 | 8080 | Yes (CUDA) | — | ✅ Live | `compose/llama-swap/compose.yaml` | `conf/llama-swap/config.yaml` |
|
||||||
|
| **ComfyUI** | 8188 | 8188 | Yes (all caps) | AI Systems | ✅ Live | `compose/comfyui/compose.yaml` | — |
|
||||||
|
| **VibeVoice** | 8745 | 8745 | Yes (gpu) | AI Systems | ✅ Live | `compose/vibevoice/compose.yaml` | — |
|
||||||
|
| **Parakeet STT** | 8300 | 8000 | Yes (gpu) | AI Systems | ✅ Live | `compose/parakeet/compose.yaml` | — |
|
||||||
|
| **ChromaDB** | 8000 | 8000 | No | AI Systems | ✅ Live | `compose/chromadb/compose.yaml` | `/opt/docker/conf/chromadb/auth_token` |
|
||||||
|
|
||||||
|
> All paths relative to `configs/pfi-ana/docker/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Details
|
||||||
|
|
||||||
|
### 1. Dockge — Docker Compose Management UI
|
||||||
|
|
||||||
|
- **Image**: `louislam/dockge:latest`
|
||||||
|
- **Port**: 5001 → 5001
|
||||||
|
- **Restart policy**: `unless-stopped`
|
||||||
|
- **Homepage group**: PFI-ANA
|
||||||
|
- **Compose file**: `compose/dockge/compose.yaml`
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Host / Volume | Container | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for managing containers |
|
||||||
|
| `dockge_data` (named volume) | `/app/data` | Dockge application data |
|
||||||
|
| `/opt/docker/compose` | `/opt/docker/compose` | Compose stack directory |
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
- `DOCKGE_STACKS_DIR=/opt/docker/compose` — tells Dockge where to find/manage compose stacks
|
||||||
|
|
||||||
|
**Notes**: Dockge is the management interface for all other compose stacks on this host. It has full Docker daemon access via the socket mount.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. llama-swap — Multi-Model LLM Gateway
|
||||||
|
|
||||||
|
- **Image**: `ghcr.io/mostlygeek/llama-swap:cuda`
|
||||||
|
- **Port**: 9292 → 8080
|
||||||
|
- **Runtime**: `nvidia` (CUDA)
|
||||||
|
- **Compose file**: `compose/llama-swap/compose.yaml`
|
||||||
|
- **Config file**: `conf/llama-swap/config.yaml`
|
||||||
|
- **Interactive**: `stdin_open: true`, `tty: true` (required by llama-swap)
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Host Path | Container Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `/opt/docker/conf/llama-swap/config.yaml` | `/app/config.yaml` | llama-swap configuration (models, groups, params) |
|
||||||
|
| `/tank/aimodels/llm` | `/models` | LLM model files (GGUF format) |
|
||||||
|
|
||||||
|
**Configured Models** (from `config.yaml`):
|
||||||
|
|
||||||
|
| Model ID | Display Name | Quantization | Context Size | TTL (s) | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `qwen3-4b` | Qwen3-4B-Instruct-2507-Q6_K | Q6_K | default | 0 (persistent) | Small general-purpose model |
|
||||||
|
| `glm4.5-air` | GLM-4.5-Air Q4_K_M | Q4_K_M | 40,000 | 600 | Flash attention enabled |
|
||||||
|
| `skyfall-r1-31b-q6k` | Skyfall 31B v4 | Q6_K_L | 40,000 | 600 | Flash attention, full GPU offload |
|
||||||
|
| `GLM-Steam-106B-QK4M-A12B` | GLM-Steam 106B A12B | Q4_K_M | 40,000 | 600 | 2-shard model, MoE with 12B active |
|
||||||
|
| `kimik2-q2kxl` | Kimi K2 Instruct | UD-Q2_K_XL | default | 600 | 8-shard model, only 2 GPU layers (CPU-heavy) |
|
||||||
|
| `qwen3-coder-30b-iq4-nl` | Qwen3 Coder 30B A3B | IQ4_NL | 40,000 | 0 (persistent) | MoE 3B active, coding-optimized |
|
||||||
|
| `unsloth-granite-4-small` | Granite 4.0 Small | Q4_K_M | 120,000 | 0 (persistent) | IBM Granite, deterministic (temp=0) |
|
||||||
|
| `qwen3.5-35-a3b` | Qwen 3.5 35B A3B | UD-Q4_K_XL | 32,768 | 0 (persistent) | MoE, thinking mode, temp=1.0 |
|
||||||
|
| `qwen3.5-35-a3b-code` | Qwen 3.5 35B A3B Code | UD-Q4_K_XL | 32,768 | 0 (persistent) | Same model, code-tuned params (temp=0.6) |
|
||||||
|
| `gemma4-26b-a4b` | Gemma 4 26B A4B | UD-Q4_K_XL | 32,768 | 600 | MoE 4B active, thinking enabled, supports images |
|
||||||
|
| `gemma4-31b-dense` | Gemma 4 31B Dense | UD-Q4_K_XL | 32,768 | 600 | Full dense model, thinking enabled, supports images |
|
||||||
|
| `embeddinggemma-300M` | Embedding Gemma 300M | Q8_0 | 2,048 | 0 (persistent) | Embedding model, cls pooling |
|
||||||
|
| `qwen3-embedding-0.6B` | Qwen3 Embedding 0.6B | Q8_0 | 32,768 | 0 (persistent) | Embedding model, mean pooling |
|
||||||
|
| `jina-reranker-v3-0.6B` | Jina Reranker v3 | Q8_0 | 32,768 | 0 (persistent) | Reranking model |
|
||||||
|
| `bge-reranker-v2-m3-0.6B` | BGE Reranker v2 m3 | Q8_0 | 32,768 | 0 (persistent) | Reranking model |
|
||||||
|
|
||||||
|
**Model Groups**:
|
||||||
|
|
||||||
|
| Group | Swap | Exclusive | Persistent | Members |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `high-reasoning` | false | false | — | qwen3.5-35-a3b, qwen3.5-35-a3b-code, gemma4-31b-dense |
|
||||||
|
| `utility` | false | false | ✅ | embeddinggemma-300M, bge-reranker-v2-m3-0.6B |
|
||||||
|
|
||||||
|
**Global Settings**:
|
||||||
|
- `healthCheckTimeout`: 1200 seconds (20 minutes) — long timeout for large models
|
||||||
|
- `logLevel`: info
|
||||||
|
- `metricsMaxInMemory`: 1000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ComfyUI — Image Generation UI
|
||||||
|
|
||||||
|
- **Image**: `mmartial/comfyui-nvidia-docker:ubuntu24_cuda13.0-latest`
|
||||||
|
- **Port**: 8188 → 8188
|
||||||
|
- **Runtime**: `nvidia` with full device reservation (gpu, compute, utility capabilities)
|
||||||
|
- **Restart policy**: `unless-stopped`
|
||||||
|
- **Homepage group**: AI Systems
|
||||||
|
- **Compose file**: `compose/comfyui/compose.yaml`
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Host Path | Container Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `/tank/comfy/run` | `/comfy/mnt` | ComfyUI workspace / output directory |
|
||||||
|
| `/tank/aimodels/img/comfy` | `/basedir` | Image models and ComfyUI base directory |
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
| Variable | Value | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `WANTED_UID` | 1001 | Run as user ID 1001 |
|
||||||
|
| `WANTED_GID` | 1002 | Run as group ID 1002 |
|
||||||
|
| `BASE_DIRECTORY` | /basedir | ComfyUI base directory path |
|
||||||
|
| `SECURITY_LEVEL` | weak | Relaxed security (private network) |
|
||||||
|
| `NVIDIA_VISIBLE_DEVICES` | all | Expose all GPUs |
|
||||||
|
| `NVIDIA_DRIVER_CAPABILITIES` | all | Enable all GPU capabilities |
|
||||||
|
|
||||||
|
**Notes**: Runs with user-mapped permissions (UID 1001 / GID 1002). The `basedir` points to the image model storage on `/tank`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. VibeVoice — Voice/Audio AI Service
|
||||||
|
|
||||||
|
- **Image**: `eworkerinc/vibevoice:latest`
|
||||||
|
- **Container name**: `vibevoice`
|
||||||
|
- **Port**: 8745 → 8745
|
||||||
|
- **GPU**: Yes (all devices, gpu capability)
|
||||||
|
- **Restart policy**: `unless-stopped`
|
||||||
|
- **Homepage group**: AI Systems
|
||||||
|
- **Compose file**: `compose/vibevoice/compose.yaml`
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Host Path | Container Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `/tank/vibevoice/hf` | `/root/.cache/huggingface` | HuggingFace model cache |
|
||||||
|
| `/tank/vibevoice/voices` | `/app/voices` | Voice data / presets |
|
||||||
|
| `/tank/vibevoice/state` | `/var/lib/eworker` | Application state persistence |
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
| Variable | Value | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `ENABLE_1_5B` | true | Enable 1.5B parameter voice model |
|
||||||
|
| `ENABLE_LARGE` | true | Enable large voice model |
|
||||||
|
| `AUTH_REQUIRED` | true | Require authentication |
|
||||||
|
| `CORS_ENABLED` | true | Enable CORS headers |
|
||||||
|
| `ALLOWED_ORIGINS` | * | Allow all origins (development/private network) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Parakeet STT — Speech-to-Text Service
|
||||||
|
|
||||||
|
- **Image**: `parakeet-stt` (locally built)
|
||||||
|
- **Port**: 8300 → 8000
|
||||||
|
- **GPU**: Yes (all devices, gpu capability)
|
||||||
|
- **Restart policy**: `unless-stopped`
|
||||||
|
- **Homepage group**: AI Systems
|
||||||
|
- **Compose file**: `compose/parakeet/compose.yaml`
|
||||||
|
- **Env file**: `.env` (not tracked in project — likely contains API keys or model config)
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Volume | Container Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `parakeet_cache` (named volume) | `/root/.cache` | Model download cache |
|
||||||
|
|
||||||
|
**Notes**: Uses a locally-built image (no registry prefix). The `.env` file is referenced but not stored in the project — it likely contains environment-specific configuration on the Docker host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. ChromaDB — Vector Database
|
||||||
|
|
||||||
|
- **Image**: `chromadb/chroma:latest`
|
||||||
|
- **Container name**: `chromadb`
|
||||||
|
- **Port**: 8000 → 8000
|
||||||
|
- **GPU**: No
|
||||||
|
- **Restart policy**: `unless-stopped`
|
||||||
|
- **Homepage group**: AI Systems
|
||||||
|
- **Compose file**: `compose/chromadb/compose.yaml`
|
||||||
|
|
||||||
|
**Volumes**:
|
||||||
|
| Host Path | Container Path | Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/opt/docker/conf/chromadb` | `/conf` | read-only | Config directory (contains auth_token) |
|
||||||
|
| `/tank/chromadb` | `/data` | read-write | Persistent vector data |
|
||||||
|
|
||||||
|
**Environment**:
|
||||||
|
| Variable | Value | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `CHROMA_SERVER_AUTHN_CREDENTIALS_FILE` | `/conf/auth_token` | Path to auth token file inside container |
|
||||||
|
| `CHROMA_SERVER_AUTHN_PROVIDER` | `chromadb.server.auth.token.TokenAuthenticationServerProvider` | Enable token-based authentication |
|
||||||
|
| `IS_PERSISTENT` | TRUE | Enable persistent storage |
|
||||||
|
| `PERSIST_DIRECTORY` | `/data` | Where vector data is stored inside container |
|
||||||
|
| `ANONYMIZED_TELEMETRY` | FALSE | Disable telemetry |
|
||||||
|
|
||||||
|
**Health Check**:
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Test | `curl -f http://localhost:8000/api/v1/health` |
|
||||||
|
| Interval | 30s |
|
||||||
|
| Timeout | 10s |
|
||||||
|
| Retries | 3 |
|
||||||
|
| Start period | 40s |
|
||||||
|
|
||||||
|
**Authentication**: Token-based. The auth token is stored at `/opt/docker/conf/chromadb/auth_token` on the host (mode 600), generated with `openssl rand -hex 32`. Clients must supply this token to access the API.
|
||||||
|
|
||||||
|
**Notes**: CPU-only service (no GPU). The compose-examples directory contains a reference compose file with Traefik labels and a demo app — see [chromadb-setup.md](chromadb-setup.md) for full deployment instructions and the demo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastructure Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ PFI-ANA (10.250.50.70) │
|
||||||
|
│ Docker Host (PFI-ANA_DOCKER) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ Manages all compose stacks │
|
||||||
|
│ │ Dockge │◄─── /opt/docker/compose/* │
|
||||||
|
│ │ :5001 │ /var/run/docker.sock │
|
||||||
|
│ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ GPU via passthrough │
|
||||||
|
│ │ llama-swap :9292 (CUDA, multi-model gateway) │ │
|
||||||
|
│ │ ├── 15 models (chat, code, embedding, reranker) │ │
|
||||||
|
│ │ ├── 2 groups (high-reasoning, utility) │ │
|
||||||
|
│ │ └── Models from /tank/aimodels/llm │ │
|
||||||
|
│ └──────────────────┘ GPU via passthrough │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ GPU via │
|
||||||
|
│ │ ComfyUI │ │ VibeVoice │ │ Parakeet │ passthrough │
|
||||||
|
│ │ :8188 │ │ :8745 │ │ :8300 │ │
|
||||||
|
│ │ (GPU, img) │ │ (GPU, voice)│ │ (GPU, STT) │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ │
|
||||||
|
│ │ ChromaDB │ Token auth, persistent vectors │
|
||||||
|
│ │ :8000 │ /tank/chromadb (data) │
|
||||||
|
│ │ (CPU only) │ /opt/docker/conf/chromadb (config) │
|
||||||
|
│ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── All services on traefik-net (external) ── │
|
||||||
|
│ ── /tank/* = persistent large data storage ── │
|
||||||
|
│ ── /opt/docker/* = config + compose files ── │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Allocation
|
||||||
|
|
||||||
|
| Port | Service | Protocol |
|
||||||
|
|---|---|---|
|
||||||
|
| 5001 | Dockge | HTTP |
|
||||||
|
| 8000 | ChromaDB | HTTP |
|
||||||
|
| 8188 | ComfyUI | HTTP |
|
||||||
|
| 8300 | Parakeet STT | HTTP (→ container 8000) |
|
||||||
|
| 8745 | VibeVoice | HTTP |
|
||||||
|
| 9292 | llama-swap | HTTP (→ container 8080) |
|
||||||
|
|
||||||
|
## Example Configurations
|
||||||
|
|
||||||
|
Reference/example compose files (not live) are stored in `configs/pfi-ana/docker/compose-examples/`:
|
||||||
|
- `compose-examples/llama-swap/docker-compose.yml` — earlier llama-swap reference
|
||||||
|
- `compose-examples/chromadb/` — ChromaDB with Traefik labels, auth, health checks, and demo app
|
||||||
|
|
||||||
|
Full ChromaDB setup instructions: [chromadb-setup.md](chromadb-setup.md)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# PFI-ANA Model Inventory
|
||||||
|
# Generated from: find /models -maxdepth 3 -name "*.gguf" | sort
|
||||||
|
# Synchronized with llama-swap config.yaml on 2025-07-18
|
||||||
|
#
|
||||||
|
# Status Key:
|
||||||
|
# [ACTIVE] = configured in llama-swap
|
||||||
|
# [PENDING] = on disk, not yet in config (or still downloading)
|
||||||
|
# [REMOVED] = was in old config, not on disk
|
||||||
|
#
|
||||||
|
# ============================================================================
|
||||||
|
# KB Optimization Pass (2025-07-18):
|
||||||
|
# - Qwen 3.5: added presence_penalty, thinking mode enable_thinking
|
||||||
|
# - Qwen3-Coder-Next: temp 0.3→1.0, top-p 0.85→0.95, top-k 20→40, min-p 0→0.01
|
||||||
|
# - Gemma 4: added repeat_penalty 1.0, thinking mode enable_thinking
|
||||||
|
# - Nemotron: already applied in prior pass (--special, min-p 0.01, seed 3407)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
[ACTIVE] ./ggml-org_Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_Qwen3.5-122B-A10B-GGUF/UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-00001-of-00003.gguf
|
||||||
|
[ACTIVE] ./unsloth_Qwen3.5-122B-A10B-GGUF/UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-00002-of-00003.gguf
|
||||||
|
[ACTIVE] ./unsloth_Qwen3.5-122B-A10B-GGUF/UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-00003-of-00003.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./ibm-granite_granite-4.0-micro-GGUF/granite-4.0-micro-Q4_K_M.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_GLM-4.7-Flash-GGUF/GLM-4.7-Flash-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./jinaai_jina-reranker-v3-GGUF/jina-reranker-v3-Q8_0.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_NVIDIA-Nemotron-3-Super-120B-A12B-GGUF/UD-Q4_K_XL/NVIDIA-Nemotron-3-Super-120B-A12B-UD-Q4_K_XL-00001-of-00003.gguf
|
||||||
|
[ACTIVE] ./unsloth_NVIDIA-Nemotron-3-Super-120B-A12B-GGUF/UD-Q4_K_XL/NVIDIA-Nemotron-3-Super-120B-A12B-UD-Q4_K_XL-00002-of-00003.gguf
|
||||||
|
[ACTIVE] ./unsloth_NVIDIA-Nemotron-3-Super-120B-A12B-GGUF/UD-Q4_K_XL/NVIDIA-Nemotron-3-Super-120B-A12B-UD-Q4_K_XL-00003-of-00003.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./bartowski_TheDrummer_Skyfall-31B-v4-GGUF/TheDrummer_Skyfall-31B-v4-Q6_K_L.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./RP/BeaverAI_Skyfall-R1-31B-v4a-GGUF/Skyfall-R1-31B-v4a-Q6_K.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./RP/bartowski_TheDrummer_GLM-Steam-106B-A12B-v1-GGUF/TheDrummer_GLM-Steam-106B-A12B-v1-Q4_K_M-00001-of-00002.gguf
|
||||||
|
[ACTIVE] ./RP/bartowski_TheDrummer_GLM-Steam-106B-A12B-v1-GGUF/TheDrummer_GLM-Steam-106B-A12B-v1-Q4_K_M-00002-of-00002.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_Qwen3.5-9B-GGUF/Qwen3.5-9B-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_granite-4.0-h-small-GGUF/granite-4.0-h-small-Q4_K_M.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00001-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00002-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00003-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00004-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00005-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00006-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00007-of-00008.gguf
|
||||||
|
[ACTIVE] ./unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00008-of-00008.gguf
|
||||||
|
|
||||||
|
[PENDING] ./unsloth_Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-UD-Q4_K_XL.gguf
|
||||||
|
NOTE: Download may be incomplete (has .incomplete files). Config entry ready.
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_Nemotron-3-Nano-30B-A3B-GGUF/Nemotron-3-Nano-30B-A3B-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_Qwen3.5-35B-A3B-GGUF/Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./Qwen_Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./unsloth_gemma-4-31B-it-GGUF/gemma-4-31B-it-UD-Q4_K_XL.gguf
|
||||||
|
|
||||||
|
[ACTIVE] ./ggml-org_embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MODELS REMOVED FROM CONFIG (not on disk or superseded):
|
||||||
|
# ============================================================================
|
||||||
|
# [REMOVED] qwen3-4b — Qwen3-4B-Instruct-2507-Q6_K (not on disk)
|
||||||
|
# [REMOVED] glm4.5-air — GLM-4.5-Air-Q4_K_M (not on disk, superseded by glm4.7-flash)
|
||||||
|
# [REMOVED] qwen3-coder-30b — Qwen3-Coder-30B-A3B (not on disk, superseded by qwen3-coder-next)
|
||||||
|
# [REMOVED] bge-reranker-v2-m3 — Replaced by qwen3-reranker-0.6B
|
||||||
|
# [FIXED] glm-steam-106b — Path fixed: was missing RP/ prefix
|
||||||
|
# ============================================================================
|
||||||
|
# STILL DOWNLOADING (not yet configured):
|
||||||
|
# ============================================================================
|
||||||
|
# [PENDING] Hermes-4-14B — Multiple quant downloads in progress (.incomplete files)
|
||||||
|
# ============================================================================
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# PFI-ANA Proxmox VM Inventory
|
||||||
|
|
||||||
|
**Hypervisor**: Proxmox VE at `10.250.250.31:8006`
|
||||||
|
**Storage Pool**: `ospool` (CEPH/zfs — all VM disks reside here)
|
||||||
|
**Network Bridge**: `vmbr0` with VLAN tag `50` on all VMs
|
||||||
|
**QEMU Version**: 7.2.0 (primary), VM 106 on 8.1.5
|
||||||
|
|
||||||
|
## Virtual Machines
|
||||||
|
|
||||||
|
### VM 100 — PFI-ANA-TRUENAS
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 100 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 2 cores = 4 vCPU (host passthrough) |
|
||||||
|
| **Memory** | 8,196 MB |
|
||||||
|
| **Disk** | `scsi0`: 80G on ospool |
|
||||||
|
| **CDROM** | `ide2`: TrueNAS-SCALE-22.12.1.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `9A:90:79:7A:86:87`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 2, delay 120s |
|
||||||
|
| **Onboot** | No |
|
||||||
|
|
||||||
|
**Purpose**: TrueNAS SCALE storage appliance. Provides NAS/NFS/iSCSI to the colo environment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 101 — PFI-ANA-DC
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 101 |
|
||||||
|
| **OS Type** | Windows 11 |
|
||||||
|
| **BIOS** | OVMF (UEFI) with TPM 2.0 |
|
||||||
|
| **Machine** | pc-q35-7.2 |
|
||||||
|
| **CPU** | 2 sockets × 6 cores = 12 vCPU (host passthrough) |
|
||||||
|
| **Memory** | 24,576 MB |
|
||||||
|
| **Disk** | `scsi0`: 240G on ospool |
|
||||||
|
| **EFI Disk** | `efidisk0`: 1M on ospool |
|
||||||
|
| **TPM** | `tpmstate0`: 4M, v2.0 on ospool |
|
||||||
|
| **CDROM** | `scsi1`: virtio-win-0.1.229.iso (VirtIO drivers) |
|
||||||
|
| **Network** | `net0`: e1000, MAC `CE:C8:D7:FE:32:40`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → net0 → ide0 → scsi1 |
|
||||||
|
| **Startup** | Order 5, delay 120s |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Windows Domain Controller for the Anaheim environment. UEFI with TPM 2.0 suggests Active Directory / Group Policy services.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 102 — PFI-ANA-Docker
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 102 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU (x86-64-v2-AES) |
|
||||||
|
| **Memory** | 16,384 MB |
|
||||||
|
| **Disk** | `scsi0`: 250G on ospool |
|
||||||
|
| **Network** | `net0`: virtio, MAC `BA:AF:E7:E9:79:23`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → net0 → scsi1 |
|
||||||
|
| **Startup** | Order 4 |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Primary Docker host for the colo. Runs Dockge for compose management and Traefik for reverse proxy. All Docker services documented in [docker-stack.md](docker-stack.md) run here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 103 — PFI-SlaveBot
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 103 |
|
||||||
|
| **OS Type** | Windows 10 |
|
||||||
|
| **Machine** | pc-i440fx-7.2 |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU (host passthrough) |
|
||||||
|
| **Memory** | 8,192 MB |
|
||||||
|
| **Disk** | `ide0`: 256G on ospool |
|
||||||
|
| **Network** | `net0`: e1000, MAC `CE:F0:49:C9:03:70`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | ide0 → net0 → scsi0 |
|
||||||
|
| **Startup** | Not configured |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Windows 10 workstation/bot. Likely a task automation or RDP-accessible machine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 104 — PFI-Mongo
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 104 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU (host passthrough) |
|
||||||
|
| **Memory** | 8,196 MB |
|
||||||
|
| **Disk** | `scsi0`: 256G on ospool |
|
||||||
|
| **Network** | `net0`: virtio, MAC `32:57:90:B2:66:61`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 3, delay 60s |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: MongoDB server. Config file contains a connection string reference: `mongodb://10.250.50.81:27017/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 105 — PFI-Postgres
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 105 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 4 sockets × 4 cores = 16 vCPU |
|
||||||
|
| **Memory** | 8,196 MB |
|
||||||
|
| **Disk** | `scsi0`: 80G on ospool |
|
||||||
|
| **CDROM** | `ide2`: debian-11.6.0-amd64-netinst.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `C2:1F:CC:71:66:D0`, vmbr0, VLAN 50 |
|
||||||
|
| **Serial** | `serial0`: socket (IPMI/serial console) |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 3, delay 60s |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: PostgreSQL database server running Debian 11.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 106 — PFI-Tailscale
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 106 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU (x86-64-v2-AES) |
|
||||||
|
| **Memory** | 2,048 MB |
|
||||||
|
| **Disk** | `scsi0`: 256G on ospool |
|
||||||
|
| **CDROM** | `ide2`: debian-12.2.0-amd64-netinst.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `BC:24:11:D7:E9:52`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Not configured |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Tailscale VPN node for mesh connectivity. Provides the VPN tunnel endpoints that link the three PFI sites together. Running Debian 12 (newer than most other VMs). Lightweight at 2G RAM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 107 — PFI-Pteradactyl
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 107 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU (host passthrough) |
|
||||||
|
| **Memory** | 8,192 MB |
|
||||||
|
| **Disk** | `scsi0`: 256G on ospool |
|
||||||
|
| **CDROM** | `ide2`: debian-11.6.0-amd64-netinst.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `CA:44:37:8A:BF:E0`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Not configured |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Pterodactyl game server panel. Manages game server instances.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 108 — PFI-ANA--DEV
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 108 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 2 sockets × 4 cores = 8 vCPU |
|
||||||
|
| **Memory** | 8,192 MB |
|
||||||
|
| **Disk** | `scsi0`: 120G on ospool |
|
||||||
|
| **CDROM** | `ide2`: debian-11.6.0-amd64-netinst.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `F2:EF:82:2C:AF:90`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 10, delay 60s |
|
||||||
|
| **Onboot** | No |
|
||||||
|
|
||||||
|
**Purpose**: Development environment. Not set to auto-boot, starts after core infrastructure (order 10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 110 — PFI-ANA-Webhost
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 110 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 4 sockets × 4 cores = 16 vCPU |
|
||||||
|
| **Memory** | 4,096 MB (balloon: 1024 MB minimum) |
|
||||||
|
| **Disk** | `scsi0`: 250G on ospool |
|
||||||
|
| **CDROM** | `ide2`: debian-11.6.0-amd64-netinst.iso |
|
||||||
|
| **Network** | `net0`: virtio, MAC `E6:F9:3A:C9:61:2A`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 30, up 120s, down 120s |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Web hosting server. Highest startup order (30) — starts last. Also has the longest graceful shutdown timeout (120s). Memory ballooning enabled for dynamic allocation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VM 111 — pfi-tacticalrmm
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **VMID** | 111 |
|
||||||
|
| **OS Type** | Linux (l26) |
|
||||||
|
| **CPU** | 4 sockets × 4 cores = 16 vCPU |
|
||||||
|
| **Memory** | 8,192 MB |
|
||||||
|
| **Disk** | `scsi0`: 256G on ospool |
|
||||||
|
| **Network** | `net0`: virtio, MAC `BA:FA:65:F6:46:25`, vmbr0, VLAN 50 |
|
||||||
|
| **Boot** | scsi0 → ide2 → net0 |
|
||||||
|
| **Startup** | Order 20 |
|
||||||
|
| **Onboot** | Yes |
|
||||||
|
|
||||||
|
**Purpose**: Tactical RMM (Remote Monitoring and Management) server. Provides IT management, remote access, and monitoring capabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Startup Order Summary
|
||||||
|
|
||||||
|
VMs are brought up in the following order on host boot:
|
||||||
|
|
||||||
|
| Order | VMID | Name | Delay |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2 | 100 | PFI-ANA-TRUENAS | 120s |
|
||||||
|
| 3 | 104 | PFI-Mongo | 60s |
|
||||||
|
| 3 | 105 | PFI-Postgres | 60s |
|
||||||
|
| 4 | 102 | PFI-ANA-Docker | — |
|
||||||
|
| 5 | 101 | PFI-ANA-DC | 120s |
|
||||||
|
| 10 | 108 | PFI-ANA--DEV | 60s |
|
||||||
|
| 20 | 111 | pfi-tacticalrmm | — |
|
||||||
|
| 30 | 110 | PFI-ANA-Webhost | 120s |
|
||||||
|
|
||||||
|
VMs without a startup order (103, 106, 107) will start based on their `onboot` setting but without a specific sequencing delay.
|
||||||
|
|
||||||
|
## Resource Summary
|
||||||
|
|
||||||
|
| VMID | Name | vCPU | RAM (MB) | Disk | OS |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 100 | PFI-ANA-TRUENAS | 4 | 8,196 | 80G | TrueNAS SCALE |
|
||||||
|
| 101 | PFI-ANA-DC | 12 | 24,576 | 240G | Windows 11 |
|
||||||
|
| 102 | PFI-ANA-Docker | 8 | 16,384 | 250G | Linux |
|
||||||
|
| 103 | PFI-SlaveBot | 8 | 8,192 | 256G | Windows 10 |
|
||||||
|
| 104 | PFI-Mongo | 8 | 8,196 | 256G | Linux |
|
||||||
|
| 105 | PFI-Postgres | 16 | 8,196 | 80G | Debian 11 |
|
||||||
|
| 106 | PFI-Tailscale | 8 | 2,048 | 256G | Debian 12 |
|
||||||
|
| 107 | PFI-Pteradactyl | 8 | 8,192 | 256G | Debian 11 |
|
||||||
|
| 108 | PFI-ANA--DEV | 8 | 8,192 | 120G | Debian 11 |
|
||||||
|
| 110 | PFI-ANA-Webhost | 16 | 4,096 | 250G | Debian 11 |
|
||||||
|
| 111 | pfi-tacticalrmm | 16 | 8,192 | 256G | Linux |
|
||||||
|
| | **Totals** | **112** | **105,348** | **2,300G** | |
|
||||||
|
|
||||||
|
## Network Notes
|
||||||
|
|
||||||
|
- All VMs are on **VLAN 50** via `vmbr0`.
|
||||||
|
- All VMs have **firewall enabled** on the network interface.
|
||||||
|
- Linux VMs use `virtio` network adapters; Windows VMs use `e1000`.
|
||||||
|
- The MongoDB connection string embedded in VM 104's config references IP `10.250.50.81`, suggesting VLAN 50 maps to the `10.250.50.0/24` subnet within the `10.250.0.0/16` range.
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
# Recommended Model Inference Settings — Reference Document
|
||||||
|
|
||||||
|
> **Source:** AIPA Knowledge Base — compiled from 5 KB reference documents.
|
||||||
|
> **Last Updated:** 2025-07-14
|
||||||
|
> **Purpose:** Canonical reference for llama-server / llama.cpp inference parameters across all model families with KB-documented settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [NVIDIA Nemotron 3 Super (120B-A12B)](#1-nvidia-nemotron-3-super-120b-a12b)
|
||||||
|
2. [NVIDIA Nemotron 3 Nano (4B / 30B-A3B)](#2-nvidia-nemotron-3-nano-4b--30b-a3b)
|
||||||
|
3. [Qwen 3.5 Family (0.8B – 397B-A17B)](#3-qwen-35-family-08b--397b-a17b)
|
||||||
|
4. [Qwen3-Coder-Next (80B MoE)](#4-qwen3-coder-next-80b-moe)
|
||||||
|
5. [Google Gemma 4 Family (E2B – 31B)](#5-google-gemma-4-family-e2b--31b)
|
||||||
|
6. [Quick Reference Cards](#6-quick-reference-cards)
|
||||||
|
7. [Critical Warnings by Model](#7-critical-warnings-by-model)
|
||||||
|
8. [Models Without KB Settings](#8-models-without-kb-settings)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. NVIDIA Nemotron 3 Super (120B-A12B)
|
||||||
|
|
||||||
|
### Model Overview
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Architecture | MoE — 120B total, **12B active parameters** |
|
||||||
|
| Max Context | **1,048,576** (1M tokens) |
|
||||||
|
| Recommended Starting Context | **16K or 32K** — increase gradually |
|
||||||
|
| Reasoning Tokens | `<think)>` (ID 12), `</think)>` (ID 13) |
|
||||||
|
| Positional Embeddings | **NoPE** — YaRN NOT needed |
|
||||||
|
| Best For | Multi-agent AI, high-efficiency reasoning, coding, math |
|
||||||
|
| Performance Tier | ~GPT-5.2 / Claude Opus 4.5 level |
|
||||||
|
|
||||||
|
### Inference Parameters
|
||||||
|
|
||||||
|
| Parameter | General Chat / Instruction | Tool Calling |
|
||||||
|
|---|---|---|
|
||||||
|
| `temperature` | **1.0** | **0.6** |
|
||||||
|
| `top_p` | **1.0** | **0.95** |
|
||||||
|
| `min_p` | **0.01** | **0.01** |
|
||||||
|
|
||||||
|
### Additional Settings
|
||||||
|
|
||||||
|
| Setting | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `--seed` | **3407** | Reproducibility |
|
||||||
|
| `--prio` | **2** or **3** | Priority scheduling |
|
||||||
|
| `--special` | Required | To see reasoning tokens |
|
||||||
|
| `--verbose-prompt` | Required | To see prepended `<think)>` tokens |
|
||||||
|
| `max_new_tokens` | 32,768 – 262,144 | Up to 1M |
|
||||||
|
|
||||||
|
### Quantization & Memory
|
||||||
|
|
||||||
|
| Precision | Memory Required |
|
||||||
|
|---|---|
|
||||||
|
| UD-Q2_K_XL (2-bit) | ~32–36 GB |
|
||||||
|
| **UD-Q4_K_XL (4-bit)** | **~64–72 GB** |
|
||||||
|
| 8-bit | ~128 GB |
|
||||||
|
| BF16 | ~240 GB |
|
||||||
|
|
||||||
|
### Example Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
--model Nemotron-3-Super-UD-Q4_K_XL.gguf \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--temp 1.0 --top-p 1.0 --min-p 0.01 \
|
||||||
|
--seed 3407 --special \
|
||||||
|
--flash-attn on \
|
||||||
|
--port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. NVIDIA Nemotron 3 Nano (4B / 30B-A3B)
|
||||||
|
|
||||||
|
### Model Variants
|
||||||
|
|
||||||
|
| Variant | Architecture | Context | Active Params | Best Fit |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Nano-4B** | Dense | 128K | 4B | Lightweight coding, math, agentic tasks |
|
||||||
|
| **Nano-30B-A3B** | MoE | 128K | 3B active | Best performance/size on 24GB devices |
|
||||||
|
|
||||||
|
### Inference Parameters (Both Variants)
|
||||||
|
|
||||||
|
| Parameter | General Chat / Instruction | Tool Calling |
|
||||||
|
|---|---|---|
|
||||||
|
| `temperature` | **1.0** | **0.6** |
|
||||||
|
| `top_p` | **1.0** | **0.95** |
|
||||||
|
| `min_p` | **0.01** | **0.01** |
|
||||||
|
|
||||||
|
### Additional Settings
|
||||||
|
|
||||||
|
| Setting | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `--seed` | **3407** | Reproducibility |
|
||||||
|
| `--special` | Not required | Standard chat template for Nano variants |
|
||||||
|
|
||||||
|
### Hardware & Quantization
|
||||||
|
|
||||||
|
#### Nano-4B
|
||||||
|
|
||||||
|
| Precision | Memory |
|
||||||
|
|---|---|
|
||||||
|
| Q8_0 (8-bit, recommended) | ~3 GB |
|
||||||
|
| 4-bit | ~5 GB |
|
||||||
|
|
||||||
|
#### Nano-30B-A3B
|
||||||
|
|
||||||
|
| Precision | Memory |
|
||||||
|
|---|---|
|
||||||
|
| **UD-Q4_K_XL (4-bit, recommended)** | **~24 GB** |
|
||||||
|
| 8-bit | ~36 GB |
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nano-4B (8-bit)
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/Nemotron-3-Nano-4B-GGUF:Q8_0 \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--temp 1.0 --top-p 1.0 --min-p 0.01 \
|
||||||
|
--seed 3407 --flash-attn on --port 8001
|
||||||
|
|
||||||
|
# Nano-30B-A3B (4-bit)
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/Nemotron-3-Nano-30B-A3B-GGUF:UD-Q4_K_XL \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--temp 1.0 --top-p 1.0 --min-p 0.01 \
|
||||||
|
--seed 3407 --flash-attn on --port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Qwen 3.5 Family (0.8B – 397B-A17B)
|
||||||
|
|
||||||
|
### Model Variants
|
||||||
|
|
||||||
|
| Variant | Architecture | Context | Languages | Best Fit |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **0.8B** | Dense | 256K | 201 | Smallest edge inference |
|
||||||
|
| **2B** | Dense | 256K | 201 | Small device inference |
|
||||||
|
| **4B** | Dense | 256K | 201 | Lightweight local use |
|
||||||
|
| **9B** | Dense | 256K | 201 | Capable small model |
|
||||||
|
| **27B** | Dense | 256K | 201 | Slightly more accurate than 35B-A3B; fits 18GB |
|
||||||
|
| **35B-A3B** | MoE (3B active) | 256K | 201 | Best speed/quality tradeoff; fits 22GB |
|
||||||
|
| **122B-A10B** | MoE (10B active) | 256K | 201 | High quality; needs ~70GB (4-bit) |
|
||||||
|
| **397B-A17B** | MoE (17B active) | 256K (extendable to 1M via YaRN) | 201 | Top-tier performance |
|
||||||
|
|
||||||
|
### 27B vs 35B-A3B Decision
|
||||||
|
|
||||||
|
- **27B** — Choose for slightly more accurate results when you can't fit a larger model.
|
||||||
|
- **35B-A3B** — Choose for much faster inference. MoE with only 3B active parameters.
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
|
||||||
|
| Variant | 3-bit | 4-bit | 6-bit | 8-bit | BF16 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **0.8B / 2B** | 3 GB | 3.5 GB | 5 GB | 7.5 GB | 9 GB |
|
||||||
|
| **4B** | 4.5 GB | 5.5 GB | 7 GB | 10 GB | 14 GB |
|
||||||
|
| **9B** | 5.5 GB | 6.5 GB | 9 GB | 13 GB | 19 GB |
|
||||||
|
| **27B** | 14 GB | 17 GB | 24 GB | 30 GB | 54 GB |
|
||||||
|
| **35B-A3B** | 17 GB | 22 GB | 30 GB | 38 GB | 70 GB |
|
||||||
|
| **122B-A10B** | 60 GB | 70 GB | 106 GB | 132 GB | 245 GB |
|
||||||
|
| **397B-A17B** | 180 GB | 214 GB | 340 GB | 512 GB | 810 GB |
|
||||||
|
|
||||||
|
### Inference Parameters
|
||||||
|
|
||||||
|
#### Thinking Mode
|
||||||
|
|
||||||
|
| Parameter | General Tasks | Precise Coding (e.g. WebDev) |
|
||||||
|
|---|---|---|
|
||||||
|
| `temperature` | **1.0** | **0.6** |
|
||||||
|
| `top_p` | **0.95** | **0.95** |
|
||||||
|
| `top_k` | **20** | **20** |
|
||||||
|
| `min_p` | **0.0** | **0.0** |
|
||||||
|
| `presence_penalty` | **1.5** | **0.0** |
|
||||||
|
| `repetition_penalty` | **1.0** (disabled) | **1.0** (disabled) |
|
||||||
|
|
||||||
|
#### Non-Thinking (Instruct) Mode
|
||||||
|
|
||||||
|
| Parameter | General Tasks | Reasoning Tasks |
|
||||||
|
|---|---|---|
|
||||||
|
| `temperature` | **0.7** | **1.0** |
|
||||||
|
| `top_p` | **0.8** | **0.95** |
|
||||||
|
| `top_k` | **20** | **20** |
|
||||||
|
| `min_p` | **0.0** | **0.0** |
|
||||||
|
| `presence_penalty` | **1.5** | **1.5** |
|
||||||
|
| `repetition_penalty` | **1.0** (disabled) | **1.0** (disabled) |
|
||||||
|
|
||||||
|
### Thinking Mode Control
|
||||||
|
|
||||||
|
Enable thinking:
|
||||||
|
```bash
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable thinking:
|
||||||
|
```bash
|
||||||
|
--chat-template-kwargs '{"enable_thinking":false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Default Thinking Behavior by Variant
|
||||||
|
|
||||||
|
| Variant | Thinking Default |
|
||||||
|
|---|---|
|
||||||
|
| **0.8B, 2B, 4B, 9B** (Small) | **Disabled** — must explicitly enable |
|
||||||
|
| **27B, 35B-A3B, 122B-A10B, 397B-A17B** | **Enabled** — must explicitly disable if unwanted |
|
||||||
|
|
||||||
|
### Context Settings
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Max context window | **262,144** (256K) |
|
||||||
|
| Context extension | Up to **1M** via YaRN |
|
||||||
|
| Recommended starting context | **16,384** (16K) for responsiveness |
|
||||||
|
| Adequate output length | **32,768** tokens |
|
||||||
|
|
||||||
|
### Quantization Notes
|
||||||
|
|
||||||
|
- All GGUFs use **Unsloth Dynamic 2.0** quantization — important layers upcasted to 8 or 16-bit even in 4-bit.
|
||||||
|
- Recommended starting point: **Dynamic 4-bit** (`UD-Q4_K_XL`).
|
||||||
|
- Minimum recommended: **Dynamic 2-bit** (`UD-Q2_K_XL`).
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 35B-A3B — Thinking Mode (General)
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/Qwen3.5-35B-A3B-GGUF:UD-Q4_K_XL \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.00 \
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}' \
|
||||||
|
--flash-attn on --port 8001
|
||||||
|
|
||||||
|
# 9B — Thinking Enabled (small models default to disabled)
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/Qwen3.5-9B-GGUF:UD-Q4_K_XL \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.00 \
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}' \
|
||||||
|
--flash-attn on --port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Qwen3-Coder-Next (80B MoE)
|
||||||
|
|
||||||
|
### Model Overview
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Architecture | MoE — 80B total, **3B active parameters** |
|
||||||
|
| Max Context | **262,144** (256K) |
|
||||||
|
| Recommended Context | **32,768** for less memory use |
|
||||||
|
| Thinking Mode | **Non-reasoning only** — no `<think)>` blocks |
|
||||||
|
| Best For | Fast agentic coding, long-horizon reasoning, complex tool use |
|
||||||
|
| Performance Tier | Comparable to models with 10–20× more active parameters |
|
||||||
|
|
||||||
|
### Inference Parameters
|
||||||
|
|
||||||
|
| Parameter | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `temperature` | **1.0** | |
|
||||||
|
| `top_p` | **0.95** | |
|
||||||
|
| `top_k` | **40** | Note: higher than Qwen3.5 general |
|
||||||
|
| `min_p` | **0.01** | llama.cpp default is 0.05 — override to 0.01 |
|
||||||
|
| `repetition_penalty` | **1.0** (disabled) | Only increase if you see looping |
|
||||||
|
|
||||||
|
### Quick Reference One-Liner
|
||||||
|
|
||||||
|
```
|
||||||
|
temperature=1.0, top_p=0.95, top_k=40, min_p=0.01, repetition_penalty=1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
|
||||||
|
| Precision | Memory Required |
|
||||||
|
|---|---|
|
||||||
|
| 3-bit (UD-IQ3_XXS) | ~34 GB |
|
||||||
|
| **4-bit (UD-Q4_K_XL)** | **~46 GB** |
|
||||||
|
| 8-bit | ~85 GB |
|
||||||
|
| BF16 | ~160 GB |
|
||||||
|
|
||||||
|
### Key Differences from Qwen3.5 General Models
|
||||||
|
|
||||||
|
- **No thinking mode** — this is a non-reasoning model; ultra-quick code responses
|
||||||
|
- **Higher `top_k`** (40 vs. 20) — broader sampling for creative code generation
|
||||||
|
- `enable_thinking` flag is not applicable
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# llama-server deployment
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
--model Qwen3-Coder-Next-UD-Q4_K_XL.gguf \
|
||||||
|
--alias "unsloth/Qwen3-Coder-Next" \
|
||||||
|
--seed 3407 \
|
||||||
|
--temp 1.0 --top-p 0.95 --min-p 0.01 --top-k 40 \
|
||||||
|
--ctx-size 32768 \
|
||||||
|
--flash-attn on --port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
### vLLM FP8 Dynamic (GPU Premium)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CUDA_VISIBLE_DEVICES='0,1,2,3' vllm serve unsloth/Qwen3-Coder-Next-FP8-Dynamic \
|
||||||
|
--served-model-name unsloth/Qwen3-Coder-Next \
|
||||||
|
--tensor-parallel-size 4 \
|
||||||
|
--tool-call-parser qwen3_coder \
|
||||||
|
--enable-auto-tool-choice \
|
||||||
|
--dtype bfloat16 --seed 3407 \
|
||||||
|
--max-model-len 200000 \
|
||||||
|
--gpu-memory-utilization 0.93 \
|
||||||
|
--port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Google Gemma 4 Family (E2B – 31B)
|
||||||
|
|
||||||
|
### Model Variants
|
||||||
|
|
||||||
|
| Variant | Architecture | Context | Modalities | Best Fit |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **E2B** | Dense + PLE | 128K | Text, Image, Audio | Phone / edge, ASR, speech translation |
|
||||||
|
| **E4B** | Dense + PLE | 128K | Text, Image, Audio | Laptops, fast local multimodal |
|
||||||
|
| **26B-A4B** | MoE (4B active) | 256K | Text, Image | Best speed/quality tradeoff |
|
||||||
|
| **31B** | Dense | 256K | Text, Image | Strongest performance |
|
||||||
|
|
||||||
|
### Inference Parameters (All Variants)
|
||||||
|
|
||||||
|
These are Google's default Gemma 4 parameters:
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|---|---|
|
||||||
|
| `temperature` | **1.0** |
|
||||||
|
| `top_p` | **0.95** |
|
||||||
|
| `top_k` | **64** |
|
||||||
|
| `repetition_penalty` | **1.0** (disabled — only increase if looping) |
|
||||||
|
| End-of-sentence token | `<turn|>` |
|
||||||
|
|
||||||
|
### Context Length
|
||||||
|
|
||||||
|
- **E2B / E4B**: max **128K**
|
||||||
|
- **26B-A4B / 31B**: max **256K**
|
||||||
|
- **Practical tip**: Start with **32K** for responsiveness, then increase as needed.
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
|
||||||
|
| Variant | 4-bit | 8-bit | BF16/FP16 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **E2B** | 4 GB | 5–8 GB | 10 GB |
|
||||||
|
| **E4B** | 5.5–6 GB | 9–12 GB | 16 GB |
|
||||||
|
| **26B-A4B** | 16–18 GB | 28–30 GB | 52 GB |
|
||||||
|
| **31B** | 17–20 GB | 34–38 GB | 62 GB |
|
||||||
|
|
||||||
|
### Quantization Recommendations
|
||||||
|
|
||||||
|
- **E2B / E4B** (small): prefer **Q8_0** (8-bit) for quality.
|
||||||
|
- **26B-A4B / 31B** (large): prefer **UD-Q4_K_XL** (Dynamic 4-bit) as starting point.
|
||||||
|
|
||||||
|
### 26B-A4B vs 31B Decision
|
||||||
|
|
||||||
|
- **26B-A4B** — Choose when RAM is limited. MoE with 4B active params = faster. Slight quality tradeoff.
|
||||||
|
- **31B** — Choose when you have ≥20 GB (4-bit) and want maximum quality. Slower inference.
|
||||||
|
|
||||||
|
### Thinking Mode
|
||||||
|
|
||||||
|
**Enable thinking** — add to system prompt:
|
||||||
|
```
|
||||||
|
<|think|>
|
||||||
|
You are a careful coding assistant. Explain your answer clearly.
|
||||||
|
```
|
||||||
|
|
||||||
|
Model outputs:
|
||||||
|
```
|
||||||
|
<|channel>thought
|
||||||
|
[internal reasoning]
|
||||||
|
<channel|>
|
||||||
|
[final answer]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Disable thinking** via llama-server:
|
||||||
|
```bash
|
||||||
|
--chat-template-kwargs '{"enable_thinking":false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Multi-turn rule:** Only keep the final visible answer in chat history. Do NOT feed prior thought blocks back.
|
||||||
|
|
||||||
|
### Multimodal Settings
|
||||||
|
|
||||||
|
- **Images/Audio** go **before text** in prompts.
|
||||||
|
- **Video**: pass frames first, then instruction.
|
||||||
|
- **Audio** is only on E2B and E4B. Max audio: 30s. Max video: 60s (1 fps).
|
||||||
|
|
||||||
|
#### Visual Token Budgets
|
||||||
|
|
||||||
|
| Budget | Use Case |
|
||||||
|
|---|---|
|
||||||
|
| 70 / 140 | Classification, captioning, fast video |
|
||||||
|
| 280 / 560 | General multimodal chat, charts, UI |
|
||||||
|
| 1120 | OCR, document parsing, handwriting |
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 26B-A4B (Dynamic 4-bit)
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL \
|
||||||
|
--temp 1.0 --top-p 0.95 --top-k 64 \
|
||||||
|
--ctx-size 32768 --flash-attn on --port 8001
|
||||||
|
|
||||||
|
# 31B (Dynamic 4-bit) with thinking
|
||||||
|
./llama.cpp/llama-server \
|
||||||
|
-hf unsloth/gemma-4-31B-it-GGUF:UD-Q4_K_XL \
|
||||||
|
--temp 1.0 --top-p 0.95 --top-k 64 \
|
||||||
|
--ctx-size 32768 \
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}' \
|
||||||
|
--flash-attn on --port 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Quick Reference Cards
|
||||||
|
|
||||||
|
### Temperature by Model & Use Case
|
||||||
|
|
||||||
|
| Model Family | General / Chat | Coding / Precise | Tool Calling | Non-Thinking |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Nemotron 3 Super** | 1.0 | — | 0.6 | — |
|
||||||
|
| **Nemotron 3 Nano** | 1.0 | — | 0.6 | — |
|
||||||
|
| **Qwen 3.5** (thinking) | 1.0 | 0.6 | — | — |
|
||||||
|
| **Qwen 3.5** (non-thinking) | — | — | — | 0.7 (general) / 1.0 (reasoning) |
|
||||||
|
| **Qwen3-Coder-Next** | 1.0 | 1.0 | — | — |
|
||||||
|
| **Gemma 4** | 1.0 | 1.0 | — | — |
|
||||||
|
|
||||||
|
### Top-P by Model & Use Case
|
||||||
|
|
||||||
|
| Model Family | General / Chat | Coding / Precise | Tool Calling | Non-Thinking |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Nemotron 3 Super** | 1.0 | — | 0.95 | — |
|
||||||
|
| **Nemotron 3 Nano** | 1.0 | — | 0.95 | — |
|
||||||
|
| **Qwen 3.5** (thinking) | 0.95 | 0.95 | — | — |
|
||||||
|
| **Qwen 3.5** (non-thinking) | — | — | — | 0.8 (general) / 0.95 (reasoning) |
|
||||||
|
| **Qwen3-Coder-Next** | 0.95 | 0.95 | — | — |
|
||||||
|
| **Gemma 4** | 0.95 | 0.95 | — | — |
|
||||||
|
|
||||||
|
### Top-K by Model
|
||||||
|
|
||||||
|
| Model Family | Top-K |
|
||||||
|
|---|---|
|
||||||
|
| **Nemotron 3** | Default (not specified) |
|
||||||
|
| **Qwen 3.5** | **20** |
|
||||||
|
| **Qwen3-Coder-Next** | **40** |
|
||||||
|
| **Gemma 4** | **64** |
|
||||||
|
|
||||||
|
### Min-P by Model
|
||||||
|
|
||||||
|
| Model Family | Min-P |
|
||||||
|
|---|---|
|
||||||
|
| **Nemotron 3 (all)** | **0.01** |
|
||||||
|
| **Qwen 3.5** | **0.0** |
|
||||||
|
| **Qwen3-Coder-Next** | **0.01** |
|
||||||
|
| **Gemma 4** | Default (not specified) |
|
||||||
|
|
||||||
|
### Presence Penalty by Model
|
||||||
|
|
||||||
|
| Model Family | General | Coding | Reasoning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Nemotron 3** | Default | Default | Default |
|
||||||
|
| **Qwen 3.5** (thinking) | **1.5** | **0.0** | **1.5** |
|
||||||
|
| **Qwen 3.5** (non-thinking) | **1.5** | — | **1.5** |
|
||||||
|
| **Qwen3-Coder-Next** | Default | Default | Default |
|
||||||
|
| **Gemma 4** | Default | Default | Default |
|
||||||
|
|
||||||
|
### Repetition Penalty by Model
|
||||||
|
|
||||||
|
| Model Family | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| **Nemotron 3** | Default | — |
|
||||||
|
| **Qwen 3.5** | **1.0** (disabled) | — |
|
||||||
|
| **Qwen3-Coder-Next** | **1.0** (disabled) | Only increase if looping |
|
||||||
|
| **Gemma 4** | **1.0** (disabled) | Only increase if looping |
|
||||||
|
|
||||||
|
### Seed Values
|
||||||
|
|
||||||
|
| Model Family | Recommended Seed |
|
||||||
|
|---|---|
|
||||||
|
| **Nemotron 3 (all)** | **3407** |
|
||||||
|
| **Qwen 3.5** | 3407 (optional) |
|
||||||
|
| **Qwen3-Coder-Next** | **3407** |
|
||||||
|
| **Gemma 4** | Default |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Critical Warnings by Model
|
||||||
|
|
||||||
|
### Nemotron 3 Super
|
||||||
|
- ⚠️ Do NOT attempt 1M context on first run — increase gradually from 16K/32K.
|
||||||
|
- ⚠️ Setting context to 1M may trigger **CUDA OOM and crash**.
|
||||||
|
- ⚠️ Router-layer fine-tuning **disabled by default** in Unsloth for MoE models.
|
||||||
|
|
||||||
|
### Qwen 3.5
|
||||||
|
- ⚠️ **No Qwen3.5 GGUF works in Ollama** due to separate mmproj vision files. Use llama.cpp-compatible backends.
|
||||||
|
- ⚠️ `presence_penalty` above 0.0 may cause **slight performance decrease**.
|
||||||
|
- ⚠️ If getting gibberish, check context length or add `--cache-type-k bf16 --cache-type-v bf16`.
|
||||||
|
- ⚠️ Redownload older GGUFs — all updated with improved quantization and tool-calling template fixes.
|
||||||
|
|
||||||
|
### Qwen3-Coder-Next
|
||||||
|
- ⚠️ **Update llama.cpp** — a previous bug in `vectorized key_gdiff` caused looping/output issues.
|
||||||
|
- ⚠️ **No Ollama support** — use llama.cpp-compatible backends.
|
||||||
|
- ⚠️ If context length too low, may see `exceeds the available context size` errors.
|
||||||
|
- ⚠️ Tool-calling improved after llama.cpp parsing fixes (Feb 19 update) — use recent version.
|
||||||
|
|
||||||
|
### Gemma 4
|
||||||
|
- ⚠️ **Do NOT use CUDA 13.2 runtime** for any GGUF — causes poor outputs.
|
||||||
|
- ⚠️ Use `llama-server` (not `llama-cli`) for thinking control — more reliable.
|
||||||
|
- ⚠️ Multi-turn: **only keep the final visible answer** in chat history. Do NOT feed prior thought blocks back.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Models Without KB Settings
|
||||||
|
|
||||||
|
The following model families are deployed in the Infrastructure-PFI environment but **do not have KB-documented inference parameters**. Settings for these models use general best practices or vendor defaults:
|
||||||
|
|
||||||
|
| Model | Notes |
|
||||||
|
|---|---|
|
||||||
|
| **DeepSeek R1 0528** | No KB doc — use general MoE defaults |
|
||||||
|
| **Mistral Small 3.1** | No KB doc — use vendor defaults |
|
||||||
|
| **GLM-4.7-Flash** | No KB doc — use vendor defaults |
|
||||||
|
| **GLM Steam 106B-A12B** | No KB doc — use general MoE defaults |
|
||||||
|
| **Granite 4.0 Micro** | No KB doc — use IBM defaults |
|
||||||
|
| **Kimi K2** | No KB doc — use general MoE defaults |
|
||||||
|
| **Skyfall R1 31B v4a** | No KB doc — use general defaults |
|
||||||
|
| **Hermes 4 14B** | No KB doc (download pending) — use vendor defaults |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KB Source Documents
|
||||||
|
|
||||||
|
| Document | Path in KB |
|
||||||
|
|---|---|
|
||||||
|
| Nemotron 3 Super Running Parameters | `reference/nemotron-3-super-running-parameters.md` |
|
||||||
|
| Nemotron 3 Nano Running Parameters | `reference/nemotron-3-nano-running-parameters.md` |
|
||||||
|
| Qwen 3.5 Running Parameters | `reference/qwen3.5-running-parameters.md` |
|
||||||
|
| Qwen3-Coder-Next Running Parameters | `reference/qwen3-coder-next-running-parameters.md` |
|
||||||
|
| Gemma 4 Running Parameters | `reference/gemma-4-running-parameters.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document generated by Linus (Systems Architect) from AIPA Knowledge Base content curated by Atlas.*
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
---
|
||||||
|
id: vm-102-matrix-appservice
|
||||||
|
title: "VM 102 — Matrix Appservice Configuration"
|
||||||
|
summary: "AIPA Matrix Application Service bridge setup for VM 102. Covers appservice registration, agent virtual users, room routing, env vars, operational notes, and troubleshooting."
|
||||||
|
tags: ["infrastructure", "pfi", "pfi-ana", "matrix", "docker", "vm-102", "appservice", "bridge", "aipa", "deployment", "integration"]
|
||||||
|
keywords: ["matrix.pfi.local", "appservice", "as_token", "hs_token", "aipa-bridge", "matrix_bridge.py", "8009", "send_notification", "virtual-users", "room-routing", "element"]
|
||||||
|
links:
|
||||||
|
- "[VM 102 — Matrix Synapse Deployment](vm-102-matrix-synapse.md)"
|
||||||
|
- "[Matrix Bridge — Application Service Integration](../../../KB/projects/aipa/matrix-bridge.md)"
|
||||||
|
- "[Docker Stack Conventions](docker-stack.md)"
|
||||||
|
created: 2026-04-11T00:00:00+00:00
|
||||||
|
modified: 2026-04-11T00:00:00+00:00
|
||||||
|
path: docs/pfi-ana/vm-102-matrix-appservice.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# VM 102 — Matrix Appservice Configuration
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AIPA Matrix bridge registers with Synapse as a Matrix Application Service. This means:
|
||||||
|
- Synapse **pushes** all relevant events to the bridge (no polling)
|
||||||
|
- The bridge manages **virtual users** for each agent — no real accounts needed
|
||||||
|
- The bridge authenticates to Synapse with a shared `as_token`
|
||||||
|
- Synapse authenticates its pushes to the bridge with a shared `hs_token`
|
||||||
|
|
||||||
|
> Prerequisite: [VM 102 — Matrix Synapse Deployment](vm-102-matrix-synapse.md) must be complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application Service Model
|
||||||
|
|
||||||
|
```
|
||||||
|
Synapse ──── push events ────► AIPA Bridge (port 8009)
|
||||||
|
│
|
||||||
|
routes to agent
|
||||||
|
│
|
||||||
|
posts response ◄──── Synapse ◄──── Element ◄──── User
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent Virtual Users
|
||||||
|
|
||||||
|
| Agent | Matrix ID | Display Name |
|
||||||
|
|----------|----------------------------|--------------|
|
||||||
|
| Atlas | `@atlas:matrix.pfi.local` | Atlas |
|
||||||
|
| Linus | `@linus:matrix.pfi.local` | Linus |
|
||||||
|
| Hermione | `@hermione:matrix.pfi.local` | Hermione |
|
||||||
|
|
||||||
|
These are **virtual** — managed entirely by the bridge. Do not register them as real Synapse accounts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Generate Tokens
|
||||||
|
|
||||||
|
Two random tokens are needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "import secrets; print(secrets.token_hex(32))" # as_token (bridge → Synapse)
|
||||||
|
python3 -c "import secrets; print(secrets.token_hex(32))" # hs_token (Synapse → bridge)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Create Appservice Registration File
|
||||||
|
|
||||||
|
File: `/opt/docker/data/synapse/aipa_appservice.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: aipa-bridge
|
||||||
|
url: http://<BRIDGE_HOST_IP>:8009 # IP/hostname the bridge is reachable from Synapse container
|
||||||
|
as_token: "<AS_TOKEN_FROM_STEP_1>"
|
||||||
|
hs_token: "<HS_TOKEN_FROM_STEP_1>"
|
||||||
|
sender_localpart: aipa-bot # @aipa-bot:matrix.pfi.local (unused fallback sender)
|
||||||
|
namespaces:
|
||||||
|
users:
|
||||||
|
- exclusive: true
|
||||||
|
regex: "@(atlas|linus|hermione):.*"
|
||||||
|
rooms: []
|
||||||
|
aliases: []
|
||||||
|
rate_limited: false
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`exclusive: true`** — only the bridge can act as those users; no one can register `@atlas` as a real account.
|
||||||
|
- **`url`** — must be reachable from inside the Synapse container. If the bridge runs on the VM host, use the host IP or Docker gateway IP (e.g., `172.17.0.1`).
|
||||||
|
|
||||||
|
### Restart Synapse to Load
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/docker/compose/synapse && docker compose restart synapse
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Configure AIPA Environment
|
||||||
|
|
||||||
|
### env.sh additions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ── Matrix bridge ──────────────────────────────────────────────────────
|
||||||
|
export MATRIX_HOMESERVER_URL="http://localhost:8008"
|
||||||
|
export MATRIX_SERVER_NAME="matrix.pfi.local"
|
||||||
|
export MATRIX_AS_TOKEN="<as_token from Step 1>"
|
||||||
|
export MATRIX_HS_TOKEN="<hs_token from Step 1>"
|
||||||
|
# Optional: room to send agent notifications when no channel is specified
|
||||||
|
# export MATRIX_DEFAULT_ROOM="!roomid:matrix.pfi.local"
|
||||||
|
```
|
||||||
|
|
||||||
|
### providers.yaml matrix section
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
matrix:
|
||||||
|
homeserver_url: "${MATRIX_HOMESERVER_URL}"
|
||||||
|
server_name: "${MATRIX_SERVER_NAME}"
|
||||||
|
as_token: "${MATRIX_AS_TOKEN}"
|
||||||
|
hs_token: "${MATRIX_HS_TOKEN}"
|
||||||
|
bridge_host: "0.0.0.0"
|
||||||
|
bridge_port: 8009
|
||||||
|
default_notification_room: "${MATRIX_DEFAULT_ROOM}"
|
||||||
|
agents:
|
||||||
|
atlas:
|
||||||
|
display_name: "Atlas"
|
||||||
|
avatar_url: ""
|
||||||
|
linus:
|
||||||
|
display_name: "Linus"
|
||||||
|
avatar_url: ""
|
||||||
|
hermione:
|
||||||
|
display_name: "Hermione"
|
||||||
|
avatar_url: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Start the Bridge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate && source env.sh
|
||||||
|
python -m core.matrix_bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m core.matrix_bridge --host 0.0.0.0 --port 8009
|
||||||
|
python -m core.matrix_bridge --no-profile-setup # skip display name / avatar init
|
||||||
|
```
|
||||||
|
|
||||||
|
On first start, the bridge:
|
||||||
|
1. Registers display names for each agent user
|
||||||
|
2. Starts the appservice HTTP server on port 8009
|
||||||
|
3. Listens for Matrix transactions from Synapse
|
||||||
|
4. Accepts invitations to rooms
|
||||||
|
5. Begins routing messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — First Use
|
||||||
|
|
||||||
|
1. Open Element at `http://10.250.50.70:8080`
|
||||||
|
2. Log in with your admin account (server: `matrix.pfi.local`)
|
||||||
|
3. Open a DM with `@atlas:matrix.pfi.local`
|
||||||
|
4. Send a message — the bridge routes it to Atlas and posts the response
|
||||||
|
|
||||||
|
To start a room with a specific agent, invite them:
|
||||||
|
- New room → Invite `@linus:matrix.pfi.local` → Linus joins automatically
|
||||||
|
- All messages in that room go to Linus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Room Routing
|
||||||
|
|
||||||
|
1. User invites an agent user to a room (or opens a DM)
|
||||||
|
2. Bridge receives the `m.room.member` invite event, agent auto-joins
|
||||||
|
3. Room is permanently mapped to that agent in `sessions/matrix_rooms.json`
|
||||||
|
4. All subsequent messages in that room are routed to the mapped agent
|
||||||
|
5. If multiple agent users are in the same room, the first invite wins for session purposes;
|
||||||
|
subsequent agents each get their own session (multi-agent collaboration rooms)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
|
||||||
|
### Room Session Persistence
|
||||||
|
|
||||||
|
The bridge persists the room→agent→session mapping to `sessions/matrix_rooms.json`
|
||||||
|
under `AIPA_ROOT`. If the bridge restarts, existing rooms continue their sessions.
|
||||||
|
|
||||||
|
To reset a room's conversation history:
|
||||||
|
- Ask the agent `/reset`, or
|
||||||
|
- Delete the entry from `matrix_rooms.json` and restart the bridge.
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
The bridge converts markdown in agent responses to Matrix HTML
|
||||||
|
(`format: org.matrix.custom.html`). Code blocks, bold, and inline code are
|
||||||
|
rendered correctly in Element.
|
||||||
|
|
||||||
|
### Typing Indicators
|
||||||
|
|
||||||
|
The bridge sends a typing indicator (`m.typing`) while the agent is processing,
|
||||||
|
so users see the animated dots while waiting.
|
||||||
|
|
||||||
|
### Proactive Notifications
|
||||||
|
|
||||||
|
Agents can push messages to Matrix rooms via `send_notification`:
|
||||||
|
|
||||||
|
```
|
||||||
|
send_notification(
|
||||||
|
content="KB rebuild complete — 1,247 chunks indexed.",
|
||||||
|
channel="!roomid:matrix.pfi.local"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `channel` is set, it must be a Matrix room ID (`!roomid:server`)
|
||||||
|
- If `channel` is omitted, the notification goes to `MATRIX_DEFAULT_ROOM` if configured
|
||||||
|
|
||||||
|
### Agent Users Always Show as Offline
|
||||||
|
|
||||||
|
Expected behavior — virtual users don't have presence. This is normal for appservice users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely Cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| Bridge starts but Synapse doesn't push events | Appservice URL wrong in registration YAML | Verify `url:` is reachable from the Synapse container; check `docker inspect synapse` network |
|
||||||
|
| Agent joins room but doesn't respond | `hs_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_HS_TOKEN` env var |
|
||||||
|
| 401 errors from Synapse | `as_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_AS_TOKEN` env var |
|
||||||
|
| Agent user shows as offline always | Expected — virtual users don't have presence | Normal for appservice users; no fix needed |
|
||||||
|
| Messages loop (agent replies to itself) | Bridge not filtering its own messages | Check `SENDER_LOCALPART` filter in bridge; bridge ignores all managed agent users as senders |
|
||||||
|
| Element can't connect to homeserver | Wrong `base_url` in element-config.json | Must be the IP/hostname Element's browser can reach, not the Docker container name |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Dependency | Version | Purpose |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| `markdown` | any | Markdown→HTML rendering for Matrix responses |
|
||||||
|
| `fastapi` | any | Appservice HTTP server |
|
||||||
|
| `httpx` | any | Client-Server API calls to Synapse |
|
||||||
|
|
||||||
|
Install: `pip install markdown` (or `pip install -r requirements.txt`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- The `as_token` and `hs_token` are secrets equivalent to admin credentials. Store
|
||||||
|
them in `env.sh` (gitignored) and never commit them.
|
||||||
|
- The bridge runs with `MATRIX_HS_TOKEN` to authenticate Synapse's push requests. Any
|
||||||
|
request without this token in the `Authorization` header is rejected (403).
|
||||||
|
- Restrict port 8009 to internal access only (firewall or bind to `127.0.0.1` if bridge and Synapse are on the same host).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Source deployment guide: `projects/matrix/matrix-deployment.md` (2026-04-11)
|
||||||
|
- AIPA bridge code: `core/matrix_bridge.py`
|
||||||
|
- AIPA bridge KB entry: `KB/projects/aipa/matrix-bridge.md`
|
||||||
|
- Matrix Synapse appservice docs: https://element-hq.github.io/synapse/latest/application_services.html
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
---
|
||||||
|
id: vm-102-matrix-synapse
|
||||||
|
title: "VM 102 — Matrix Synapse Deployment"
|
||||||
|
summary: "Docker deployment of Synapse homeserver with PostgreSQL and Element Web on VM 102 (PFI-ANA-Docker). Covers compose file, configuration, storage paths, and admin setup."
|
||||||
|
tags: ["infrastructure", "pfi", "pfi-ana", "matrix", "docker", "vm-102", "synapse", "self-hosted", "deployment"]
|
||||||
|
keywords: ["matrix.pfi.local", "synapse", "element-web", "10.250.50.70", "VM-102", "PFI-ANA-Docker", "postgres:16", "appservice", "AIPA"]
|
||||||
|
links:
|
||||||
|
- "[Matrix Protocol Reference](../Infrastructure-PFI-Project-Index.md)"
|
||||||
|
- "[VM-102 Proxmox Config](../../../configs/pfi-ana/proxmox/vm-102.conf)"
|
||||||
|
- "[Docker Stack Conventions](docker-stack.md)"
|
||||||
|
created: 2026-04-11T00:00:00+00:00
|
||||||
|
modified: 2026-04-11T00:00:00+00:00
|
||||||
|
path: docs/pfi-ana/vm-102-matrix-synapse.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# VM 102 — Matrix Synapse Deployment
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Matrix (Synapse) is deployed on VM 102 (PFI-ANA-Docker, `10.250.50.70`) as the primary
|
||||||
|
human-to-agent communication channel for AIPA. The stack consists of:
|
||||||
|
|
||||||
|
- **Synapse** — Matrix homeserver (event routing, auth, persistence)
|
||||||
|
- **PostgreSQL 16** — Synapse database backend
|
||||||
|
- **Element Web** — Web client for users
|
||||||
|
|
||||||
|
Federation is disabled (internal-only deployment). Registration is disabled (admin-created accounts only).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
User (Element client)
|
||||||
|
│ m.room.message events
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Synapse │ Matrix homeserver — event routing, auth, persistence
|
||||||
|
│ (homeserver) │ Port 8008 (Client-Server API)
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│ Appservice push PUT /_matrix/app/v1/transactions/{txnId}
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ AIPA Matrix Bridge (core/matrix_bridge.py) │
|
||||||
|
│ Port 8009 │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
> Full appservice bridge details: [VM 102 — Matrix Appservice Configuration](vm-102-matrix-appservice.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
| Component | Image | Port | Purpose |
|
||||||
|
|-----------------|--------------------------------|-------|-----------------------------------|
|
||||||
|
| `synapse` | `matrixdotorg/synapse:latest` | 8008 | Matrix homeserver (Client-Server API) |
|
||||||
|
| `synapse-db` | `postgres:16` | — | Synapse database |
|
||||||
|
| `element-web` | `vectorim/element-web:latest` | 8080 | Web client for users |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage Paths
|
||||||
|
|
||||||
|
| Host Path | Purpose |
|
||||||
|
|------------------------------------|-----------------------------------------|
|
||||||
|
| `/opt/docker/compose/synapse/` | Compose file (managed by Dockge) |
|
||||||
|
| `/opt/docker/conf/synapse/` | Configuration files |
|
||||||
|
| `/opt/docker/data/synapse/` | Synapse data (`homeserver.yaml`, signing keys, appservice reg) |
|
||||||
|
| `/opt/docker/conf/synapse/element-config.json` | Element Web client config |
|
||||||
|
|
||||||
|
> Follows the VM 102 convention: compose in `/opt/docker/compose/<service>/`, config in `/opt/docker/conf/<service>/`.
|
||||||
|
|
||||||
|
### Volumes
|
||||||
|
|
||||||
|
| Volume / Path | Container Path | Purpose |
|
||||||
|
|--------------------------------|-------------------------------|-----------------------------|
|
||||||
|
| `/opt/docker/data/synapse` | `/data` | Synapse config, signing keys, media store |
|
||||||
|
| `synapse-db-data` (named vol) | `/var/lib/postgresql/data` | PostgreSQL persistent data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Create Directory Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/docker/data/synapse
|
||||||
|
mkdir -p /opt/docker/conf/synapse
|
||||||
|
mkdir -p /opt/docker/compose/synapse
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Generate Synapse Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-v /opt/docker/data/synapse:/data \
|
||||||
|
-e SYNAPSE_SERVER_NAME=matrix.pfi.local \
|
||||||
|
-e SYNAPSE_REPORT_STATS=no \
|
||||||
|
matrixdotorg/synapse:latest generate
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes `/opt/docker/data/synapse/homeserver.yaml` and
|
||||||
|
`/opt/docker/data/synapse/matrix.pfi.local.signing.key`.
|
||||||
|
|
||||||
|
**Do not modify `server_name` after generation — it is permanent.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Edit homeserver.yaml
|
||||||
|
|
||||||
|
Open `/opt/docker/data/synapse/homeserver.yaml` and apply:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Use PostgreSQL instead of SQLite (required for production)
|
||||||
|
database:
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
user: synapse
|
||||||
|
password: synapse_db_password # match POSTGRES_PASSWORD in compose
|
||||||
|
database: synapse
|
||||||
|
host: synapse-db
|
||||||
|
cp_min: 5
|
||||||
|
cp_max: 10
|
||||||
|
|
||||||
|
# Disable open registration — accounts are created by admin only
|
||||||
|
enable_registration: false
|
||||||
|
|
||||||
|
# Disable federation (internal deployment only)
|
||||||
|
federation_domain_whitelist: []
|
||||||
|
|
||||||
|
# Allow the application service to be registered (add AFTER generating the AS file)
|
||||||
|
app_service_config_files:
|
||||||
|
- /data/aipa_appservice.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Docker Compose
|
||||||
|
|
||||||
|
File: `/opt/docker/compose/synapse/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
# =============================================================================
|
||||||
|
# Synapse Matrix Homeserver — AIPA internal deployment on VM 102
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# Conventions:
|
||||||
|
# - Config: /opt/docker/conf/synapse/
|
||||||
|
# - Data: /opt/docker/data/synapse/ (bind mount) + synapse-db-data (named vol)
|
||||||
|
# - Compose: /opt/docker/compose/synapse/
|
||||||
|
# - Network: synapse-net (dedicated, not on traefik-net)
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - Federation disabled (internal only)
|
||||||
|
# - Port 8448 (federation) commented out
|
||||||
|
# - Resource limits set for VM 102 (8 vCPU, 16 GB RAM)
|
||||||
|
|
||||||
|
services:
|
||||||
|
synapse-db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: synapse-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: synapse
|
||||||
|
POSTGRES_PASSWORD: synapse_db_password
|
||||||
|
POSTGRES_DB: synapse
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||||
|
volumes:
|
||||||
|
- synapse-db-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- synapse-net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U synapse"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
synapse:
|
||||||
|
image: matrixdotorg/synapse:latest
|
||||||
|
container_name: synapse
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
synapse-db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8008:8008" # Client-Server API (HTTP)
|
||||||
|
# - "8448:8448" # Server-Server API (federation) — disabled for internal use
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/data/synapse:/data
|
||||||
|
networks:
|
||||||
|
- synapse-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: "2.0"
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://localhost:8008/health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
element-web:
|
||||||
|
image: vectorim/element-web:latest
|
||||||
|
container_name: element-web
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/synapse/element-config.json:/app/config.json:ro
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
networks:
|
||||||
|
- synapse-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
synapse-db-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
synapse-net:
|
||||||
|
name: synapse-net
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/docker/compose/synapse
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
curl http://localhost:8008/health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Element Web Configuration
|
||||||
|
|
||||||
|
File: `/opt/docker/conf/synapse/element-config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default_server_config": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "http://10.250.50.70:8008",
|
||||||
|
"server_name": "matrix.pfi.local"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": "AIPA",
|
||||||
|
"default_theme": "dark",
|
||||||
|
"disable_guests": true,
|
||||||
|
"disable_login_language_selector": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6 — Create Admin User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it synapse register_new_matrix_user \
|
||||||
|
-u admin \
|
||||||
|
-p 'yourpassword' \
|
||||||
|
-a \
|
||||||
|
http://localhost:8008
|
||||||
|
```
|
||||||
|
|
||||||
|
> The `-a` flag makes the user an admin. Omit for regular users.
|
||||||
|
|
||||||
|
**Agent users** (`@atlas`, `@linus`, `@hermione`) are **virtual** — managed by the
|
||||||
|
appservice. Do **not** register them as real accounts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Port Allocation
|
||||||
|
|
||||||
|
| Port | Service | Purpose | Protocol |
|
||||||
|
|------|---------------|----------------------------|----------|
|
||||||
|
| 8008 | Synapse | Client-Server API | HTTP |
|
||||||
|
| 8080 | Element Web | Web client | HTTP |
|
||||||
|
| 8009 | AIPA Bridge | Appservice endpoint | HTTP |
|
||||||
|
|
||||||
|
> See [docker-stack.md](docker-stack.md) for the full VM 102 port allocation table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
This deployment uses a **dedicated `synapse-net` network** (not `traefik-net`), because:
|
||||||
|
|
||||||
|
- Synapse is accessed directly by IP (no public domain routing needed)
|
||||||
|
- The AIPA bridge connects to Synapse at `http://localhost:8008` from the host
|
||||||
|
- Element Web connects at the VM IP:8008 from the browser
|
||||||
|
|
||||||
|
If TLS/reverse proxy is added later, join `traefik-net` and add Traefik labels.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Synapse is exposed on port 8008 (HTTP). For any externally accessible deployment,
|
||||||
|
put it behind a TLS-terminating reverse proxy (Traefik/nginx) and restrict 8009
|
||||||
|
to internal access only.
|
||||||
|
- Registration is disabled (`enable_registration: false`) — accounts created by admin only.
|
||||||
|
- Federation is disabled (`federation_domain_whitelist: []`) — internal use only.
|
||||||
|
- The `as_token` and `hs_token` in the appservice registration are secrets equivalent to
|
||||||
|
admin credentials. Store in `env.sh` (gitignored), never commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely Cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| Bridge starts but Synapse doesn't push events | Appservice URL wrong in registration YAML | Verify `url:` is reachable from the Synapse container; check `docker inspect synapse` network |
|
||||||
|
| 401 errors from Synapse | `as_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_AS_TOKEN` env var |
|
||||||
|
| Element can't connect to homeserver | Wrong `base_url` in element-config.json | Must be the IP/hostname Element's browser can reach, not the Docker container name |
|
||||||
|
| Synapse won't start | Database connection failure | Verify `synapse-db` is healthy first; check password matches in `homeserver.yaml` and compose |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After Synapse is running and healthy, configure the AIPA appservice bridge:
|
||||||
|
→ [VM 102 — Matrix Appservice Configuration](vm-102-matrix-appservice.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Source deployment guide: `projects/matrix/matrix-deployment.md` (2026-04-11)
|
||||||
|
- VM 102 Proxmox config: `configs/pfi-ana/proxmox/vm-102.conf`
|
||||||
|
- Docker Stack conventions: `docs/pfi-ana/docker-stack.md`
|
||||||
|
- Matrix Protocol Reference: `infrastructure/matrix-docker-deployment.md` (KB)
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# add-host.sh — register a new server so refresh-server-info.sh picks it up.
|
||||||
|
#
|
||||||
|
# Creates servers/<name>/ and writes servers/<name>/ssh-target with the
|
||||||
|
# given IP (or user@ip). The next `scripts/refresh-server-info.sh <name>`
|
||||||
|
# run will discover the host and pull its first system-details.txt.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/add-host.sh <name> <ip-or-user@ip>
|
||||||
|
# scripts/add-host.sh <name> <ip-or-user@ip> --force # overwrite existing
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# scripts/add-host.sh la-docker 10.60.50.12
|
||||||
|
# scripts/add-host.sh edge-01 admin@203.0.113.9
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SERVERS_DIR="$REPO_ROOT/servers"
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
POSITIONAL=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-f|--force) FORCE=1 ;;
|
||||||
|
-h|--help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||||
|
-*) echo "error: unknown flag $arg" >&2; exit 2 ;;
|
||||||
|
*) POSITIONAL+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#POSITIONAL[@]}" -ne 2 ]; then
|
||||||
|
echo "usage: $(basename "$0") <name> <ip-or-user@ip> [--force]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
name="${POSITIONAL[0]}"
|
||||||
|
target="${POSITIONAL[1]}"
|
||||||
|
|
||||||
|
if [[ ! "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
|
||||||
|
echo "error: invalid host name '$name' (expected [a-zA-Z0-9._-])" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Trivial sanity check on the target — not exhaustive, just catches typos.
|
||||||
|
if [[ -z "$target" ]] || [[ "$target" =~ [[:space:]] ]]; then
|
||||||
|
echo "error: invalid ssh target '$target'" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
host_dir="$SERVERS_DIR/$name"
|
||||||
|
ssh_target_file="$host_dir/ssh-target"
|
||||||
|
|
||||||
|
if [ -f "$ssh_target_file" ] && [ "$FORCE" -ne 1 ]; then
|
||||||
|
existing=$(awk 'NF{print $1; exit}' "$ssh_target_file")
|
||||||
|
if [ "$existing" = "$target" ]; then
|
||||||
|
echo "host '$name' already registered with target '$target' — nothing to do"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "error: $ssh_target_file already exists (currently '$existing'); pass --force to overwrite" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$host_dir"
|
||||||
|
printf '%s\n' "$target" > "$ssh_target_file"
|
||||||
|
|
||||||
|
printf 'registered: %s → %s\n' "$name" "$target"
|
||||||
|
printf 'next: scripts/refresh-server-info.sh %s\n' "$name"
|
||||||
Executable
+257
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy-stack.sh — push a local stacks-mirror dir to a server, with
|
||||||
|
# per-file diff and confirmation prompt.
|
||||||
|
#
|
||||||
|
# Layout assumed:
|
||||||
|
# stacks-mirror/<host>/<stack>/<file> → <host>:/opt/docker/compose/<stack>/<file>
|
||||||
|
# stacks-mirror/<host>/<stack>/conf/<file> → <host>:/opt/docker/conf/<stack>/<file>
|
||||||
|
#
|
||||||
|
# Secrets / runtime state are never pushed (same exclude list as
|
||||||
|
# sync-stacks.sh): .env*, acme.json, *.key/crt/pem/pfx, *.sqlite*, *.db,
|
||||||
|
# *.log*, *.pid, hub/, logs/, client_secrets.json.
|
||||||
|
#
|
||||||
|
# The script:
|
||||||
|
# 1. Runs rsync --dry-run to find which files would change.
|
||||||
|
# 2. Prints a unified diff for each changed/added file (deletions noted).
|
||||||
|
# 3. Prompts [y/N]; applies the rsync only on 'y'.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/deploy-stack.sh <host> <stack>
|
||||||
|
# scripts/deploy-stack.sh <host> <stack> --yes # skip prompt (use sparingly)
|
||||||
|
# scripts/deploy-stack.sh <host> <stack> --compose # push only compose side
|
||||||
|
# scripts/deploy-stack.sh <host> <stack> --conf # push only conf side
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v rsync >/dev/null 2>&1; then
|
||||||
|
echo "error: rsync is not installed on this workstation" >&2
|
||||||
|
echo " install it (e.g. 'sudo apt install rsync') and ensure the target host has it too" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SERVERS_DIR="$REPO_ROOT/servers"
|
||||||
|
MIRROR_DIR="$REPO_ROOT/stacks-mirror"
|
||||||
|
|
||||||
|
EXCLUDES=(
|
||||||
|
# Include .env.example / *.env.example templates before the broader
|
||||||
|
# .env* exclude — rsync processes these in order, first match wins.
|
||||||
|
--include='.env.example'
|
||||||
|
--include='*.env.example'
|
||||||
|
--exclude=.env
|
||||||
|
--exclude='.env.*'
|
||||||
|
--exclude=acme.json
|
||||||
|
--exclude=client_secrets.json
|
||||||
|
--exclude='*.pem'
|
||||||
|
--exclude='*.key'
|
||||||
|
--exclude='*.crt'
|
||||||
|
--exclude='*.pfx'
|
||||||
|
--exclude='*.sqlite'
|
||||||
|
--exclude='*.sqlite3'
|
||||||
|
--exclude='*.db'
|
||||||
|
--exclude='*.log'
|
||||||
|
--exclude='*.log.*'
|
||||||
|
--exclude='*.pid'
|
||||||
|
--exclude='hub/'
|
||||||
|
--exclude='logs/'
|
||||||
|
)
|
||||||
|
|
||||||
|
HOST=
|
||||||
|
STACK=
|
||||||
|
ASSUME_YES=0
|
||||||
|
DO_COMPOSE=1
|
||||||
|
DO_CONF=1
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--yes|-y) ASSUME_YES=1 ;;
|
||||||
|
--compose) DO_CONF=0 ;;
|
||||||
|
--conf) DO_COMPOSE=0 ;;
|
||||||
|
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
|
||||||
|
-*) echo "error: unknown flag $a" >&2; exit 2 ;;
|
||||||
|
*)
|
||||||
|
if [ -z "$HOST" ]; then HOST="$a"
|
||||||
|
elif [ -z "$STACK" ]; then STACK="$a"
|
||||||
|
else echo "error: unexpected positional '$a'" >&2; exit 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$HOST" ] || { echo "usage: $(basename "$0") <host> <stack>" >&2; exit 2; }
|
||||||
|
[ -n "$STACK" ] || { echo "usage: $(basename "$0") <host> <stack>" >&2; exit 2; }
|
||||||
|
|
||||||
|
resolve_target() {
|
||||||
|
local host="$1"
|
||||||
|
local effective
|
||||||
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
||||||
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
||||||
|
echo "$host"; return
|
||||||
|
fi
|
||||||
|
local fb="$SERVERS_DIR/$host/ssh-target"
|
||||||
|
if [ -f "$fb" ]; then awk 'NF{print $1; exit}' "$fb"; return; fi
|
||||||
|
echo "$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
TARGET=$(resolve_target "$HOST")
|
||||||
|
STACK_DIR="$MIRROR_DIR/$HOST/$STACK"
|
||||||
|
|
||||||
|
[ -d "$STACK_DIR" ] || { echo "error: $STACK_DIR not found (pull with sync-stacks.sh first)" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Collect the two src/dest pairs we need to consider.
|
||||||
|
PAIRS=() # each entry: "<kind>|<src>|<dest>"
|
||||||
|
if [ "$DO_COMPOSE" -eq 1 ]; then
|
||||||
|
PAIRS+=("compose|$STACK_DIR/|$TARGET:/opt/docker/compose/$STACK/")
|
||||||
|
fi
|
||||||
|
if [ "$DO_CONF" -eq 1 ] && [ -d "$STACK_DIR/conf" ]; then
|
||||||
|
PAIRS+=("conf|$STACK_DIR/conf/|$TARGET:/opt/docker/conf/$STACK/")
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#PAIRS[@]}" -gt 0 ] || { echo "nothing to deploy"; exit 0; }
|
||||||
|
|
||||||
|
# --------- Dry-run summary: which files would change, per kind. --------
|
||||||
|
declare -A CHANGED_FILES_BY_KIND=() # kind → newline-separated list
|
||||||
|
declare -A DELETED_FILES_BY_KIND=()
|
||||||
|
declare -A RAW_RSYNC_OUT_BY_KIND=() # kind → raw rsync itemize output
|
||||||
|
any_change=0
|
||||||
|
|
||||||
|
for entry in "${PAIRS[@]}"; do
|
||||||
|
IFS='|' read -r kind src dest <<<"$entry"
|
||||||
|
extra=()
|
||||||
|
[ "$kind" = compose ] && extra+=(--exclude='conf/')
|
||||||
|
|
||||||
|
# Pre-create the remote dir. Without this, rsync against a nonexistent
|
||||||
|
# destination can fail in ways the dry-run doesn't surface cleanly.
|
||||||
|
remote_path="/opt/docker/$kind/$STACK/"
|
||||||
|
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 "$TARGET" \
|
||||||
|
"mkdir -p '$remote_path'" 2>/dev/null; then
|
||||||
|
echo "error: could not create $remote_path on $TARGET (check perms / ssh)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_out=$(mktemp) tmp_err=$(mktemp)
|
||||||
|
rc=0
|
||||||
|
rsync -az --delete --dry-run \
|
||||||
|
--out-format='%i %n' \
|
||||||
|
"${EXCLUDES[@]}" "${extra[@]}" \
|
||||||
|
"$src" "$dest" >"$tmp_out" 2>"$tmp_err" || rc=$?
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo "error: rsync dry-run failed (exit $rc) for $src → $dest" >&2
|
||||||
|
sed 's/^/ /' "$tmp_err" >&2
|
||||||
|
rm -f "$tmp_out" "$tmp_err"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
mapfile -t lines < "$tmp_out"
|
||||||
|
RAW_RSYNC_OUT_BY_KIND[$kind]=$(cat "$tmp_out")
|
||||||
|
rm -f "$tmp_out" "$tmp_err"
|
||||||
|
|
||||||
|
changed=""
|
||||||
|
deleted=""
|
||||||
|
for ln in "${lines[@]}"; do
|
||||||
|
# Itemized codes (rsync uses '<' for push, '>' for pull):
|
||||||
|
# <f+++++++++ newfile (new file, push)
|
||||||
|
# >f+++++++++ newfile (new file, pull)
|
||||||
|
# <f..t...... file.yaml (content update, push)
|
||||||
|
# *deleting oldfile (deletion, either direction)
|
||||||
|
# .d..t...... ./ (metadata on a dir — skip)
|
||||||
|
# cd+++++++++ somedir/ (new dir — skip, we diff files only)
|
||||||
|
[ -z "$ln" ] && continue
|
||||||
|
code=$(awk '{print $1}' <<<"$ln")
|
||||||
|
name=$(awk '{ $1=""; sub(/^ /,""); print }' <<<"$ln")
|
||||||
|
[ -z "$name" ] && continue
|
||||||
|
[ "${name: -1}" = "/" ] && continue # directory entry
|
||||||
|
|
||||||
|
case "$code" in
|
||||||
|
'*deleting') deleted+="$name"$'\n' ;;
|
||||||
|
'<f'*|'>f'*) changed+="$name"$'\n' ;;
|
||||||
|
*) : ;; # dir entries, metadata-only, unknown
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
CHANGED_FILES_BY_KIND[$kind]="$changed"
|
||||||
|
DELETED_FILES_BY_KIND[$kind]="$deleted"
|
||||||
|
if [ -n "$changed$deleted" ]; then any_change=1; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$any_change" -eq 0 ]; then
|
||||||
|
echo "up to date: $HOST/$STACK is already in sync with server."
|
||||||
|
# Diagnostic: if the remote dir actually looks empty, we may have been
|
||||||
|
# fooled by an rsync quirk — dump what rsync saw so the user can tell.
|
||||||
|
for entry in "${PAIRS[@]}"; do
|
||||||
|
IFS='|' read -r kind _ _ <<<"$entry"
|
||||||
|
raw=${RAW_RSYNC_OUT_BY_KIND[$kind]:-}
|
||||||
|
remote_path="/opt/docker/$kind/$STACK/"
|
||||||
|
remote_count=$(ssh -o BatchMode=yes "$TARGET" \
|
||||||
|
"find '$remote_path' -mindepth 1 -maxdepth 1 2>/dev/null | wc -l" \
|
||||||
|
2>/dev/null || echo "?")
|
||||||
|
printf ' %s: remote has %s entries, rsync itemize output:\n' "$kind" "$remote_count"
|
||||||
|
if [ -z "$raw" ]; then
|
||||||
|
printf ' (empty — rsync reported no work)\n'
|
||||||
|
else
|
||||||
|
sed 's/^/ /' <<<"$raw"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------- Print diffs. -----------------------------------------------
|
||||||
|
divider() { printf '\n%s\n' "------------------------------------------------------------"; }
|
||||||
|
|
||||||
|
for entry in "${PAIRS[@]}"; do
|
||||||
|
IFS='|' read -r kind src dest <<<"$entry"
|
||||||
|
remote_base="/opt/docker/$kind/$STACK"
|
||||||
|
changed=${CHANGED_FILES_BY_KIND[$kind]:-}
|
||||||
|
deleted=${DELETED_FILES_BY_KIND[$kind]:-}
|
||||||
|
[ -z "$changed$deleted" ] && continue
|
||||||
|
|
||||||
|
printf '\n=== %s → %s ===\n' "$src" "$dest"
|
||||||
|
|
||||||
|
while IFS= read -r rel; do
|
||||||
|
[ -z "$rel" ] && continue
|
||||||
|
local_file="$src$rel"
|
||||||
|
remote_file="$remote_base/$rel"
|
||||||
|
divider
|
||||||
|
if ssh -o BatchMode=yes "$TARGET" "[ -f '$remote_file' ]" 2>/dev/null; then
|
||||||
|
printf 'MODIFY %s\n' "$rel"
|
||||||
|
diff -u --label "a/$rel (remote)" --label "b/$rel (local)" \
|
||||||
|
<(ssh -o BatchMode=yes "$TARGET" "cat '$remote_file'" 2>/dev/null) \
|
||||||
|
"$local_file" || true
|
||||||
|
else
|
||||||
|
printf 'ADD %s\n' "$rel"
|
||||||
|
diff -u --label /dev/null --label "b/$rel (local)" \
|
||||||
|
/dev/null "$local_file" || true
|
||||||
|
fi
|
||||||
|
done <<<"$changed"
|
||||||
|
|
||||||
|
while IFS= read -r rel; do
|
||||||
|
[ -z "$rel" ] && continue
|
||||||
|
remote_file="$remote_base/$rel"
|
||||||
|
divider
|
||||||
|
printf 'DELETE %s\n' "$rel"
|
||||||
|
diff -u --label "a/$rel (remote)" --label /dev/null \
|
||||||
|
<(ssh -o BatchMode=yes "$TARGET" "cat '$remote_file'" 2>/dev/null) \
|
||||||
|
/dev/null || true
|
||||||
|
done <<<"$deleted"
|
||||||
|
done
|
||||||
|
|
||||||
|
divider
|
||||||
|
|
||||||
|
if [ "$ASSUME_YES" -ne 1 ]; then
|
||||||
|
read -r -p "Apply these changes to $TARGET? [y/N] " ans
|
||||||
|
case "$ans" in
|
||||||
|
y|Y|yes|YES) ;;
|
||||||
|
*) echo "aborted."; exit 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------- Apply. -----------------------------------------------------
|
||||||
|
for entry in "${PAIRS[@]}"; do
|
||||||
|
IFS='|' read -r kind src dest <<<"$entry"
|
||||||
|
extra=()
|
||||||
|
[ "$kind" = compose ] && extra+=(--exclude='conf/')
|
||||||
|
printf 'pushing %s → %s\n' "$src" "$dest"
|
||||||
|
rsync -az --delete \
|
||||||
|
"${EXCLUDES[@]}" "${extra[@]}" \
|
||||||
|
"$src" "$dest"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "done."
|
||||||
Executable
+260
@@ -0,0 +1,260 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# refresh-server-info.sh — pull a fresh system-details.txt from servers.
|
||||||
|
#
|
||||||
|
# Hosts are discovered by listing `servers/*/` directory names. Each name
|
||||||
|
# is used as the SSH target, so put matching entries in ~/.ssh/config to
|
||||||
|
# customize user / port / identity.
|
||||||
|
#
|
||||||
|
# Fallback: if the dir name doesn't resolve (and ssh_config doesn't rewrite
|
||||||
|
# it), the script looks for `servers/<host>/ssh-target` (one line, containing
|
||||||
|
# an IP or `user@ip`) and uses that instead. This keeps the tool working
|
||||||
|
# from a fresh clone without requiring DNS or ssh_config setup.
|
||||||
|
#
|
||||||
|
# For each host:
|
||||||
|
# 1. Run scripts/server_inspect.sh on the remote via `ssh <target> 'bash -s'`.
|
||||||
|
# 2. Write output atomically to `servers/<host>/system-details.txt`.
|
||||||
|
# A failed SSH/run never clobbers the previous good snapshot.
|
||||||
|
#
|
||||||
|
# Exit status is non-zero if any host failed.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/refresh-server-info.sh show this help
|
||||||
|
# scripts/refresh-server-info.sh all refresh every host
|
||||||
|
# scripts/refresh-server-info.sh ana-docker refresh one host
|
||||||
|
# scripts/refresh-server-info.sh ana-docker ana-ml2 refresh several
|
||||||
|
# scripts/refresh-server-info.sh --dry-run all preview, no ssh
|
||||||
|
# scripts/refresh-server-info.sh --validate-only all checks only
|
||||||
|
# scripts/refresh-server-info.sh --validate-only <h> checks one host
|
||||||
|
#
|
||||||
|
# Discovery is validated before any ssh is attempted. Each host prints
|
||||||
|
# an indented "! <reason>" line per warning (unreadable files, empty or
|
||||||
|
# missing ssh-target, unresolvable name with no fallback, missing README
|
||||||
|
# or system-details, etc). Warnings never block the refresh — they're
|
||||||
|
# informational — but are surfaced so drift is visible.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
INSPECT="$SCRIPT_DIR/server_inspect.sh"
|
||||||
|
SERVERS_DIR="$REPO_ROOT/servers"
|
||||||
|
|
||||||
|
if [ ! -f "$INSPECT" ]; then
|
||||||
|
echo "error: $INSPECT not found" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
VALIDATE_ONLY=0
|
||||||
|
REQUESTED=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--validate-only|--validate) VALIDATE_ONLY=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,32p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*) echo "error: unknown flag $arg" >&2; exit 2 ;;
|
||||||
|
*) REQUESTED+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
discover_hosts() {
|
||||||
|
find "$SERVERS_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
# No positional args → show help. Fleet-wide operations must be explicit.
|
||||||
|
if [ "${#REQUESTED[@]}" -eq 0 ]; then
|
||||||
|
sed -n '2,32p' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The literal keyword "all" expands to every discovered host. Using it as
|
||||||
|
# a sentinel (rather than making "no args" mean "all") makes fleet-wide
|
||||||
|
# runs deliberate — useful when you might otherwise hit 20 hosts by accident.
|
||||||
|
if [ "${#REQUESTED[@]}" -eq 1 ] && [ "${REQUESTED[0]}" = "all" ]; then
|
||||||
|
mapfile -t HOSTS < <(discover_hosts)
|
||||||
|
else
|
||||||
|
# Check for 'all' mixed with other names — almost certainly a mistake.
|
||||||
|
for r in "${REQUESTED[@]}"; do
|
||||||
|
if [ "$r" = "all" ]; then
|
||||||
|
echo "error: 'all' must be the only argument when used" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
HOSTS=("${REQUESTED[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#HOSTS[@]}" -eq 0 ]; then
|
||||||
|
echo "error: no hosts found under $SERVERS_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_target() {
|
||||||
|
# Prints the SSH target to use for a given dir name.
|
||||||
|
# Preference order:
|
||||||
|
# 1. The dir name — if ssh's effective hostname (after ssh_config) resolves.
|
||||||
|
# 2. Contents of servers/<host>/ssh-target (first whitespace token).
|
||||||
|
# 3. The dir name as-is — let ssh fail with its own error.
|
||||||
|
local host="$1"
|
||||||
|
local effective
|
||||||
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
||||||
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
||||||
|
echo "$host"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local fallback="$SERVERS_DIR/$host/ssh-target"
|
||||||
|
if [ -f "$fallback" ]; then
|
||||||
|
awk 'NF{print $1; exit}' "$fallback"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_host() {
|
||||||
|
# Prints one warning per line. Empty output = clean.
|
||||||
|
local host="$1"
|
||||||
|
local dir="$SERVERS_DIR/$host"
|
||||||
|
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
printf '%s\n' "dir missing: $dir"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ ! -r "$dir" ] || [ ! -x "$dir" ]; then
|
||||||
|
printf '%s\n' "dir not readable/searchable (check permissions)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local stf="$dir/ssh-target"
|
||||||
|
if [ -e "$stf" ]; then
|
||||||
|
if [ ! -f "$stf" ]; then
|
||||||
|
printf '%s\n' "ssh-target is not a regular file"
|
||||||
|
elif [ ! -r "$stf" ]; then
|
||||||
|
printf '%s\n' "ssh-target not readable"
|
||||||
|
elif [ ! -s "$stf" ]; then
|
||||||
|
printf '%s\n' "ssh-target is empty"
|
||||||
|
else
|
||||||
|
local t
|
||||||
|
t=$(awk 'NF{print $1; exit}' "$stf" 2>/dev/null || true)
|
||||||
|
if [ -z "$t" ]; then
|
||||||
|
printf '%s\n' "ssh-target has no non-blank content"
|
||||||
|
elif [[ "$t" =~ [[:space:]] ]]; then
|
||||||
|
printf '%s\n' "ssh-target first token contains whitespace ('$t')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local effective=""
|
||||||
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
||||||
|
local resolves=0
|
||||||
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
||||||
|
resolves=1
|
||||||
|
fi
|
||||||
|
if [ "$resolves" -eq 0 ] && [ ! -s "$stf" ]; then
|
||||||
|
printf '%s\n' "name '$host' does not resolve and no ssh-target fallback present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$dir/README.md" ] || printf '%s\n' "README.md missing"
|
||||||
|
if [ ! -f "$dir/system-details.txt" ]; then
|
||||||
|
printf '%s\n' "system-details.txt missing (never refreshed)"
|
||||||
|
else
|
||||||
|
validate_snapshot "$dir/system-details.txt"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_snapshot() {
|
||||||
|
# Read the captured system-details.txt and yield a warning for each
|
||||||
|
# known problem marker that indicates the remote inspect ran but didn't
|
||||||
|
# have the rights / environment it needed.
|
||||||
|
local file="$1"
|
||||||
|
if grep -q 'docker daemon not reachable by current user' "$file"; then
|
||||||
|
printf '%s\n' "remote user cannot reach docker daemon (add to 'docker' group or fix socket perms)"
|
||||||
|
fi
|
||||||
|
if grep -qE '^Server:[[:space:]]*$' "$file" \
|
||||||
|
&& ! grep -q 'docker daemon not reachable by current user' "$file"; then
|
||||||
|
printf '%s\n' "docker 'Server:' line is blank (daemon down or inaccessible)"
|
||||||
|
fi
|
||||||
|
if grep -q '^nvidia-smi present, but failed' "$file"; then
|
||||||
|
printf '%s\n' "nvidia-smi failed on remote (driver broken or no permission)"
|
||||||
|
fi
|
||||||
|
if ! grep -q '^===== DONE =====' "$file"; then
|
||||||
|
printf '%s\n' "snapshot appears truncated (missing trailing '===== DONE =====' marker)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warnings() {
|
||||||
|
# $1 = indent, rest = warning lines
|
||||||
|
local indent="$1"; shift
|
||||||
|
local w
|
||||||
|
for w in "$@"; do
|
||||||
|
printf '%s! %s\n' "$indent" "$w"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
pad=0
|
||||||
|
for h in "${HOSTS[@]}"; do (( ${#h} > pad )) && pad=${#h}; done
|
||||||
|
|
||||||
|
if [ "$VALIDATE_ONLY" -eq 1 ]; then
|
||||||
|
printf 'Validating %d host(s):\n' "${#HOSTS[@]}"
|
||||||
|
total_warn=0
|
||||||
|
for host in "${HOSTS[@]}"; do
|
||||||
|
mapfile -t warnings < <(validate_host "$host")
|
||||||
|
if [ "${#warnings[@]}" -eq 0 ]; then
|
||||||
|
printf ' %-*s ok\n' "$pad" "$host"
|
||||||
|
else
|
||||||
|
printf ' %-*s %d warning(s)\n' "$pad" "$host" "${#warnings[@]}"
|
||||||
|
print_warnings " " "${warnings[@]}"
|
||||||
|
total_warn=$((total_warn + ${#warnings[@]}))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$total_warn" -gt 0 ]; then exit 1; fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Refreshing %d host(s):\n' "${#HOSTS[@]}"
|
||||||
|
failed=()
|
||||||
|
for host in "${HOSTS[@]}"; do
|
||||||
|
out="$SERVERS_DIR/$host/system-details.txt"
|
||||||
|
tmp="$out.new"
|
||||||
|
|
||||||
|
mapfile -t warnings < <(validate_host "$host")
|
||||||
|
|
||||||
|
target=$(resolve_target "$host")
|
||||||
|
if [ "$target" = "$host" ]; then
|
||||||
|
label="$host"
|
||||||
|
else
|
||||||
|
label="$host → $target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf ' %-*s ' "$pad" "$label"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf 'would run: ssh %s bash -s < %s > %s\n' "$target" "$INSPECT" "$out"
|
||||||
|
[ "${#warnings[@]}" -gt 0 ] && print_warnings " " "${warnings[@]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$SERVERS_DIR/$host"
|
||||||
|
|
||||||
|
if ssh -o BatchMode=yes -o ConnectTimeout=10 "$target" 'bash -s' < "$INSPECT" > "$tmp" 2> "$tmp.err"; then
|
||||||
|
mv "$tmp" "$out"
|
||||||
|
rm -f "$tmp.err"
|
||||||
|
bytes=$(wc -c < "$out")
|
||||||
|
printf 'ok (%s bytes)\n' "$bytes"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
rm -f "$tmp"
|
||||||
|
err=$(head -n 1 "$tmp.err" 2>/dev/null || true)
|
||||||
|
rm -f "$tmp.err"
|
||||||
|
printf 'FAIL (rc=%d) %s\n' "$rc" "$err"
|
||||||
|
failed+=("$host")
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#warnings[@]}" -gt 0 ] && print_warnings " " "${warnings[@]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#failed[@]}" -gt 0 ]; then
|
||||||
|
printf '\n%d host(s) failed: %s\n' "${#failed[@]}" "${failed[*]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+166
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# server_inspect.sh — collect server details for writing Docker Compose files.
|
||||||
|
#
|
||||||
|
# Conventions this script assumes and reports on:
|
||||||
|
# - Compose files: /opt/docker/compose/<stack>/{docker-compose.yml,compose.yaml}
|
||||||
|
# - Config mounts: /opt/docker/conf/<stack>/...
|
||||||
|
# - Named volumes preferred over bind mounts for persistent state.
|
||||||
|
#
|
||||||
|
# Safe: read-only. No modifications are made.
|
||||||
|
# Usage:
|
||||||
|
# bash server_inspect.sh # print to stdout
|
||||||
|
# bash server_inspect.sh /tmp/report.txt # also save to file
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
OUT="${1:-}"
|
||||||
|
if [ -n "$OUT" ]; then exec > >(tee "$OUT") 2>&1; fi
|
||||||
|
|
||||||
|
hr() { printf '\n===== %s =====\n\n' "$*"; }
|
||||||
|
sub() { printf '\n----- %s -----\n' "$*"; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# --- HOST --------------------------------------------------------------------
|
||||||
|
hr "HOST"
|
||||||
|
echo "Hostname: $(hostname -f 2>/dev/null || hostname)"
|
||||||
|
echo "Date: $(date -Iseconds)"
|
||||||
|
echo "Uptime: $(uptime -p 2>/dev/null || uptime)"
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
echo "OS: ${PRETTY_NAME:-unknown}"
|
||||||
|
fi
|
||||||
|
echo "Kernel: $(uname -r)"
|
||||||
|
echo "Arch: $(uname -m)"
|
||||||
|
|
||||||
|
# --- HARDWARE ----------------------------------------------------------------
|
||||||
|
hr "HARDWARE"
|
||||||
|
if [ -f /proc/cpuinfo ]; then
|
||||||
|
echo "CPU cores: $(grep -c ^processor /proc/cpuinfo)"
|
||||||
|
echo "CPU model: $(awk -F: '/model name/ {print $2; exit}' /proc/cpuinfo | sed 's/^[[:space:]]*//')"
|
||||||
|
fi
|
||||||
|
if [ -f /proc/meminfo ]; then
|
||||||
|
awk '/^MemTotal:|^MemAvailable:/ {printf "%-11s %.1f GB\n", $1, $2/1024/1024}' /proc/meminfo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- GPUS --------------------------------------------------------------------
|
||||||
|
hr "GPUS"
|
||||||
|
if have nvidia-smi; then
|
||||||
|
nvidia-smi --query-gpu=index,name,memory.total,memory.free,driver_version --format=csv
|
||||||
|
else
|
||||||
|
echo "nvidia-smi not present (no NVIDIA GPUs or driver not installed)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- FILESYSTEM --------------------------------------------------------------
|
||||||
|
hr "FILESYSTEMS (df)"
|
||||||
|
df -h -x tmpfs -x devtmpfs -x overlay 2>/dev/null
|
||||||
|
|
||||||
|
hr "PERSISTENT MOUNTS (/etc/fstab, non-comment)"
|
||||||
|
if [ -r /etc/fstab ]; then
|
||||||
|
grep -vE '^\s*(#|$)' /etc/fstab
|
||||||
|
fi
|
||||||
|
|
||||||
|
hr "TARGETED DATA PATHS"
|
||||||
|
for path in /tank /models /opt /opt/docker /opt/docker/compose /opt/docker/conf \
|
||||||
|
/var/lib/docker /data /srv; do
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
echo "$path (total: ${size:-?})"
|
||||||
|
ls -la --time-style=long-iso "$path" 2>/dev/null | sed 's/^/ /' | head -30
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- DOCKER ------------------------------------------------------------------
|
||||||
|
hr "DOCKER"
|
||||||
|
if ! have docker; then
|
||||||
|
echo "docker not installed"
|
||||||
|
else
|
||||||
|
docker version --format 'Server: {{.Server.Version}} Client: {{.Client.Version}}' 2>/dev/null \
|
||||||
|
|| echo "docker daemon not reachable by current user"
|
||||||
|
|
||||||
|
sub "docker info"
|
||||||
|
docker info --format 'Containers: {{.Containers}} (running {{.ContainersRunning}}, paused {{.ContainersPaused}}, stopped {{.ContainersStopped}})
|
||||||
|
Images: {{.Images}}
|
||||||
|
Runtimes: {{.Runtimes}}
|
||||||
|
Default runtime: {{.DefaultRuntime}}
|
||||||
|
Storage driver: {{.Driver}}
|
||||||
|
Root dir: {{.DockerRootDir}}
|
||||||
|
Server version: {{.ServerVersion}}' 2>/dev/null
|
||||||
|
|
||||||
|
sub "running containers"
|
||||||
|
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null
|
||||||
|
|
||||||
|
sub "all containers"
|
||||||
|
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null
|
||||||
|
|
||||||
|
sub "networks"
|
||||||
|
docker network ls --format 'table {{.Name}}\t{{.Driver}}\t{{.Scope}}' 2>/dev/null
|
||||||
|
|
||||||
|
sub "networks (external, non-default — worth knowing for compose external: true)"
|
||||||
|
docker network ls --filter driver=bridge --format '{{.Name}}' 2>/dev/null \
|
||||||
|
| grep -vE '^(bridge|host|none)$' || true
|
||||||
|
|
||||||
|
sub "named volumes"
|
||||||
|
docker volume ls --format 'table {{.Name}}\t{{.Driver}}' 2>/dev/null
|
||||||
|
|
||||||
|
sub "compose projects currently running"
|
||||||
|
docker ps --format '{{.Label "com.docker.compose.project"}}' 2>/dev/null \
|
||||||
|
| sort -u | grep -v '^$' || echo "(none)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- COMPOSE FILES -----------------------------------------------------------
|
||||||
|
hr "COMPOSE FILES (/opt/docker/compose/)"
|
||||||
|
if [ -d /opt/docker/compose ]; then
|
||||||
|
find /opt/docker/compose -maxdepth 3 -type f \
|
||||||
|
\( -name 'docker-compose.y*ml' -o -name 'compose.y*ml' \) 2>/dev/null \
|
||||||
|
| sort | while read -r f; do
|
||||||
|
printf '\n>>> %s\n' "$f"
|
||||||
|
cat "$f"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "/opt/docker/compose not present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- CONFIG LAYOUT -----------------------------------------------------------
|
||||||
|
hr "CONFIG LAYOUT (/opt/docker/conf/ — top 200 entries)"
|
||||||
|
if [ -d /opt/docker/conf ]; then
|
||||||
|
find /opt/docker/conf -maxdepth 4 2>/dev/null | sort | head -200
|
||||||
|
else
|
||||||
|
echo "/opt/docker/conf not present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- PORTS -------------------------------------------------------------------
|
||||||
|
hr "LISTENING PORTS"
|
||||||
|
if have ss; then
|
||||||
|
ss -tlnH 2>/dev/null | awk '{print $4}' | sort -u
|
||||||
|
elif have netstat; then
|
||||||
|
netstat -tln 2>/dev/null | awk 'NR>2 {print $4}' | sort -u
|
||||||
|
else
|
||||||
|
echo "ss and netstat both unavailable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- HF / MODEL CACHES -------------------------------------------------------
|
||||||
|
hr "MODEL / HUGGINGFACE CACHES"
|
||||||
|
for path in /tank/aimodels/huggingface /tank/aimodels/llm ~/.cache/huggingface \
|
||||||
|
/data/huggingface /opt/huggingface; do
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
echo "$path (${size:-?})"
|
||||||
|
if [ -d "$path/hub" ]; then
|
||||||
|
echo " hub entries:"
|
||||||
|
ls "$path/hub" 2>/dev/null | sed 's/^/ /' | head -30
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- SYSTEMD SERVICES (docker-adjacent) --------------------------------------
|
||||||
|
hr "DOCKER-ADJACENT SYSTEMD SERVICES"
|
||||||
|
if have systemctl; then
|
||||||
|
systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null \
|
||||||
|
| awk '{print $1, $4}' \
|
||||||
|
| grep -Ei 'docker|container|traefik|nvidia' || echo "(none matching)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
hr "DONE"
|
||||||
|
echo "Paste the above back into the chat, or pass a path as argv[1] to save."
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sync-stacks.sh — pull /opt/docker/{compose,conf}/<stack>/ from every
|
||||||
|
# server into version-controlled `stacks-mirror/<host>/<stack>/`.
|
||||||
|
#
|
||||||
|
# Layout (flat per stack):
|
||||||
|
# stacks-mirror/<host>/<stack>/ <- mirrors /opt/docker/compose/<stack>/
|
||||||
|
# stacks-mirror/<host>/<stack>/conf/ <- mirrors /opt/docker/conf/<stack>/
|
||||||
|
#
|
||||||
|
# Opt-out (per-stack, per-kind):
|
||||||
|
# stacks-mirror/<host>/<stack>/.no-sync → skip stack entirely
|
||||||
|
# stacks-mirror/<host>/<stack>/conf/.no-sync → skip conf only
|
||||||
|
# The marker file is preserved; only the rsync is suppressed. Create the
|
||||||
|
# marker manually for any stack you don't want mirrored.
|
||||||
|
#
|
||||||
|
# Secrets and runtime state are always excluded regardless of opt-out:
|
||||||
|
# .env, .env.*, acme.json, client_secrets.json,
|
||||||
|
# *.pem, *.key, *.crt, *.pfx,
|
||||||
|
# *.sqlite, *.sqlite3, *.db, *.log, *.log.*, *.pid,
|
||||||
|
# hub/, logs/
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sync-stacks.sh # pull from every discovered host
|
||||||
|
# scripts/sync-stacks.sh ana-docker nh3-docker
|
||||||
|
# scripts/sync-stacks.sh --dry-run # show what would change, no writes
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v rsync >/dev/null 2>&1; then
|
||||||
|
echo "error: rsync is not installed on this workstation" >&2
|
||||||
|
echo " install it (e.g. 'sudo apt install rsync') and ensure every remote host has it too" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SERVERS_DIR="$REPO_ROOT/servers"
|
||||||
|
MIRROR_DIR="$REPO_ROOT/stacks-mirror"
|
||||||
|
|
||||||
|
EXCLUDES=(
|
||||||
|
# Include .env.example / *.env.example templates before the broader
|
||||||
|
# .env* exclude — rsync processes these in order, first match wins.
|
||||||
|
--include='.env.example'
|
||||||
|
--include='*.env.example'
|
||||||
|
--exclude=.env
|
||||||
|
--exclude='.env.*'
|
||||||
|
--exclude=acme.json
|
||||||
|
--exclude=client_secrets.json
|
||||||
|
--exclude='*.pem'
|
||||||
|
--exclude='*.key'
|
||||||
|
--exclude='*.crt'
|
||||||
|
--exclude='*.pfx'
|
||||||
|
--exclude='*.sqlite'
|
||||||
|
--exclude='*.sqlite3'
|
||||||
|
--exclude='*.db'
|
||||||
|
--exclude='*.log'
|
||||||
|
--exclude='*.log.*'
|
||||||
|
--exclude='*.pid'
|
||||||
|
--exclude='hub/'
|
||||||
|
--exclude='logs/'
|
||||||
|
)
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
REQUESTED=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
|
||||||
|
-*) echo "error: unknown flag $arg" >&2; exit 2 ;;
|
||||||
|
*) REQUESTED+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
resolve_target() {
|
||||||
|
local host="$1"
|
||||||
|
local effective
|
||||||
|
effective=$(ssh -G "$host" 2>/dev/null | awk '/^hostname /{print $2; exit}')
|
||||||
|
if [ -n "$effective" ] && getent hosts "$effective" >/dev/null 2>&1; then
|
||||||
|
echo "$host"; return
|
||||||
|
fi
|
||||||
|
local fb="$SERVERS_DIR/$host/ssh-target"
|
||||||
|
if [ -f "$fb" ]; then awk 'NF{print $1; exit}' "$fb"; return; fi
|
||||||
|
echo "$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_remote_subdirs() {
|
||||||
|
# $1 = ssh target, $2 = remote parent path
|
||||||
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$1" \
|
||||||
|
"find '$2' -maxdepth 1 -mindepth 1 -type d -printf '%f\n' 2>/dev/null | sort" \
|
||||||
|
2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_one() {
|
||||||
|
# $1 = host, $2 = ssh target, $3 = stack,
|
||||||
|
# $4 = 'compose'|'conf' (kind),
|
||||||
|
# $5 = local dest dir
|
||||||
|
local host="$1" target="$2" stack="$3" kind="$4" dest="$5"
|
||||||
|
local remote_src="/opt/docker/$kind/$stack/"
|
||||||
|
local skip="$dest/.no-sync"
|
||||||
|
local label
|
||||||
|
if [ "$kind" = conf ]; then label='conf '; else label='compose'; fi
|
||||||
|
|
||||||
|
mkdir -p "$dest"
|
||||||
|
|
||||||
|
if [ -f "$skip" ]; then
|
||||||
|
printf ' %s skip (.no-sync)\n' "$label"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local extra=()
|
||||||
|
# Don't recurse into conf/ from the compose side — it's its own mirror target.
|
||||||
|
[ "$kind" = compose ] && extra+=(--exclude='conf/')
|
||||||
|
[ "$DRY_RUN" -eq 1 ] && extra+=(--dry-run)
|
||||||
|
|
||||||
|
local err rc=0
|
||||||
|
err=$(
|
||||||
|
rsync -az --delete --info=stats0,flist0 \
|
||||||
|
"${EXCLUDES[@]}" "${extra[@]}" \
|
||||||
|
"$target:$remote_src" "$dest/" 2>&1
|
||||||
|
) || rc=$?
|
||||||
|
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
printf ' %s FAIL (rc=%d) %s\n' "$label" "$rc" "$(echo "$err" | head -n 1)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf ' %s dry-run ok\n' "$label"
|
||||||
|
else
|
||||||
|
printf ' %s ok\n' "$label"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${#REQUESTED[@]}" -eq 0 ]; then
|
||||||
|
mapfile -t HOSTS < <(find "$SERVERS_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
|
||||||
|
else
|
||||||
|
HOSTS=("${REQUESTED[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#HOSTS[@]}" -eq 0 ] && { echo "error: no hosts found" >&2; exit 2; }
|
||||||
|
[ "$DRY_RUN" -eq 1 ] && echo "(dry-run)"
|
||||||
|
|
||||||
|
mkdir -p "$MIRROR_DIR"
|
||||||
|
total_fail=0
|
||||||
|
|
||||||
|
for host in "${HOSTS[@]}"; do
|
||||||
|
target=$(resolve_target "$host")
|
||||||
|
printf '%s (%s):\n' "$host" "$target"
|
||||||
|
|
||||||
|
mapfile -t compose_stacks < <(list_remote_subdirs "$target" /opt/docker/compose)
|
||||||
|
mapfile -t conf_stacks < <(list_remote_subdirs "$target" /opt/docker/conf)
|
||||||
|
|
||||||
|
if [ "${#compose_stacks[@]}" -eq 0 ] && [ "${#conf_stacks[@]}" -eq 0 ]; then
|
||||||
|
printf ' (no stacks discovered — check ssh + remote /opt/docker layout)\n'
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Union of stack names.
|
||||||
|
mapfile -t all_stacks < <(printf '%s\n' "${compose_stacks[@]}" "${conf_stacks[@]}" | sort -u | grep .)
|
||||||
|
|
||||||
|
# Warn about local stacks that no longer exist on the remote.
|
||||||
|
if [ -d "$MIRROR_DIR/$host" ]; then
|
||||||
|
for local_stack in "$MIRROR_DIR/$host"/*/; do
|
||||||
|
[ -d "$local_stack" ] || continue
|
||||||
|
name=$(basename "$local_stack")
|
||||||
|
if ! printf '%s\n' "${all_stacks[@]}" | grep -qxF "$name"; then
|
||||||
|
printf ' ! %s exists locally but not on remote (stale — remove manually if intentional)\n' "$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for stack in "${all_stacks[@]}"; do
|
||||||
|
printf ' %s\n' "$stack"
|
||||||
|
stack_root="$MIRROR_DIR/$host/$stack"
|
||||||
|
if [ -f "$stack_root/.no-sync" ]; then
|
||||||
|
printf ' skip (.no-sync at stack root)\n'
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if printf '%s\n' "${compose_stacks[@]}" | grep -qxF "$stack"; then
|
||||||
|
sync_one "$host" "$target" "$stack" compose "$stack_root" || total_fail=$((total_fail+1))
|
||||||
|
fi
|
||||||
|
if printf '%s\n' "${conf_stacks[@]}" | grep -qxF "$stack"; then
|
||||||
|
sync_one "$host" "$target" "$stack" conf "$stack_root/conf" || total_fail=$((total_fail+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$total_fail" -gt 0 ]; then
|
||||||
|
printf '\n%d sync operation(s) failed\n' "$total_fail" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# ana-docker
|
||||||
|
|
||||||
|
General-purpose Docker host for the Anaheim colo. Runs everything at `10.250.0.0/16` that doesn't need a GPU — pair it with ana-ml2, which handles the GPU workloads.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
- **LAN IP:** 10.250.50.70
|
||||||
|
- **FQDN:** `ana-docker.phasefinal.com`
|
||||||
|
- **SSH:** standard port 22
|
||||||
|
- **Traefik entrypoints:** 80/443 terminate here; cert resolver `anaprod` (Let's Encrypt)
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **CPU:** 8 vCPU (QEMU virtual — this is a VM)
|
||||||
|
- **RAM:** 15.6 GB
|
||||||
|
- **GPUs:** none
|
||||||
|
- **Storage:** 245 GB root (ext4) + NFS mounts from `10.250.50.50` (TrueNAS)
|
||||||
|
- **OS:** Debian 12 (bookworm), kernel 6.1.x
|
||||||
|
- **Docker:** 20.10.24
|
||||||
|
|
||||||
|
## Key paths
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/opt/docker/compose/<stack>/` | Compose files (also a git repo) |
|
||||||
|
| `/opt/docker/conf/<stack>/` | Config bind mounts |
|
||||||
|
| `/opt/docker/data/` | Service state (legacy — most stacks now use named volumes) |
|
||||||
|
| `/mnt/compose` | NFS — `10.250.50.50:/mnt/docker` |
|
||||||
|
| `/mnt/backup` | NFS — `10.250.50.50:/mnt/backup` (restic target) |
|
||||||
|
| `/mnt/tnvms` | NFS — `10.250.50.50:/mnt/pve-VMStorage` |
|
||||||
|
|
||||||
|
## Running stacks
|
||||||
|
|
||||||
|
| Stack | Port(s) | Notes |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| traefik | 80 / 443 / 8380 | Reverse proxy + TLS (cert resolver `anaprod`) |
|
||||||
|
| crowdsec (+ blocklist-mirror) | 41412 | IPS; bouncer runs as a Traefik plugin |
|
||||||
|
| gitea | 3000 / 222 | Git hosting — `gitea.phasefinal.com` |
|
||||||
|
| vaultwarden | 9080 | Password vault — `vaultwarden.phasefinal.com` |
|
||||||
|
| synapse (+ synapse-db, element-web) | internal | Matrix homeserver — `matrix.phasefinal.com`, client at `chat.phasefinal.com` |
|
||||||
|
| seafile (+ mariadb, memcached) | 9180 | File sync — `seafile.phasefinal.com` |
|
||||||
|
| searxng | 9996 | Private search metaengine |
|
||||||
|
| openwebui | 3100 | Chat UI frontend |
|
||||||
|
| sillytavern | 8100 | Chat UI |
|
||||||
|
| mailrise | 8025 | SMTP-to-notification gateway |
|
||||||
|
| rustdesk (hbbs + hbbr) | host-net 21115-21119 | Remote desktop relay — `rustdesk.phasefinal.com` |
|
||||||
|
| dockge | 5001 | Docker stack management UI |
|
||||||
|
| beszel | 8090 | Fleet metrics hub (+ local agent); agents on the other hosts report here |
|
||||||
|
| dozzle (hub as `dozzle-hub`) | 8088 | Fleet log viewer; agents on the other hosts report here |
|
||||||
|
| restic rest-server | 8000 | Anaheim-side restic endpoint (writes to TrueNAS NFS at `/mnt/backup/restic/repo/ana/`); paired with `rest-server-nh3` on the Synology for the NH3 side |
|
||||||
|
| backrest | 9898 | Fleet-wide restic snapshot viewer / restore UI — points at both rest-servers |
|
||||||
|
| it-tools | 8780 | Dev utilities |
|
||||||
|
| mattermost | — | Stopped; kept around for reference |
|
||||||
|
|
||||||
|
Portainer was retired from this host; stack management is now handled via Dockge + Beszel.
|
||||||
|
|
||||||
|
## Refresh state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh ana-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Latest snapshot: `system-details.txt` (regenerate as needed).
|
||||||
|
|
||||||
|
## Placement rule
|
||||||
|
|
||||||
|
If a new stack needs a GPU it goes on **ana-ml2**, otherwise it lands here.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
10.250.50.70
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
|||||||
|
# ana-ml2
|
||||||
|
|
||||||
|
Primary AI inference host for PFI.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
- **LAN IP:** 10.250.50.54
|
||||||
|
- **SSH:** standard port 22
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **CPU:** AMD EPYC 9254 24-core (96 threads)
|
||||||
|
- **RAM:** 566 GB
|
||||||
|
- **GPUs:** 2x NVIDIA RTX 6000 Ada Generation (46 GB VRAM each, GPU 0 and GPU 1)
|
||||||
|
- **Storage:** ZFS `zroot` (434 GB root) + `tank` pool (8.6 TB at `/tank`)
|
||||||
|
- **OS:** Debian 13 (trixie), kernel 6.12.x
|
||||||
|
- **Docker:** 29.3.1, runtimes: runc (default), nvidia, io.containerd.runc.v2
|
||||||
|
|
||||||
|
## Key paths
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/opt/docker/compose/<stack>/` | Compose files |
|
||||||
|
| `/opt/docker/conf/<stack>/` | Config bind mounts |
|
||||||
|
| `/tank/aimodels/huggingface/` | HF cache (267 GB, pre-downloaded models) |
|
||||||
|
| `/tank/aimodels/llm/` | Legacy GGUF models (790 GB, referenced by llama-swap as `/models/`) |
|
||||||
|
| `/var/lib/docker/` | Docker data (on zroot) |
|
||||||
|
|
||||||
|
## Running stacks
|
||||||
|
|
||||||
|
| Stack | Port | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| llama-swap | 9292 | GGUF model server via llama.cpp |
|
||||||
|
| vllm-embed (Qwen3) | 8001 | OpenAI-compatible embeddings; part of the `vllm-qwen3` stack (GPU 1) |
|
||||||
|
| vllm-rerank (Qwen3) | 8002 | OpenAI-compatible reranker; part of the `vllm-qwen3` stack (GPU 1) |
|
||||||
|
| dockge | 5001 | Docker stack management UI |
|
||||||
|
| dozzle-agent | 7007 | Log agent; reports to the Dozzle hub on ana-docker |
|
||||||
|
| beszel-agent | 45876 | Metrics agent; reports to the Beszel hub on ana-docker |
|
||||||
|
|
||||||
|
**Retired since last README update:**
|
||||||
|
- `infinity` — replaced by `vllm-qwen3` after the upstream image stopped shipping a `transformers` build that knew Qwen3.
|
||||||
|
- `LibreChat (+ rag_api, vectordb, mongodb, meilisearch)` — removed from this host.
|
||||||
|
- `searxng` — now hosted on ana-docker for the whole fleet.
|
||||||
|
- Residual networks (`librechat_default`, `kokoro-tts-gpu_default`) from prior experiments are still present; safe to `docker network rm` at leisure.
|
||||||
|
|
||||||
|
## Refresh state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh ana-ml2
|
||||||
|
```
|
||||||
|
|
||||||
|
Latest snapshot: `system-details.txt` (regenerate as needed).
|
||||||
|
|
||||||
|
## GPU allocation policy
|
||||||
|
|
||||||
|
By default, no container is pinned. For predictable performance when multiple GPU workloads run concurrently:
|
||||||
|
|
||||||
|
- **GPU 0:** heavy LLM (llama-swap big models).
|
||||||
|
- **GPU 1:** light services (both vllm-qwen3 services share this GPU via `--gpu-memory-utilization`).
|
||||||
|
|
||||||
|
Use `deploy.resources.reservations.devices[].device_ids: ["<id>"]` in compose to pin.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
10.250.50.54
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
|||||||
|
# esh-docker-vm
|
||||||
|
|
||||||
|
General-purpose Docker host at the **ESH home-lab site** (`esteban.net` / `10.0.0.0/8` space). VM, no GPU. Separate scope from the PFI colo work but tracked here because it's part of the same fleet.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
- **LAN IP:** 10.0.50.45
|
||||||
|
- **FQDN:** `esh-vm-docker.esteban.net`
|
||||||
|
- **Subnet:** `10.0.50.0/24` (inferred from macvlan config)
|
||||||
|
- **SSH:** standard port 22
|
||||||
|
- **Traefik entrypoints:** 80/443 terminate here; DNS-01 challenge via AWS Route53; protected by CrowdSec Traefik plugin
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **CPU:** 16 vCPU (QEMU virtual — VM)
|
||||||
|
- **RAM:** 15.6 GB
|
||||||
|
- **GPUs:** none
|
||||||
|
- **Storage:** 250 GB root (ext4) + NFS mounts from `10.0.50.50`
|
||||||
|
- **OS:** Debian 12 (bookworm), kernel 6.1.x
|
||||||
|
- **Docker:** running (daemon socket at `/var/run/docker.sock`; also listens on `:2375`)
|
||||||
|
|
||||||
|
## Key paths
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/opt/docker/compose/<stack>/` | Compose files (also a git repo) |
|
||||||
|
| `/opt/docker/conf/<stack>/` | Config bind mounts |
|
||||||
|
| `/opt/docker/docker-bu/` | Legacy backup staging (ad-hoc) |
|
||||||
|
| `/mnt/compose` | NFS — `10.0.50.50:/mnt/compose` |
|
||||||
|
| `/mnt/backup` | NFS — `10.0.50.50:/mnt/backup` (volume-backup sidecars write here) |
|
||||||
|
| `/mnt/documents` | NFS — `10.0.50.50:/mnt/documents` (Paperless-ngx source) |
|
||||||
|
| `/mnt/books` | NFS — `10.0.50.50:/mnt/books` (Calibre library) |
|
||||||
|
|
||||||
|
## Running stacks
|
||||||
|
|
||||||
|
| Stack | Port(s) | Notes |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| traefik | 80 / 443 / 8380 | Reverse proxy + TLS (AWS Route53 DNS-01); CrowdSec bouncer plugin |
|
||||||
|
| cloudflare-ddns-esh | — | Keeps the `esteban.net` record in sync with the dynamic WAN IP |
|
||||||
|
| adguard | 53 / 853 / 8080 / 8443 / 3000 / 784 | DNS + DoT for the ESH site |
|
||||||
|
| dockge | 5001 | Docker stack management UI |
|
||||||
|
| homepage | 5100 | Dashboard (`eshhome` / `eshhome.esteban.net`) |
|
||||||
|
| uptime-kuma | 3001 | Service uptime monitor |
|
||||||
|
| homeassistant | macvlan `10.0.50.46:8123` | Home automation (direct LAN IP via macvlan on `ens18`) |
|
||||||
|
| esphome | host net / 6052 | ESPHome firmware dashboard |
|
||||||
|
| mosquitto | 1883 | MQTT broker |
|
||||||
|
| calibre | 8082 / 8181 / 8281 | Ebook server |
|
||||||
|
| calibre-web | 8083 | Ebook web UI |
|
||||||
|
| paperless-ngx (+ redis broker + volume-backup sidecar) | 8200 | Document archive; Postgres on `10.0.50.60:5432` |
|
||||||
|
| pgadmin (+ volume-backup sidecar) | 5050 | Postgres admin UI |
|
||||||
|
| drawio | 8087 / 8447 | Diagram editor |
|
||||||
|
| dozzle-agent | 7007 | Log agent; feeds ana-docker's Dozzle hub |
|
||||||
|
| beszel-esh-vm-docker (agent) | 45876 | Metrics agent; feeds ana-docker's Beszel hub |
|
||||||
|
| portainer (+ portainer_agent) | 9443 / 8000 / 9001 | Container management UI (ad-hoc, no compose dir under `/opt/docker/compose/`) |
|
||||||
|
|
||||||
|
## Refresh state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh esh-docker-vm
|
||||||
|
```
|
||||||
|
|
||||||
|
Latest snapshot: `system-details.txt` (regenerate as needed).
|
||||||
|
|
||||||
|
## Cross-site monitoring
|
||||||
|
|
||||||
|
Like `nh3-docker`, this host runs **Dozzle** and **Beszel** agents that report back to the hubs on `ana-docker`, so container logs and metrics show up alongside PFI hosts in the shared dashboards.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Macvlan for Home Assistant** — the HA container gets its own LAN IP (`10.0.50.46`) via a macvlan network on `ens18`, avoiding NAT so multicast/mDNS for HA discovery works cleanly.
|
||||||
|
- **External Postgres** — Paperless-ngx connects to a DB running elsewhere (`10.0.50.60:5432`), not a sidecar. Paperless creds in that compose file currently look like defaults; rotate before exposing.
|
||||||
|
- **Volume backups already in place** — `paperless-ngx` and `pgadmin` include `offen/docker-volume-backup:latest` sidecars that tar named volumes to `/mnt/backup/docker/esh-vm-docker/<stack>/`. When the fleet-wide restic plan lands, decide whether to subsume these or leave the per-stack sidecars alone.
|
||||||
|
|
||||||
|
## Placement rule
|
||||||
|
|
||||||
|
Home-lab workloads for the ESH site go here. Not part of the PFI colo topology.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
10.0.50.45
|
||||||
@@ -0,0 +1,961 @@
|
|||||||
|
|
||||||
|
===== HOST =====
|
||||||
|
|
||||||
|
Hostname: esh-vm-docker.esteban.net
|
||||||
|
Date: 2026-04-19T22:15:59-07:00
|
||||||
|
Uptime: up 2 weeks, 6 days, 5 hours, 59 minutes
|
||||||
|
OS: Debian GNU/Linux 12 (bookworm)
|
||||||
|
Kernel: 6.1.0-41-amd64
|
||||||
|
Arch: x86_64
|
||||||
|
|
||||||
|
===== HARDWARE =====
|
||||||
|
|
||||||
|
CPU cores: 16
|
||||||
|
CPU model: QEMU Virtual CPU version 2.5+
|
||||||
|
MemTotal: 15.6 GB
|
||||||
|
MemAvailable: 11.4 GB
|
||||||
|
|
||||||
|
===== GPUS =====
|
||||||
|
|
||||||
|
nvidia-smi not present (no NVIDIA GPUs or driver not installed)
|
||||||
|
|
||||||
|
===== FILESYSTEMS (df) =====
|
||||||
|
|
||||||
|
Filesystem Size Used Avail Use% Mounted on
|
||||||
|
/dev/sda1 250G 122G 117G 52% /
|
||||||
|
10.0.50.50:/mnt/backup 92T 26G 92T 1% /mnt/backup
|
||||||
|
10.0.50.50:/mnt/compose 1.5T 4.1G 1.5T 1% /mnt/compose
|
||||||
|
10.0.50.50:/mnt/documents 92T 0 92T 0% /mnt/documents
|
||||||
|
10.0.50.50:/mnt/books 92T 96G 92T 1% /mnt/books
|
||||||
|
|
||||||
|
===== PERSISTENT MOUNTS (/etc/fstab, non-comment) =====
|
||||||
|
|
||||||
|
UUID=a2e1bc05-3afa-4803-9ab2-dfee1d5c9e6c / ext4 errors=remount-ro 0 1
|
||||||
|
UUID=4f4fa84b-2b71-4ee5-9be4-2deb4c287d8e none swap sw 0 0
|
||||||
|
/dev/sr0 /media/cdrom0 udf,iso9660 user,noauto 0 0
|
||||||
|
10.0.50.50:/mnt/compose /mnt/compose nfs defaults 0 0
|
||||||
|
10.0.50.50:/mnt/books /mnt/books nfs defaults 0 0
|
||||||
|
10.0.50.50:/mnt/documents /mnt/documents nfs defaults 0 0
|
||||||
|
10.0.50.50:/mnt/backup /mnt/backup nfs defaults 0 0
|
||||||
|
|
||||||
|
===== TARGETED DATA PATHS =====
|
||||||
|
|
||||||
|
/opt (total: 62M)
|
||||||
|
total 16
|
||||||
|
drwxr-xr-x 4 root root 4096 2025-03-18 23:59 .
|
||||||
|
drwxr-xr-x 19 root root 4096 2025-12-29 14:50 ..
|
||||||
|
drwxr-xr-x 5 lkraven lkraven 4096 2025-03-18 23:58 docker
|
||||||
|
drwxrwxrwx 6 lkraven lkraven 4096 2024-06-13 22:00 docker-bu
|
||||||
|
|
||||||
|
/opt/docker (total: 53M)
|
||||||
|
total 32
|
||||||
|
drwxr-xr-x 5 lkraven lkraven 4096 2025-03-18 23:58 .
|
||||||
|
drwxr-xr-x 4 root root 4096 2025-03-18 23:59 ..
|
||||||
|
drwxr-xr-x 18 lkraven lkraven 4096 2026-04-19 01:08 compose
|
||||||
|
drwxr-xr-x 7 lkraven lkraven 4096 2025-11-29 00:26 conf
|
||||||
|
drwxr-xr-x 8 lkraven lkraven 4096 2025-03-18 23:59 .git
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 34 2025-03-18 23:58 .gitignore
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 1059 2025-03-18 23:58 LICENSE
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 60 2025-03-18 23:58 README.md
|
||||||
|
|
||||||
|
/opt/docker/compose (total: 172K)
|
||||||
|
total 76
|
||||||
|
drwxr-xr-x 18 lkraven lkraven 4096 2026-04-19 01:08 .
|
||||||
|
drwxr-xr-x 5 lkraven lkraven 4096 2025-03-18 23:58 ..
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 adguard
|
||||||
|
drwxr-xr-x 2 root root 4096 2026-04-19 01:07 beszel-esh-vm-docker
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 calibre
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 calibre-web
|
||||||
|
drwxr-xr-x 2 root root 4096 2025-10-03 22:01 cloudflare-ddns-esh
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 dockge
|
||||||
|
drwxr-xr-x 2 root root 4096 2026-04-19 00:30 dozzle-agent
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 drawio
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 esphome
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-08-31 17:39 homeassistant
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-05-19 22:10 homepage
|
||||||
|
drwxr-xr-x 2 root root 4096 2025-11-29 00:40 mosquitto
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-08-28 09:37 paperless-ngx
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-08-31 17:41 pgadmin
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 25 2025-03-18 23:58 README.md
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-05-19 22:10 traefik
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-03-18 23:58 uptimekuma
|
||||||
|
|
||||||
|
/opt/docker/conf (total: 50M)
|
||||||
|
total 28
|
||||||
|
drwxr-xr-x 7 lkraven lkraven 4096 2025-11-29 00:26 .
|
||||||
|
drwxr-xr-x 5 lkraven lkraven 4096 2025-03-18 23:58 ..
|
||||||
|
drwxr-xr-x 2 nas nas 4096 2026-03-20 21:00 calibre-web
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2025-10-03 22:13 cloudflare-ddns
|
||||||
|
drwxr-xr-x 5 lkraven lkraven 4096 2026-03-30 22:30 homepage
|
||||||
|
drwxr-xr-x 2 1883 1883 4096 2025-11-29 00:50 mosquitto
|
||||||
|
drwxr-xr-x 3 lkraven lkraven 4096 2025-03-18 23:58 traefik-esh
|
||||||
|
|
||||||
|
/var/lib/docker (total: 4.0K)
|
||||||
|
|
||||||
|
/srv (total: 20K)
|
||||||
|
total 12
|
||||||
|
drwxr-xr-x 3 root root 4096 2024-06-13 21:52 .
|
||||||
|
drwxr-xr-x 19 root root 4096 2025-12-29 14:50 ..
|
||||||
|
drwxrwxrwx 3 root root 4096 2024-06-13 21:54 backups
|
||||||
|
|
||||||
|
|
||||||
|
===== DOCKER =====
|
||||||
|
|
||||||
|
Server: 20.10.24+dfsg1 Client: 20.10.24+dfsg1
|
||||||
|
|
||||||
|
----- docker info -----
|
||||||
|
Containers: 24 (running 21, paused 0, stopped 3)
|
||||||
|
Images: 84
|
||||||
|
Runtimes: map[io.containerd.runc.v2:{runc [] <nil>} io.containerd.runtime.v1.linux:{runc [] <nil>} runc:{runc [] <nil>}]
|
||||||
|
Default runtime: runc
|
||||||
|
Storage driver: overlay2
|
||||||
|
Root dir: /var/lib/docker
|
||||||
|
Server version: 20.10.24+dfsg1
|
||||||
|
|
||||||
|
----- running containers -----
|
||||||
|
NAMES IMAGE STATUS PORTS
|
||||||
|
beszel-agent henrygd/beszel-agent:latest Up 21 hours
|
||||||
|
dozzle-agent amir20/dozzle:latest Up 22 hours 0.0.0.0:7007->7007/tcp, 8080/tcp
|
||||||
|
traefik traefik:latest Up 3 days 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:8380->8080/tcp, :::8380->8080/tcp
|
||||||
|
paperless-ngx-webserver-1 ghcr.io/paperless-ngx/paperless-ngx:latest Up 2 weeks (healthy) 0.0.0.0:8200->8000/tcp, :::8200->8000/tcp
|
||||||
|
paperless-ngx-broker-1 redis:7 Up 2 weeks 6379/tcp
|
||||||
|
paperless-ngx-backup-1 offen/docker-volume-backup:latest Up 2 weeks
|
||||||
|
homepage ghcr.io/gethomepage/homepage:latest Up 2 weeks (healthy) 0.0.0.0:5100->3000/tcp, :::5100->3000/tcp
|
||||||
|
homeassistant homeassistant/home-assistant:latest Up 30 hours
|
||||||
|
mosquitto eclipse-mosquitto:latest Up 2 weeks 0.0.0.0:1883->1883/tcp, :::1883->1883/tcp
|
||||||
|
adguardhome adguard/adguardhome Up 2 weeks 67-68/udp, 0.0.0.0:53->53/udp, :::53->53/udp, 443/udp, 0.0.0.0:53->53/tcp, :::53->53/tcp, 853/udp, 0.0.0.0:853->853/tcp, :::853->853/tcp, 3000/udp, 5443/tcp, 0.0.0.0:3000->3000/tcp, 0.0.0.0:784->784/udp, :::3000->3000/tcp, :::784->784/udp, 5443/udp, 6060/tcp, 0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp
|
||||||
|
cloudflare-ddns timothyjmiller/cloudflare-ddns:latest Up 2 weeks
|
||||||
|
pgadmin4_container dpage/pgadmin4 Up 2 weeks 443/tcp, 0.0.0.0:5050->80/tcp, :::5050->80/tcp
|
||||||
|
pgadmin-backup-1 1127ad194f2f Up 2 weeks
|
||||||
|
esphome ghcr.io/esphome/esphome Up 2 weeks (healthy)
|
||||||
|
drawio jgraph/drawio Up 2 weeks (healthy) 0.0.0.0:8087->8080/tcp, :::8087->8080/tcp, 0.0.0.0:8447->8443/tcp, :::8447->8443/tcp
|
||||||
|
calibre-web lscr.io/linuxserver/calibre-web:latest Up 2 weeks 0.0.0.0:8083->8083/tcp, :::8083->8083/tcp
|
||||||
|
calibre lscr.io/linuxserver/calibre:latest Up 2 weeks 3000-3001/tcp, 0.0.0.0:8181->8181/tcp, :::8181->8181/tcp, 0.0.0.0:8082->8080/tcp, :::8082->8080/tcp, 0.0.0.0:8281->8081/tcp, :::8281->8081/tcp
|
||||||
|
45d2522a8cb6_uptime-kuma louislam/uptime-kuma:latest Up 2 weeks (healthy) 0.0.0.0:3001->3001/tcp, :::3001->3001/tcp
|
||||||
|
dockge-dockge-1 louislam/dockge:latest Up 2 weeks (healthy) 0.0.0.0:5001->5001/tcp, :::5001->5001/tcp
|
||||||
|
portainer_agent portainer/agent:2.19.4 Up 2 weeks 0.0.0.0:9001->9001/tcp, :::9001->9001/tcp
|
||||||
|
portainer portainer/portainer-ce:latest Up 2 weeks 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp, 9000/tcp
|
||||||
|
|
||||||
|
----- all containers -----
|
||||||
|
NAMES IMAGE STATUS
|
||||||
|
beszel-agent henrygd/beszel-agent:latest Up 21 hours
|
||||||
|
dozzle-agent amir20/dozzle:latest Up 22 hours
|
||||||
|
traefik traefik:latest Up 3 days
|
||||||
|
paperless-ngx-webserver-1 ghcr.io/paperless-ngx/paperless-ngx:latest Up 2 weeks (healthy)
|
||||||
|
paperless-ngx-broker-1 redis:7 Up 2 weeks
|
||||||
|
paperless-ngx-backup-1 offen/docker-volume-backup:latest Up 2 weeks
|
||||||
|
homepage ghcr.io/gethomepage/homepage:latest Up 2 weeks (healthy)
|
||||||
|
homeassistant homeassistant/home-assistant:latest Up 30 hours
|
||||||
|
mosquitto eclipse-mosquitto:latest Up 2 weeks
|
||||||
|
adguardhome adguard/adguardhome Up 2 weeks
|
||||||
|
friendly_maxwell 0745ced90756 Exited (13) 4 months ago
|
||||||
|
focused_pascal 0745ced90756 Exited (13) 4 months ago
|
||||||
|
pedantic_proskuriakova 0745ced90756 Exited (3) 4 months ago
|
||||||
|
cloudflare-ddns timothyjmiller/cloudflare-ddns:latest Up 2 weeks
|
||||||
|
pgadmin4_container dpage/pgadmin4 Up 2 weeks
|
||||||
|
pgadmin-backup-1 1127ad194f2f Up 2 weeks
|
||||||
|
esphome ghcr.io/esphome/esphome Up 2 weeks (healthy)
|
||||||
|
drawio jgraph/drawio Up 2 weeks (healthy)
|
||||||
|
calibre-web lscr.io/linuxserver/calibre-web:latest Up 2 weeks
|
||||||
|
calibre lscr.io/linuxserver/calibre:latest Up 2 weeks
|
||||||
|
45d2522a8cb6_uptime-kuma louislam/uptime-kuma:latest Up 2 weeks (healthy)
|
||||||
|
dockge-dockge-1 louislam/dockge:latest Up 2 weeks (healthy)
|
||||||
|
portainer_agent portainer/agent:2.19.4 Up 2 weeks
|
||||||
|
portainer portainer/portainer-ce:latest Up 2 weeks
|
||||||
|
|
||||||
|
----- networks -----
|
||||||
|
NAME DRIVER SCOPE
|
||||||
|
adguard_default bridge local
|
||||||
|
bridge bridge local
|
||||||
|
calibre-web_default bridge local
|
||||||
|
cloudflare-ddns-esh_default bridge local
|
||||||
|
homeassistant_macvlan_net macvlan local
|
||||||
|
homepage_default bridge local
|
||||||
|
host host local
|
||||||
|
none null local
|
||||||
|
pgadmin_default bridge local
|
||||||
|
traefik-net bridge local
|
||||||
|
|
||||||
|
----- networks (external, non-default — worth knowing for compose external: true) -----
|
||||||
|
adguard_default
|
||||||
|
calibre-web_default
|
||||||
|
cloudflare-ddns-esh_default
|
||||||
|
homepage_default
|
||||||
|
pgadmin_default
|
||||||
|
traefik-net
|
||||||
|
|
||||||
|
----- named volumes -----
|
||||||
|
VOLUME NAME DRIVER
|
||||||
|
6cdde6cf4b58151d434e8a3da23e77d9c04acdaa574a91ff7bbe60ea113a2adf local
|
||||||
|
6f7206c067f22a53bf7d0c737f74075f849c11fd535b884c4d3cbb73d275bc21 local
|
||||||
|
007fc81bd8008b22173f8e4a365b2ca43a37080902ab961a197dde0d8782c786 local
|
||||||
|
8b1ff384b6cb4859c64a21b51d32780027a3682dc1852a36617b59870ea15e32 local
|
||||||
|
adguard_adguard-confdir local
|
||||||
|
adguard_adguard-workdir local
|
||||||
|
ae127367f71c43b662b16c8232a135c59f2a873dcdf606dfa01967fad1ac915e local
|
||||||
|
beszel-esh-vm-docker_beszel_agent_data local
|
||||||
|
beszel-vm-esh-nas_beszel_agent_data local
|
||||||
|
c7a5978e1b811c4d5b15404e405c0ec32a541f8a921a054b0f725a3010a76684 local
|
||||||
|
dockge_dockge_data local
|
||||||
|
dozzle-agent_dozzle_agent_data local
|
||||||
|
homeassistant_homeassistant_data local
|
||||||
|
mosquitto_mosquitto_data local
|
||||||
|
mosquitto_mosquitto_log local
|
||||||
|
paperless-ngx_data local
|
||||||
|
paperless-ngx_media local
|
||||||
|
paperless-ngx_redisdata local
|
||||||
|
pgadmin_pgadmin-data local
|
||||||
|
portainer_data local
|
||||||
|
uptimekuma_uptime-kuma local
|
||||||
|
|
||||||
|
----- compose projects currently running -----
|
||||||
|
adguard
|
||||||
|
beszel-esh-vm-docker
|
||||||
|
calibre
|
||||||
|
calibre-web
|
||||||
|
cloudflare-ddns-esh
|
||||||
|
dockge
|
||||||
|
dozzle-agent
|
||||||
|
drawio
|
||||||
|
esphome
|
||||||
|
homeassistant
|
||||||
|
homepage
|
||||||
|
mosquitto
|
||||||
|
paperless-ngx
|
||||||
|
pgadmin
|
||||||
|
traefik
|
||||||
|
uptimekuma
|
||||||
|
|
||||||
|
===== COMPOSE FILES (/opt/docker/compose/) =====
|
||||||
|
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/adguard/docker-compose.yml
|
||||||
|
#version: "3"
|
||||||
|
services:
|
||||||
|
adguardhome:
|
||||||
|
image: adguard/adguardhome
|
||||||
|
container_name: adguardhome
|
||||||
|
ports:
|
||||||
|
- 53:53/tcp
|
||||||
|
- 53:53/udp
|
||||||
|
- 784:784/udp
|
||||||
|
- 853:853/tcp
|
||||||
|
- 3000:3000/tcp
|
||||||
|
- 8080:80/tcp
|
||||||
|
- 8443:443/tcp
|
||||||
|
volumes:
|
||||||
|
- adguard-workdir:/opt/adguardhome/work
|
||||||
|
- adguard-confdir:/opt/adguardhome/conf
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
# This means the container will be stopped during backup to ensure
|
||||||
|
# backup integrity. You can omit this label if stopping during backup
|
||||||
|
# not required. Can be omitted if false.
|
||||||
|
- docker-volume-backup.stop-during-backup=false
|
||||||
|
- homepage.group=ESH
|
||||||
|
- homepage.name=AdGuard Home
|
||||||
|
- homepage.icon=si-adguard
|
||||||
|
- homepage.description=DNS
|
||||||
|
- homepage.href=http://10.0.50.45:8080
|
||||||
|
- homepage.widget.type=adguard
|
||||||
|
- homepage.widget.url=http://10.0.50.45:8080
|
||||||
|
- homepage.widget.username=lkraven
|
||||||
|
- homepage.widget.password=[REDACTED-upstream-compose-inlines-this]
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
adguard-workdir: null
|
||||||
|
adguard-confdir: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/beszel-esh-vm-docker/compose.yaml
|
||||||
|
# Beszel — lightweight server/container monitoring.
|
||||||
|
#
|
||||||
|
# Hub: single web UI with the SQLite store. Agents: per-host metric collectors
|
||||||
|
# that the hub pulls from over SSH.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → hub only (ana-docker)
|
||||||
|
# COMPOSE_PROFILES=hub,agent → hub + local agent on the same host
|
||||||
|
# COMPOSE_PROFILES=agent → agent only (ana-ml2)
|
||||||
|
#
|
||||||
|
# The agent uses network_mode: host so it sees real host CPU/mem/net/disk
|
||||||
|
# counters rather than container-scoped ones — that's why it can't share
|
||||||
|
# the tnet network with the hub.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
beszel:
|
||||||
|
image: henrygd/beszel:${BESZEL_VERSION}
|
||||||
|
container_name: beszel
|
||||||
|
profiles: [hub]
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${BESZEL_PORT}:8090"
|
||||||
|
volumes:
|
||||||
|
- beszel_data:/beszel_data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Beszel
|
||||||
|
- homepage.icon=mdi-chart-line
|
||||||
|
- homepage.description=Server + container monitoring
|
||||||
|
- homepage.href=http://10.250.50.70:${BESZEL_PORT}
|
||||||
|
|
||||||
|
beszel-agent:
|
||||||
|
image: henrygd/beszel-agent:${BESZEL_VERSION}
|
||||||
|
container_name: beszel-agent
|
||||||
|
profiles: [agent]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- beszel_agent_data:/var/lib/beszel-agent
|
||||||
|
environment:
|
||||||
|
- PORT=${BESZEL_AGENT_PORT:-45876}
|
||||||
|
- KEY=${BESZEL_HUB_KEY}
|
||||||
|
- HUB_URL=${HUB_URL}
|
||||||
|
- TOKEN=${BESZEL_TOKEN}
|
||||||
|
- EXTRA_FILESYSTEMS=${BESZEL_EXTRA_FS:-}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
beszel_data:
|
||||||
|
beszel_agent_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
>>> /opt/docker/compose/calibre/compose.yaml
|
||||||
|
version: "2.1"
|
||||||
|
services:
|
||||||
|
calibre:
|
||||||
|
image: lscr.io/linuxserver/calibre:latest
|
||||||
|
container_name: calibre
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined #optional
|
||||||
|
environment:
|
||||||
|
- PUID=2000
|
||||||
|
- PGID=2000
|
||||||
|
- TZ=America/Los_Angeles
|
||||||
|
- PASSWORD= #optional
|
||||||
|
- CLI_ARGS= #optional
|
||||||
|
volumes:
|
||||||
|
- /mnt/books/calibre:/config
|
||||||
|
ports:
|
||||||
|
- 8082:8080
|
||||||
|
- 8181:8181
|
||||||
|
- 8281:8081
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- homepage.group=Media
|
||||||
|
- homepage.name=Calibre
|
||||||
|
- homepage.icon=mdi-bookshelf
|
||||||
|
- homepage.description=EBook Server (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:8082
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/calibre-web/compose.yaml
|
||||||
|
services:
|
||||||
|
calibre-web:
|
||||||
|
image: lscr.io/linuxserver/calibre-web:latest
|
||||||
|
container_name: calibre-web
|
||||||
|
environment:
|
||||||
|
- PUID=2000
|
||||||
|
- PGID=2000
|
||||||
|
- TZ=Etc/UTC
|
||||||
|
- DOCKER_MODS=linuxserver/mods:universal-calibre #optional
|
||||||
|
- OAUTHLIB_RELAX_TOKEN_SCOPE=1 #optional
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/calibre-web:/config
|
||||||
|
- /mnt/books/calibre/calibre_library:/books
|
||||||
|
labels:
|
||||||
|
- homepage.group=Media
|
||||||
|
- homepage.name=Calibre-Web
|
||||||
|
- homepage.icon=si-calibreweb
|
||||||
|
- homepage.description=EBook Server (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:8083
|
||||||
|
ports:
|
||||||
|
- 8083:8083
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: {}
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/cloudflare-ddns-esh/compose.yaml
|
||||||
|
services:
|
||||||
|
cloudflare-ddns:
|
||||||
|
image: timothyjmiller/cloudflare-ddns:latest
|
||||||
|
container_name: cloudflare-ddns
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/cloudflare-ddns/config.json:/config.json
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: {}
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/dockge/compose.yaml
|
||||||
|
services:
|
||||||
|
dockge:
|
||||||
|
image: louislam/dockge:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Host Port : Container Port
|
||||||
|
- 5001:5001
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- dockge_data:/app/data
|
||||||
|
- /opt/docker/compose:/opt/docker/compose
|
||||||
|
labels:
|
||||||
|
- homepage.group=ESH
|
||||||
|
- homepage.name=Dockge
|
||||||
|
- homepage.icon=si-portainer
|
||||||
|
- homepage.description=Docker
|
||||||
|
- homepage.href=http://10.0.50.45:5001
|
||||||
|
environment:
|
||||||
|
# Tell Dockge where is your stacks directory
|
||||||
|
- DOCKGE_STACKS_DIR=/opt/docker/compose
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dockge_data: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/dozzle-agent/compose.yaml
|
||||||
|
# Dozzle — container log viewer.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → runs the web UI (deploy on ana-docker)
|
||||||
|
# COMPOSE_PROFILES=agent → runs the remote agent (deploy on ana-ml2)
|
||||||
|
#
|
||||||
|
# Same compose.yaml on both servers; per-host `.env` picks the profile.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
dozzle:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle
|
||||||
|
profiles:
|
||||||
|
- hub
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- ${DOZZLE_PORT}:8080
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
- DOZZLE_REMOTE_AGENT=${DOZZLE_REMOTE_AGENT:-}
|
||||||
|
- DOZZLE_AUTH_PROVIDER=${DOZZLE_AUTH_PROVIDER:-none}
|
||||||
|
- DOZZLE_USERNAME=${DOZZLE_USERNAME:-}
|
||||||
|
- DOZZLE_PASSWORD=${DOZZLE_PASSWORD:-}
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- /dozzle
|
||||||
|
- healthcheck
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Dozzle
|
||||||
|
- homepage.icon=mdi-text-box-search
|
||||||
|
- homepage.description=Container logs (ana-docker + ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.70:${DOZZLE_PORT}
|
||||||
|
dozzle-agent:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle-agent
|
||||||
|
profiles:
|
||||||
|
- agent
|
||||||
|
restart: unless-stopped
|
||||||
|
command: agent
|
||||||
|
ports:
|
||||||
|
- ${DOZZLE_AGENT_BIND:-0.0.0.0}:${DOZZLE_AGENT_PORT}:7007
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_agent_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
volumes:
|
||||||
|
dozzle_data: null
|
||||||
|
dozzle_agent_data: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/drawio/compose.yaml
|
||||||
|
services:
|
||||||
|
drawio:
|
||||||
|
image: jgraph/drawio
|
||||||
|
container_name: drawio
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8087:8080
|
||||||
|
- 8447:8443
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- curl -f http://127.0.0.1:8080 || exit 1
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- homepage.group=Apps
|
||||||
|
- homepage.name=draw.io
|
||||||
|
- homepage.icon=mdi-pencil
|
||||||
|
- homepage.description=Draw.IO Graphing (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:8087
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/esphome/compose.yaml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
esphome:
|
||||||
|
container_name: esphome
|
||||||
|
image: ghcr.io/esphome/esphome
|
||||||
|
volumes:
|
||||||
|
- /path/to/esphome/config:/config
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
restart: always
|
||||||
|
privileged: true
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
- USERNAME=test
|
||||||
|
- PASSWORD=ChangeMe
|
||||||
|
>>> /opt/docker/compose/homeassistant/compose.yaml
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
image: homeassistant/home-assistant:latest
|
||||||
|
container_name: homeassistant
|
||||||
|
environment:
|
||||||
|
- PUID=2000
|
||||||
|
- PGID=2000
|
||||||
|
- TZ=America/Los_Angeles
|
||||||
|
volumes:
|
||||||
|
- homeassistant_data:/config
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
macvlan_net:
|
||||||
|
ipv4_address: 10.0.50.46
|
||||||
|
labels:
|
||||||
|
- homepage.group=Apps
|
||||||
|
- homepage.name=Home Assistant
|
||||||
|
- homepage.icon=si-homeassistant
|
||||||
|
- homepage.description=Home Automation (esh)
|
||||||
|
- homepage.href=http://10.0.50.46:8123
|
||||||
|
networks:
|
||||||
|
macvlan_net:
|
||||||
|
driver: macvlan
|
||||||
|
driver_opts:
|
||||||
|
parent: ens18
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 10.0.50.0/24
|
||||||
|
volumes:
|
||||||
|
homeassistant_data: null
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/homepage/compose.yaml
|
||||||
|
services:
|
||||||
|
homepage:
|
||||||
|
image: ghcr.io/gethomepage/homepage:latest
|
||||||
|
container_name: homepage
|
||||||
|
environment:
|
||||||
|
PUID: 1000
|
||||||
|
PGID: 1000
|
||||||
|
ports:
|
||||||
|
- 5100:3000
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/homepage:/app/config # Make sure your local config directory exists
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro # optional, for docker integrations
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.homepage.rule=host(`eshhome`) || host(`10.0.50.45`)
|
||||||
|
|| host (`eshhome.esteban.net`)
|
||||||
|
- traefik.http.services.homepage.loadbalancer.server.port=3000
|
||||||
|
- traefik.http.routers.homepage.priority=1
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/mosquitto/compose.yaml
|
||||||
|
services:
|
||||||
|
mosquitto:
|
||||||
|
container_name: mosquitto
|
||||||
|
image: eclipse-mosquitto:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 1883:1883/tcp
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/mosquitto:/mosquitto/config
|
||||||
|
- mosquitto_data:/mosquitto/data
|
||||||
|
- mosquitto_log:/mosquitto/log
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=Apps
|
||||||
|
- homepage.name=Mosquitto
|
||||||
|
- homepage.icon=mdi-bug
|
||||||
|
- homepage.description=MQQT Broker port 1883
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
mosquitto_data: null
|
||||||
|
mosquitto_log: null
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/paperless-ngx/compose.yaml
|
||||||
|
#version: "3.4"
|
||||||
|
services:
|
||||||
|
broker:
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
webserver:
|
||||||
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- broker
|
||||||
|
ports:
|
||||||
|
- 8200:8000
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- -fs
|
||||||
|
- -S
|
||||||
|
- --max-time
|
||||||
|
- "2"
|
||||||
|
- http://localhost:8000
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- data:/usr/src/paperless/data
|
||||||
|
- media:/usr/src/paperless/media
|
||||||
|
- /mnt/documents/paperless/export:/usr/src/paperless/export
|
||||||
|
- /mnt/documents/paperless/consume:/usr/src/paperless/consume
|
||||||
|
# env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_DBHOST: 10.0.50.60
|
||||||
|
PAPERLESS_DBPORT: 5432
|
||||||
|
PAPERLESS_DBNAME: paperless-ng
|
||||||
|
PAPERLESS_DBUSER: paperless-ng
|
||||||
|
PAPERLESS_DBPASS: paperless-ng
|
||||||
|
# PAPERLESS_MEDIA_ROOT=/data/media
|
||||||
|
PAPERLESS_CONSUMPTION_DIR: /usr/src/paperless/consume
|
||||||
|
PAPERLESS_EXPORT_DIR: /usr/src/paperless/export
|
||||||
|
PAPERLESS_CONSUMER_POLLING: 30
|
||||||
|
# PAPERLESS_DATA_DIR=/config
|
||||||
|
labels:
|
||||||
|
- homepage.group=Media
|
||||||
|
- homepage.name=PaperlessNGX
|
||||||
|
- homepage.icon=mdi-file-cabinet
|
||||||
|
- homepage.description=Document Store (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:8200
|
||||||
|
- homepage.sitemonitor=http://10.0.50.45:8200
|
||||||
|
- homepage.widget.type=paperlessngx
|
||||||
|
- homepage.widget.url=http://10.0.50.45:8200
|
||||||
|
- homepage.widget.key=${API_KEY}
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:latest
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# volumes to backup.
|
||||||
|
- data:/backup/paperless-data:ro
|
||||||
|
- media:/backup/paperless-media:ro
|
||||||
|
- redisdata:/backup/paperless-redisdata:ro
|
||||||
|
- /mnt/backup/docker/esh-vm-docker/paperless:/archive
|
||||||
|
# can omit below if not stopping service.
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
data: null
|
||||||
|
media: null
|
||||||
|
redisdata: null
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/pgadmin/compose.yaml
|
||||||
|
# version: "3.8"
|
||||||
|
services:
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4
|
||||||
|
container_name: pgadmin4_container
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 5050:80
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: ${DEFAULT_EMAIL}
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- pgadmin-data:/var/lib/pgadmin
|
||||||
|
labels:
|
||||||
|
- homepage.group=Apps
|
||||||
|
- homepage.name=PGAdmin
|
||||||
|
- homepage.icon=mdi-database
|
||||||
|
- homepage.description=pgsql Manager (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:5050
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
backup:
|
||||||
|
image: offen/docker-volume-backup:latest
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# volumes to backup.
|
||||||
|
- pgadmin-data:/backup/pgadmin-data:ro
|
||||||
|
- /mnt/backup/docker/esh-vm-docker/pgadmin:/archive
|
||||||
|
# can omit below if not stopping service.
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
pgadmin-data: null
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/traefik/docker-compose.yml
|
||||||
|
#version: "3.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:latest
|
||||||
|
container_name: traefik
|
||||||
|
command:
|
||||||
|
- --log.level=DEBUG
|
||||||
|
- --configFile=/etc/traefik/traefik.yml
|
||||||
|
- --api.insecure=true
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
- --experimental.plugins.bouncer.version=v1.6.0
|
||||||
|
environment:
|
||||||
|
- AWS_ACCESS_KEY_ID=${AWS_KEY}
|
||||||
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
|
||||||
|
- AWS_REGION=${AWS_REGION}
|
||||||
|
- AWS_HOSTED_ZONE_ID=${AWS_ZONEID}
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8380:8080
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /opt/docker/conf/traefik-esh:/etc/traefik
|
||||||
|
labels:
|
||||||
|
- homepage.group=ESH
|
||||||
|
- homepage.name=traefik
|
||||||
|
- homepage.icon=si-traefikproxy
|
||||||
|
- homepage.sitemonitor=http://10.0.50.45:8380
|
||||||
|
- homepage.href=http://10.0.50.45:8380
|
||||||
|
- homepage.widget.type=traefik
|
||||||
|
- homepage.widget.url=http://10.0.50.45:8380
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=${CROWDSEC_KEY}
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=live
|
||||||
|
- traefik.http.middlewares.crowdsec.plugin.bouncer.defaultDecisionSeconds=60
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/uptimekuma/compose.yaml
|
||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 3001:3001
|
||||||
|
volumes:
|
||||||
|
- uptime-kuma:/app/data
|
||||||
|
container_name: uptime-kuma
|
||||||
|
image: louislam/uptime-kuma:latest
|
||||||
|
labels:
|
||||||
|
- homepage.group=Apps
|
||||||
|
- homepage.name=Uptime Kuma
|
||||||
|
- homepage.icon=mdi-arrow-up-bold-circle
|
||||||
|
- homepage.description=Service Monitoring (esh)
|
||||||
|
- homepage.href=http://10.0.50.45:3001
|
||||||
|
- homepage.widget.type=uptimekuma
|
||||||
|
- homepage.widget.url=http://10.0.50.45:3001
|
||||||
|
- homepage.widget.slug=nethealth
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
volumes:
|
||||||
|
uptime-kuma: {}
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
===== CONFIG LAYOUT (/opt/docker/conf/ — top 200 entries) =====
|
||||||
|
|
||||||
|
/opt/docker/conf
|
||||||
|
/opt/docker/conf/calibre-web
|
||||||
|
/opt/docker/conf/calibre-web/app.db
|
||||||
|
/opt/docker/conf/calibre-web/.CALIBRE_RELEASE
|
||||||
|
/opt/docker/conf/calibre-web/calibre-web.log
|
||||||
|
/opt/docker/conf/calibre-web/calibre-web.log.1
|
||||||
|
/opt/docker/conf/calibre-web/calibre-web.log.2
|
||||||
|
/opt/docker/conf/calibre-web/client_secrets.json
|
||||||
|
/opt/docker/conf/calibre-web/gdrive.db
|
||||||
|
/opt/docker/conf/calibre-web/.key
|
||||||
|
/opt/docker/conf/cloudflare-ddns
|
||||||
|
/opt/docker/conf/cloudflare-ddns/config.json
|
||||||
|
/opt/docker/conf/homepage
|
||||||
|
/opt/docker/conf/homepage/bookmarks.yaml
|
||||||
|
/opt/docker/conf/homepage/custom.css
|
||||||
|
/opt/docker/conf/homepage/custom.js
|
||||||
|
/opt/docker/conf/homepage/docker.yaml
|
||||||
|
/opt/docker/conf/homepage/imgs
|
||||||
|
/opt/docker/conf/homepage/imgs/bg1.jpg
|
||||||
|
/opt/docker/conf/homepage/imgs/Thumbs.db
|
||||||
|
/opt/docker/conf/homepage/kubernetes.yaml
|
||||||
|
/opt/docker/conf/homepage/logs
|
||||||
|
/opt/docker/conf/homepage/logs/homepage.log
|
||||||
|
/opt/docker/conf/homepage/nas-tls
|
||||||
|
/opt/docker/conf/homepage/nas-tls/ca.pem
|
||||||
|
/opt/docker/conf/homepage/nas-tls/cert.pem
|
||||||
|
/opt/docker/conf/homepage/nas-tls/key.pem
|
||||||
|
/opt/docker/conf/homepage/proxmox.yaml
|
||||||
|
/opt/docker/conf/homepage/services.yaml
|
||||||
|
/opt/docker/conf/homepage/settings.yaml
|
||||||
|
/opt/docker/conf/homepage/widgets.yaml
|
||||||
|
/opt/docker/conf/mosquitto
|
||||||
|
/opt/docker/conf/mosquitto/mosquitto.conf
|
||||||
|
/opt/docker/conf/mosquitto/mosquitto.log
|
||||||
|
/opt/docker/conf/mosquitto/mosquitto.passwd
|
||||||
|
/opt/docker/conf/traefik-esh
|
||||||
|
/opt/docker/conf/traefik-esh/certs
|
||||||
|
/opt/docker/conf/traefik-esh/certs/acme.json
|
||||||
|
/opt/docker/conf/traefik-esh/traefik.yml
|
||||||
|
|
||||||
|
===== LISTENING PORTS =====
|
||||||
|
|
||||||
|
0.0.0.0:111
|
||||||
|
0.0.0.0:1883
|
||||||
|
0.0.0.0:22
|
||||||
|
0.0.0.0:3000
|
||||||
|
0.0.0.0:3001
|
||||||
|
0.0.0.0:443
|
||||||
|
0.0.0.0:5001
|
||||||
|
0.0.0.0:5050
|
||||||
|
0.0.0.0:5100
|
||||||
|
0.0.0.0:53
|
||||||
|
0.0.0.0:6052
|
||||||
|
0.0.0.0:7007
|
||||||
|
0.0.0.0:80
|
||||||
|
0.0.0.0:8000
|
||||||
|
0.0.0.0:8080
|
||||||
|
0.0.0.0:8082
|
||||||
|
0.0.0.0:8083
|
||||||
|
0.0.0.0:8087
|
||||||
|
0.0.0.0:8181
|
||||||
|
0.0.0.0:8200
|
||||||
|
0.0.0.0:8281
|
||||||
|
0.0.0.0:8380
|
||||||
|
0.0.0.0:8443
|
||||||
|
0.0.0.0:8447
|
||||||
|
0.0.0.0:853
|
||||||
|
0.0.0.0:9001
|
||||||
|
0.0.0.0:9443
|
||||||
|
[::]:111
|
||||||
|
127.0.0.1:35469
|
||||||
|
[::]:1883
|
||||||
|
[::]:22
|
||||||
|
*:2375
|
||||||
|
[::]:3000
|
||||||
|
[::]:3001
|
||||||
|
[::]:443
|
||||||
|
[::]:5001
|
||||||
|
[::]:5050
|
||||||
|
[::]:5100
|
||||||
|
[::]:53
|
||||||
|
[::]:80
|
||||||
|
[::]:8000
|
||||||
|
[::]:8080
|
||||||
|
[::]:8082
|
||||||
|
[::]:8083
|
||||||
|
[::]:8087
|
||||||
|
[::]:8181
|
||||||
|
[::]:8200
|
||||||
|
[::]:8281
|
||||||
|
[::]:8380
|
||||||
|
[::]:8443
|
||||||
|
[::]:8447
|
||||||
|
[::]:853
|
||||||
|
[::]:9001
|
||||||
|
[::]:9443
|
||||||
|
|
||||||
|
===== MODEL / HUGGINGFACE CACHES =====
|
||||||
|
|
||||||
|
|
||||||
|
===== DOCKER-ADJACENT SYSTEMD SERVICES =====
|
||||||
|
|
||||||
|
containerd.service running
|
||||||
|
docker.service running
|
||||||
|
|
||||||
|
===== DONE =====
|
||||||
|
|
||||||
|
Paste the above back into the chat, or pass a path as argv[1] to save.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# nh3-docker
|
||||||
|
|
||||||
|
General-purpose Docker host for the New Hampshire (nh3) site. Small VM, no GPU. Separate LAN from the Anaheim colo.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
- **LAN IP:** 10.100.50.40
|
||||||
|
- **LAN subnet:** 10.100.0.0/16 (NH site)
|
||||||
|
- **FQDN:** `nh3-docker.phasefinal.com`
|
||||||
|
- **SSH:** standard port 22
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **CPU:** 8 vCPU (QEMU virtual — VM)
|
||||||
|
- **RAM:** 7.8 GB
|
||||||
|
- **GPUs:** none
|
||||||
|
- **Storage:** 125 GB root (ext4) + NFS mounts from `10.100.50.50` (Synology)
|
||||||
|
- **OS:** Debian 12 (bookworm), kernel 6.1.x
|
||||||
|
- **Docker:** running (daemon socket at `/var/run/docker.sock`; also listens on `:2375` — be aware if firewalling)
|
||||||
|
|
||||||
|
## Key paths
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/opt/docker/compose/<stack>/` | Compose files (also a git repo) |
|
||||||
|
| `/opt/docker/conf/<stack>/` | Config bind mounts (empty as of last inspection) |
|
||||||
|
| `/mnt/compose` | NFS — `10.100.50.50:/volume1/compose` |
|
||||||
|
| `/mnt/backup` | NFS — `10.100.50.50:/volume1/Backup` |
|
||||||
|
|
||||||
|
## Running stacks
|
||||||
|
|
||||||
|
| Stack | Port(s) | Notes |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| adguard | 53 / 853 / 8080 / 8443 / 3000 / 784 | AdGuard Home — primary DNS for the NH site |
|
||||||
|
| dockge | 5001 | Docker stack management UI |
|
||||||
|
| dozzle-agent | 7007 | Log agent; hub on ana-docker pulls from here |
|
||||||
|
| beszel-nh3-docker (agent) | 45876 | Metrics agent; hub on ana-docker |
|
||||||
|
| portainer (+ portainer_agent) | 9443 / 8000 / 9001 | Container management UI (ad-hoc, no compose dir under `/opt/docker/compose/`) |
|
||||||
|
|
||||||
|
No Traefik / TLS terminator on this host — services are accessed on raw LAN ports. If that changes, mirror the ana-docker pattern (`/opt/docker/conf/traefik-nh3/…` + `anaprod`-style cert resolver).
|
||||||
|
|
||||||
|
## Refresh state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/refresh-server-info.sh nh3-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Latest snapshot: `system-details.txt` (regenerate as needed).
|
||||||
|
|
||||||
|
## Placement rule
|
||||||
|
|
||||||
|
No GPU → this is the default target for NH-site Docker workloads. GPU-only stacks still go to **ana-ml2**; non-GPU Anaheim-specific services stay on **ana-docker**.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
10.100.50.40
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
|
||||||
|
===== HOST =====
|
||||||
|
|
||||||
|
Hostname: nh3-docker.phasefinal.com
|
||||||
|
Date: 2026-04-19T22:16:00-07:00
|
||||||
|
Uptime: up 5 weeks, 4 days, 15 hours, 4 minutes
|
||||||
|
OS: Debian GNU/Linux 12 (bookworm)
|
||||||
|
Kernel: 6.1.0-21-amd64
|
||||||
|
Arch: x86_64
|
||||||
|
|
||||||
|
===== HARDWARE =====
|
||||||
|
|
||||||
|
CPU cores: 8
|
||||||
|
CPU model: QEMU Virtual CPU version 2.5+
|
||||||
|
MemTotal: 7.8 GB
|
||||||
|
MemAvailable: 6.5 GB
|
||||||
|
|
||||||
|
===== GPUS =====
|
||||||
|
|
||||||
|
nvidia-smi not present (no NVIDIA GPUs or driver not installed)
|
||||||
|
|
||||||
|
===== FILESYSTEMS (df) =====
|
||||||
|
|
||||||
|
Filesystem Size Used Avail Use% Mounted on
|
||||||
|
/dev/sda1 125G 6.5G 112G 6% /
|
||||||
|
|
||||||
|
===== PERSISTENT MOUNTS (/etc/fstab, non-comment) =====
|
||||||
|
|
||||||
|
UUID=dc0e74f7-3973-4501-b04e-1d9a3888f739 / ext4 errors=remount-ro 0 1
|
||||||
|
UUID=cc35604a-fcb3-41e8-9a30-1b85e28eea99 none swap sw 0 0
|
||||||
|
/dev/sr0 /media/cdrom0 udf,iso9660 user,noauto 0 0
|
||||||
|
10.100.50.50:/volume1/compose /mnt/compose nfs defaults 0 0
|
||||||
|
10.100.50.50:/volume1/Backup /mnt/backup nfs defaults 0 0
|
||||||
|
|
||||||
|
===== TARGETED DATA PATHS =====
|
||||||
|
|
||||||
|
/opt (total: 1.6M)
|
||||||
|
total 12
|
||||||
|
drwxr-xr-x 3 root root 4096 2024-05-30 00:24 .
|
||||||
|
drwxr-xr-x 18 root root 4096 2024-05-14 14:44 ..
|
||||||
|
drwxrwxrwx 5 root root 4096 2024-05-30 19:12 docker
|
||||||
|
|
||||||
|
/opt/docker (total: 1.6M)
|
||||||
|
total 32
|
||||||
|
drwxrwxrwx 5 root root 4096 2024-05-30 19:12 .
|
||||||
|
drwxr-xr-x 3 root root 4096 2024-05-30 00:24 ..
|
||||||
|
drwxr-xr-x 6 lkraven lkraven 4096 2026-04-19 01:03 compose
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2024-05-30 19:11 conf
|
||||||
|
drwxr-xr-x 8 lkraven lkraven 4096 2024-05-31 19:05 .git
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 14 2024-05-30 19:12 .gitignore
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 1059 2024-05-30 00:27 LICENSE
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 60 2024-05-30 00:27 README.md
|
||||||
|
|
||||||
|
/opt/docker/compose (total: 56K)
|
||||||
|
total 28
|
||||||
|
drwxr-xr-x 6 lkraven lkraven 4096 2026-04-19 01:03 .
|
||||||
|
drwxrwxrwx 5 root root 4096 2024-05-30 19:12 ..
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2024-05-30 19:14 adguard
|
||||||
|
drwxr-xr-x 2 root root 4096 2026-04-19 01:03 beszel-nh3-docker
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2024-05-30 19:14 dockge
|
||||||
|
drwxr-xr-x 2 root root 4096 2026-04-19 00:44 dozzle-agent
|
||||||
|
-rw-r--r-- 1 lkraven lkraven 25 2024-05-30 00:27 README.md
|
||||||
|
|
||||||
|
/opt/docker/conf (total: 4.0K)
|
||||||
|
total 8
|
||||||
|
drwxr-xr-x 2 lkraven lkraven 4096 2024-05-30 19:11 .
|
||||||
|
drwxrwxrwx 5 root root 4096 2024-05-30 19:12 ..
|
||||||
|
|
||||||
|
/var/lib/docker (total: 4.0K)
|
||||||
|
|
||||||
|
/srv (total: 4.0K)
|
||||||
|
total 8
|
||||||
|
drwxr-xr-x 2 root root 4096 2024-05-14 14:43 .
|
||||||
|
drwxr-xr-x 18 root root 4096 2024-05-14 14:44 ..
|
||||||
|
|
||||||
|
|
||||||
|
===== DOCKER =====
|
||||||
|
|
||||||
|
Server: 20.10.24+dfsg1 Client: 20.10.24+dfsg1
|
||||||
|
|
||||||
|
----- docker info -----
|
||||||
|
Containers: 6 (running 6, paused 0, stopped 0)
|
||||||
|
Images: 13
|
||||||
|
Runtimes: map[io.containerd.runc.v2:{runc [] <nil>} io.containerd.runtime.v1.linux:{runc [] <nil>} runc:{runc [] <nil>}]
|
||||||
|
Default runtime: runc
|
||||||
|
Storage driver: overlay2
|
||||||
|
Root dir: /var/lib/docker
|
||||||
|
Server version: 20.10.24+dfsg1
|
||||||
|
|
||||||
|
----- running containers -----
|
||||||
|
NAMES IMAGE STATUS PORTS
|
||||||
|
beszel-agent henrygd/beszel-agent:latest Up 21 hours
|
||||||
|
dozzle-agent amir20/dozzle:latest Up 22 hours 0.0.0.0:7007->7007/tcp, 8080/tcp
|
||||||
|
dockge-dockge-1 louislam/dockge:latest Up 5 weeks (healthy) 0.0.0.0:5001->5001/tcp, :::5001->5001/tcp
|
||||||
|
adguardhome adguard/adguardhome Up 5 weeks 67-68/udp, 0.0.0.0:53->53/udp, :::53->53/udp, 443/udp, 0.0.0.0:53->53/tcp, :::53->53/tcp, 853/udp, 0.0.0.0:853->853/tcp, :::853->853/tcp, 3000/udp, 5443/tcp, 0.0.0.0:3000->3000/tcp, 0.0.0.0:784->784/udp, :::3000->3000/tcp, :::784->784/udp, 5443/udp, 6060/tcp, 0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8443->443/tcp, :::8443->443/tcp
|
||||||
|
portainer_agent portainer/agent:2.19.4 Up 5 weeks 0.0.0.0:9001->9001/tcp, :::9001->9001/tcp
|
||||||
|
portainer portainer/portainer-ce:latest Up 5 weeks 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp, 9000/tcp
|
||||||
|
|
||||||
|
----- all containers -----
|
||||||
|
NAMES IMAGE STATUS
|
||||||
|
beszel-agent henrygd/beszel-agent:latest Up 21 hours
|
||||||
|
dozzle-agent amir20/dozzle:latest Up 22 hours
|
||||||
|
dockge-dockge-1 louislam/dockge:latest Up 5 weeks (healthy)
|
||||||
|
adguardhome adguard/adguardhome Up 5 weeks
|
||||||
|
portainer_agent portainer/agent:2.19.4 Up 5 weeks
|
||||||
|
portainer portainer/portainer-ce:latest Up 5 weeks
|
||||||
|
|
||||||
|
----- networks -----
|
||||||
|
NAME DRIVER SCOPE
|
||||||
|
bridge bridge local
|
||||||
|
host host local
|
||||||
|
none null local
|
||||||
|
traefik-net bridge local
|
||||||
|
|
||||||
|
----- networks (external, non-default — worth knowing for compose external: true) -----
|
||||||
|
traefik-net
|
||||||
|
|
||||||
|
----- named volumes -----
|
||||||
|
VOLUME NAME DRIVER
|
||||||
|
adguard_adguard-confdir local
|
||||||
|
adguard_adguard-workdir local
|
||||||
|
beszel-nh3-docker_beszel_agent_data local
|
||||||
|
dockge_dockge_data local
|
||||||
|
dozzle-agent_dozzle_agent_data local
|
||||||
|
portainer_data local
|
||||||
|
|
||||||
|
----- compose projects currently running -----
|
||||||
|
adguard
|
||||||
|
beszel-nh3-docker
|
||||||
|
dockge
|
||||||
|
dozzle-agent
|
||||||
|
|
||||||
|
===== COMPOSE FILES (/opt/docker/compose/) =====
|
||||||
|
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/adguard/docker-compose.yml
|
||||||
|
#version: "3"
|
||||||
|
services:
|
||||||
|
adguardhome:
|
||||||
|
image: adguard/adguardhome
|
||||||
|
container_name: adguardhome
|
||||||
|
ports:
|
||||||
|
- 53:53/tcp
|
||||||
|
- 53:53/udp
|
||||||
|
- 784:784/udp
|
||||||
|
- 853:853/tcp
|
||||||
|
- 3000:3000/tcp
|
||||||
|
- 8080:80/tcp
|
||||||
|
- 8443:443/tcp
|
||||||
|
volumes:
|
||||||
|
- adguard-workdir:/opt/adguardhome/work
|
||||||
|
- adguard-confdir:/opt/adguardhome/conf
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-NH3
|
||||||
|
- homepage.name=AdGuard Home
|
||||||
|
- homepage.icon=si-adguard
|
||||||
|
- homepage.description=DNS
|
||||||
|
- homepage.href=http://10.100.50.40:8080
|
||||||
|
- homepage.widget.type=adguard
|
||||||
|
- homepage.widget.url=http://10.100.50.40:8080
|
||||||
|
- homepage.widget.username=lkraven
|
||||||
|
- homepage.widget.password=${userpass}
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
adguard-workdir: null
|
||||||
|
adguard-confdir: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/beszel-nh3-docker/compose.yaml
|
||||||
|
# Beszel — lightweight server/container monitoring.
|
||||||
|
#
|
||||||
|
# Hub: single web UI with the SQLite store. Agents: per-host metric collectors
|
||||||
|
# that the hub pulls from over SSH.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → hub only (ana-docker)
|
||||||
|
# COMPOSE_PROFILES=hub,agent → hub + local agent on the same host
|
||||||
|
# COMPOSE_PROFILES=agent → agent only (ana-ml2)
|
||||||
|
#
|
||||||
|
# The agent uses network_mode: host so it sees real host CPU/mem/net/disk
|
||||||
|
# counters rather than container-scoped ones — that's why it can't share
|
||||||
|
# the tnet network with the hub.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
beszel:
|
||||||
|
image: henrygd/beszel:${BESZEL_VERSION}
|
||||||
|
container_name: beszel
|
||||||
|
profiles: [hub]
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${BESZEL_PORT}:8090"
|
||||||
|
volumes:
|
||||||
|
- beszel_data:/beszel_data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Beszel
|
||||||
|
- homepage.icon=mdi-chart-line
|
||||||
|
- homepage.description=Server + container monitoring
|
||||||
|
- homepage.href=http://10.250.50.70:${BESZEL_PORT}
|
||||||
|
|
||||||
|
beszel-agent:
|
||||||
|
image: henrygd/beszel-agent:${BESZEL_VERSION}
|
||||||
|
container_name: beszel-agent
|
||||||
|
profiles: [agent]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- beszel_agent_data:/var/lib/beszel-agent
|
||||||
|
environment:
|
||||||
|
- PORT=${BESZEL_AGENT_PORT:-45876}
|
||||||
|
- KEY=${BESZEL_HUB_KEY}
|
||||||
|
- HUB_URL=${HUB_URL}
|
||||||
|
- TOKEN=${BESZEL_TOKEN}
|
||||||
|
- EXTRA_FILESYSTEMS=${BESZEL_EXTRA_FS:-}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
beszel_data:
|
||||||
|
beszel_agent_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
>>> /opt/docker/compose/dockge/compose.yaml
|
||||||
|
services:
|
||||||
|
dockge:
|
||||||
|
image: louislam/dockge:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Host Port : Container Port
|
||||||
|
- 5001:5001
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- dockge_data:/app/data
|
||||||
|
- /opt/docker/compose:/opt/docker/compose
|
||||||
|
environment:
|
||||||
|
# Tell Dockge where is your stacks directory
|
||||||
|
- DOCKGE_STACKS_DIR=/opt/docker/compose
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-NH3
|
||||||
|
- homepage.name=Dockge
|
||||||
|
- homepage.icon=si-portainer
|
||||||
|
- homepage.description=Docker
|
||||||
|
- homepage.href=http://10.100.50.40:5001
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
volumes:
|
||||||
|
dockge_data: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
|
||||||
|
>>> /opt/docker/compose/dozzle-agent/compose.yaml
|
||||||
|
# Dozzle — container log viewer.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → runs the web UI (deploy on ana-docker)
|
||||||
|
# COMPOSE_PROFILES=agent → runs the remote agent (deploy on ana-ml2)
|
||||||
|
#
|
||||||
|
# Same compose.yaml on both servers; per-host `.env` picks the profile.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
dozzle:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle
|
||||||
|
profiles:
|
||||||
|
- hub
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- ${DOZZLE_PORT}:8080
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
- DOZZLE_REMOTE_AGENT=${DOZZLE_REMOTE_AGENT:-}
|
||||||
|
- DOZZLE_AUTH_PROVIDER=${DOZZLE_AUTH_PROVIDER:-none}
|
||||||
|
- DOZZLE_USERNAME=${DOZZLE_USERNAME:-}
|
||||||
|
- DOZZLE_PASSWORD=${DOZZLE_PASSWORD:-}
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- /dozzle
|
||||||
|
- healthcheck
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Dozzle
|
||||||
|
- homepage.icon=mdi-text-box-search
|
||||||
|
- homepage.description=Container logs (ana-docker + ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.70:${DOZZLE_PORT}
|
||||||
|
dozzle-agent:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle-agent
|
||||||
|
profiles:
|
||||||
|
- agent
|
||||||
|
restart: unless-stopped
|
||||||
|
command: agent
|
||||||
|
ports:
|
||||||
|
- ${DOZZLE_AGENT_BIND:-0.0.0.0}:${DOZZLE_AGENT_PORT}:7007
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_agent_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
volumes:
|
||||||
|
dozzle_data: null
|
||||||
|
dozzle_agent_data: null
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
|
===== CONFIG LAYOUT (/opt/docker/conf/ — top 200 entries) =====
|
||||||
|
|
||||||
|
/opt/docker/conf
|
||||||
|
|
||||||
|
===== LISTENING PORTS =====
|
||||||
|
|
||||||
|
0.0.0.0:111
|
||||||
|
0.0.0.0:22
|
||||||
|
0.0.0.0:3000
|
||||||
|
0.0.0.0:5001
|
||||||
|
0.0.0.0:53
|
||||||
|
0.0.0.0:7007
|
||||||
|
0.0.0.0:8000
|
||||||
|
0.0.0.0:8080
|
||||||
|
0.0.0.0:8443
|
||||||
|
0.0.0.0:853
|
||||||
|
0.0.0.0:9001
|
||||||
|
0.0.0.0:9443
|
||||||
|
[::]:111
|
||||||
|
127.0.0.1:40185
|
||||||
|
[::]:22
|
||||||
|
*:2375
|
||||||
|
[::]:3000
|
||||||
|
[::]:5001
|
||||||
|
[::]:53
|
||||||
|
[::]:8000
|
||||||
|
[::]:8080
|
||||||
|
[::]:8443
|
||||||
|
[::]:853
|
||||||
|
[::]:9001
|
||||||
|
[::]:9443
|
||||||
|
|
||||||
|
===== MODEL / HUGGINGFACE CACHES =====
|
||||||
|
|
||||||
|
|
||||||
|
===== DOCKER-ADJACENT SYSTEMD SERVICES =====
|
||||||
|
|
||||||
|
containerd.service running
|
||||||
|
docker.service running
|
||||||
|
|
||||||
|
===== DONE =====
|
||||||
|
|
||||||
|
Paste the above back into the chat, or pass a path as argv[1] to save.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# backrest stack tunables. Copy to `.env` on ana-docker before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit if needed
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Repos, S3 endpoints, restic passwords, and schedules are all configured
|
||||||
|
# inside the Backrest web UI after first boot — nothing belongs here.
|
||||||
|
|
||||||
|
# Image version — pin for reproducibility (`latest` for edge)
|
||||||
|
BACKREST_VERSION=latest
|
||||||
|
|
||||||
|
# Host port for the web UI (container listens on 9898 internally)
|
||||||
|
BACKREST_PORT=9898
|
||||||
|
|
||||||
|
# Timezone — affects scheduler display and log timestamps
|
||||||
|
TZ=America/Los_Angeles
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# backrest
|
||||||
|
|
||||||
|
Web UI over restic repositories. Runs **once, on ana-docker**, and points at every per-host restic repo on both site-local S3 endpoints to give a single pane of glass for snapshot history, restores, and alerts.
|
||||||
|
|
||||||
|
**Server:** ana-docker
|
||||||
|
**Port:** `http://10.250.50.70:9898`
|
||||||
|
|
||||||
|
## Role in the fleet
|
||||||
|
|
||||||
|
- Actual backups are run by per-host `systemd` timers calling `restic` (see the backup design; not yet deployed). Each host writes to its site-local S3 bucket (TrueNAS on ana, Synology on nh3).
|
||||||
|
- **This container does not run backups by default** — it's a viewer/manager pointed at existing repos. (Backrest *can* be the scheduler instead of systemd timers; we're keeping the scheduler on the host for simplicity.)
|
||||||
|
- Because it only needs to talk to S3 endpoints (not host filesystems), no bind mounts of host paths are required.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On ana-docker:
|
||||||
|
sudo mkdir -p /opt/docker/compose/backrest
|
||||||
|
sudo chown $USER /opt/docker/compose/backrest
|
||||||
|
cd /opt/docker/compose/backrest
|
||||||
|
|
||||||
|
# scp compose.yaml + .env.example from this workspace, then:
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env if you want a different port or TZ
|
||||||
|
|
||||||
|
docker compose config
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://10.250.50.70:9898` and set the admin credentials on first load.
|
||||||
|
|
||||||
|
## First-time configuration (in the UI)
|
||||||
|
|
||||||
|
For each per-host repo (to be added once the restic pipeline is running):
|
||||||
|
|
||||||
|
1. **Add Repository** → fill in:
|
||||||
|
- **ID:** e.g. `ana-docker`, `ana-ml2`, `nh3-docker`, `esh-docker-vm`
|
||||||
|
- **URI:** `s3:https://10.250.50.50:4521/pfi-backups/ana/ana-docker/` (or the nh3 endpoint, depending on host)
|
||||||
|
- **Password:** the restic passphrase for that host
|
||||||
|
- **Env:** `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` for the S3 endpoint
|
||||||
|
2. *(Do not create a plan unless you want Backrest to drive the schedule — leave schedules to the systemd timers on each host.)*
|
||||||
|
3. Verify: the repo should list snapshots pulled by the host's systemd timer within a few minutes.
|
||||||
|
|
||||||
|
## Backup scope, when wired up
|
||||||
|
|
||||||
|
- ana-docker, ana-ml2 → TrueNAS S3 at `10.250.50.50:4521`
|
||||||
|
- nh3-docker, esh-docker-vm → Synology S3 at `10.100.50.50:4521`
|
||||||
|
- Cross-site rclone sync makes each bucket also hold the other site's data, so you can restore *either* host from *either* side if one NAS is down.
|
||||||
|
|
||||||
|
## Scaling knobs
|
||||||
|
|
||||||
|
- **Upgrade:** `docker compose pull && docker compose up -d`.
|
||||||
|
- **Backup of Backrest itself:** its config/DB lives in the `backrest_config` and `backrest_data` volumes — include those in ana-docker's restic plan so recreating the UI doesn't mean re-entering every repo.
|
||||||
|
|
||||||
|
## Why not `offen/docker-volume-backup`?
|
||||||
|
|
||||||
|
Two instances on `esh-docker-vm` (paperless-ngx, pgadmin) currently use the `offen/docker-volume-backup` sidecar pattern. Those will be retired once restic is in place:
|
||||||
|
|
||||||
|
- No encryption — backups sit in the clear on NFS.
|
||||||
|
- No dedup — every run writes a full tarball; storage grows linearly.
|
||||||
|
- Per-stack config — every new service needs its own sidecar wiring.
|
||||||
|
- No cross-host index — restores require knowing which tarball lives where.
|
||||||
|
|
||||||
|
Restic + Backrest solves all four at the cost of one extra binary per host. Leave the sidecars running until the restic plan is verified, then remove them in a scheduled change.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Backrest — web UI over restic repositories.
|
||||||
|
#
|
||||||
|
# Role here: single central viewer for every host's restic repo on both
|
||||||
|
# site-local S3 endpoints (TrueNAS at ana, Synology at nh3). Per-host
|
||||||
|
# `restic` runs will still be driven by systemd timers on each host; this
|
||||||
|
# stack is how we see what ran, browse snapshots, and restore.
|
||||||
|
#
|
||||||
|
# Repos and S3 credentials are configured in the Backrest UI after first
|
||||||
|
# boot — nothing baked into this file. Data (its own SQLite + queue) lives
|
||||||
|
# in a named volume so the config survives container recreation.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
backrest:
|
||||||
|
image: garethgeorge/backrest:${BACKREST_VERSION}
|
||||||
|
container_name: backrest
|
||||||
|
hostname: backrest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${BACKREST_PORT}:9898"
|
||||||
|
volumes:
|
||||||
|
- backrest_data:/data
|
||||||
|
- backrest_config:/config
|
||||||
|
- backrest_cache:/cache
|
||||||
|
- backrest_tmp:/tmp
|
||||||
|
environment:
|
||||||
|
- BACKREST_DATA=/data
|
||||||
|
- BACKREST_CONFIG=/config/config.json
|
||||||
|
- XDG_CACHE_HOME=/cache
|
||||||
|
- TMPDIR=/tmp
|
||||||
|
- TZ=${TZ:-America/Los_Angeles}
|
||||||
|
- BACKREST_PORT=0.0.0.0:9898
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:9898/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Backrest
|
||||||
|
- homepage.icon=mdi-backup-restore
|
||||||
|
- homepage.description=Restic snapshot viewer / restore UI
|
||||||
|
- homepage.href=http://10.250.50.70:${BACKREST_PORT}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backrest_data:
|
||||||
|
backrest_config:
|
||||||
|
backrest_cache:
|
||||||
|
backrest_tmp:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# beszel stack tunables. Copy to `.env` on each server before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit for this host
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Same compose.yaml on both servers — COMPOSE_PROFILES picks the role.
|
||||||
|
|
||||||
|
# Image version — pin for reproducibility (`latest` for edge)
|
||||||
|
BESZEL_VERSION=latest
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# On ana-docker (hub + local agent):
|
||||||
|
# COMPOSE_PROFILES=hub,agent
|
||||||
|
# BESZEL_PORT=8090
|
||||||
|
# BESZEL_HUB_KEY=<copy from hub "Add system" dialog after first boot>
|
||||||
|
#
|
||||||
|
# On ana-ml2 (agent only):
|
||||||
|
# COMPOSE_PROFILES=agent
|
||||||
|
# BESZEL_HUB_KEY=<same key as above>
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
COMPOSE_PROFILES=hub,agent
|
||||||
|
|
||||||
|
# ---- Hub-only -----------------------------------------------------------
|
||||||
|
|
||||||
|
# Host port for the web UI (container listens on 8090 internally).
|
||||||
|
BESZEL_PORT=8090
|
||||||
|
|
||||||
|
# ---- Agent-only ---------------------------------------------------------
|
||||||
|
|
||||||
|
# Host port the agent listens on. The hub SSHes into agents over this port.
|
||||||
|
BESZEL_AGENT_PORT=45876
|
||||||
|
|
||||||
|
# Hub's SSH public key — paste from the hub UI on first run.
|
||||||
|
# Grab it by clicking "Add System" → copy the key shown in the dialog.
|
||||||
|
BESZEL_HUB_KEY=
|
||||||
|
|
||||||
|
# Extra filesystems to track beyond the root mount, comma-separated.
|
||||||
|
# Examples:
|
||||||
|
# on ana-ml2: /tank
|
||||||
|
# on ana-docker: /mnt/backup,/mnt/compose
|
||||||
|
BESZEL_EXTRA_FS=
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# beszel
|
||||||
|
|
||||||
|
Lightweight monitoring — CPU, memory, disk, network, and per-container stats for every Docker host, with alerts over email/webhook. Pairs with Dozzle (logs) on the same server.
|
||||||
|
|
||||||
|
**Deploys to:**
|
||||||
|
- **ana-docker** (hub + local agent) — UI at `http://10.250.50.70:8090`
|
||||||
|
- **ana-ml2** (agent only) — listens on `10.250.50.54:45876`
|
||||||
|
- **nh3-docker** (agent only, cross-site) — listens on `10.100.50.40:45876`
|
||||||
|
|
||||||
|
Same compose.yaml on each host. Per-host `.env` sets `COMPOSE_PROFILES` to bring up the right combination. Each agent host is added individually in the hub UI.
|
||||||
|
|
||||||
|
## How hub ↔ agent auth works
|
||||||
|
|
||||||
|
Beszel uses SSH-key-based auth: the hub generates its own keypair on first boot, and each agent must be seeded with the hub's **public key** via the `KEY` env var. Agents listen on a port (default 45876); the hub pulls metrics by connecting to them with that key.
|
||||||
|
|
||||||
|
Operator flow on first deploy:
|
||||||
|
|
||||||
|
1. Bring up the **hub** on ana-docker with `BESZEL_HUB_KEY=` blank and the agent profile disabled.
|
||||||
|
2. Open the UI, create the admin account, click **Add System** — Beszel shows the public key.
|
||||||
|
3. Copy the key into `BESZEL_HUB_KEY` in the `.env` on both hosts.
|
||||||
|
4. Re-deploy the hub with `COMPOSE_PROFILES=hub,agent` to add the local agent; deploy the agent on ana-ml2.
|
||||||
|
5. Back in the UI, **Add System** with `host=127.0.0.1 port=45876` (local) and `host=10.250.50.54 port=45876` (ana-ml2).
|
||||||
|
|
||||||
|
## Deploy — hub + local agent (ana-docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker
|
||||||
|
sudo mkdir -p /opt/docker/compose/beszel
|
||||||
|
sudo chown $USER /opt/docker/compose/beszel
|
||||||
|
cd /opt/docker/compose/beszel
|
||||||
|
|
||||||
|
# scp compose.yaml + .env.example, then:
|
||||||
|
cp .env.example .env
|
||||||
|
# First pass — hub only, no key yet:
|
||||||
|
# COMPOSE_PROFILES=hub
|
||||||
|
# BESZEL_PORT=8090
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Open http://10.250.50.70:8090 → create admin → click "Add System" →
|
||||||
|
# copy the displayed public key into BESZEL_HUB_KEY.
|
||||||
|
|
||||||
|
# Second pass — add the local agent:
|
||||||
|
# COMPOSE_PROFILES=hub,agent
|
||||||
|
# BESZEL_EXTRA_FS=/mnt/backup,/mnt/compose
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy — agent (ana-ml2)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-ml2
|
||||||
|
sudo mkdir -p /opt/docker/compose/beszel
|
||||||
|
sudo chown $USER /opt/docker/compose/beszel
|
||||||
|
cd /opt/docker/compose/beszel
|
||||||
|
|
||||||
|
# scp the same compose.yaml + .env.example, then:
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit to:
|
||||||
|
# COMPOSE_PROFILES=agent
|
||||||
|
# BESZEL_HUB_KEY=<same key as the hub>
|
||||||
|
# BESZEL_EXTRA_FS=/tank
|
||||||
|
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the hub UI, **Add System** with `host=10.250.50.54`, `port=45876`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hub health
|
||||||
|
curl -s http://10.250.50.70:8090/api/health
|
||||||
|
|
||||||
|
# Agent reachable
|
||||||
|
ssh ana-docker 'nc -zv 10.250.50.54 45876'
|
||||||
|
|
||||||
|
# Local agent reachable from hub container
|
||||||
|
docker exec beszel nc -zv host.docker.internal 45876
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sizing / impact
|
||||||
|
|
||||||
|
The agent is ~10 MB RAM and negligible CPU — runs fine alongside anything on ana-ml2 including GPU workloads. Host-mode networking means it has no port conflicts with other stacks as long as `BESZEL_AGENT_PORT` stays unique.
|
||||||
|
|
||||||
|
## Alerts
|
||||||
|
|
||||||
|
Configured inside the hub UI (Settings → Notifications). Supports email (SMTP), Gotify, ntfy, Discord, Slack, and generic webhooks. Alert rules attach to per-system or global thresholds (CPU, memory, disk, container down, etc.).
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Beszel — lightweight server/container monitoring.
|
||||||
|
#
|
||||||
|
# Hub: single web UI with the SQLite store. Agents: per-host metric collectors
|
||||||
|
# that the hub pulls from over SSH.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → hub only (ana-docker)
|
||||||
|
# COMPOSE_PROFILES=hub,agent → hub + local agent on the same host
|
||||||
|
# COMPOSE_PROFILES=agent → agent only (ana-ml2)
|
||||||
|
#
|
||||||
|
# The agent uses network_mode: host so it sees real host CPU/mem/net/disk
|
||||||
|
# counters rather than container-scoped ones — that's why it can't share
|
||||||
|
# the tnet network with the hub.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
beszel:
|
||||||
|
image: henrygd/beszel:${BESZEL_VERSION}
|
||||||
|
container_name: beszel
|
||||||
|
profiles: [hub]
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${BESZEL_PORT}:8090"
|
||||||
|
volumes:
|
||||||
|
- beszel_data:/beszel_data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Beszel
|
||||||
|
- homepage.icon=mdi-chart-line
|
||||||
|
- homepage.description=Server + container monitoring
|
||||||
|
- homepage.href=http://10.250.50.70:${BESZEL_PORT}
|
||||||
|
|
||||||
|
beszel-agent:
|
||||||
|
image: henrygd/beszel-agent:${BESZEL_VERSION}
|
||||||
|
container_name: beszel-agent
|
||||||
|
profiles: [agent]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- beszel_agent_data:/var/lib/beszel-agent
|
||||||
|
environment:
|
||||||
|
- PORT=${BESZEL_AGENT_PORT:-45876}
|
||||||
|
- KEY=${BESZEL_HUB_KEY}
|
||||||
|
- EXTRA_FILESYSTEMS=${BESZEL_EXTRA_FS:-}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
beszel_data:
|
||||||
|
beszel_agent_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# dozzle stack tunables. Copy to `.env` on each server before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit for this host
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Same compose.yaml on both servers — COMPOSE_PROFILES picks the role.
|
||||||
|
|
||||||
|
# Image version — pin for reproducibility (`latest` for edge)
|
||||||
|
DOZZLE_VERSION=latest
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# On ana-docker (hub):
|
||||||
|
# COMPOSE_PROFILES=hub
|
||||||
|
# DOZZLE_HOSTNAME=ana-docker
|
||||||
|
# DOZZLE_REMOTE_AGENT=10.250.50.54:7007
|
||||||
|
#
|
||||||
|
# On ana-ml2 (agent):
|
||||||
|
# COMPOSE_PROFILES=agent
|
||||||
|
# DOZZLE_HOSTNAME=ana-ml2
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
COMPOSE_PROFILES=hub
|
||||||
|
|
||||||
|
# Display name for this host in the UI (shown as a tab / section header).
|
||||||
|
DOZZLE_HOSTNAME=ana-docker
|
||||||
|
|
||||||
|
# ---- Hub-only -----------------------------------------------------------
|
||||||
|
|
||||||
|
# Host port for the web UI (container listens on 8080 internally).
|
||||||
|
DOZZLE_PORT=8088
|
||||||
|
|
||||||
|
# Comma-separated list of remote agents the hub should connect to.
|
||||||
|
# Leave blank if this host only views its own containers.
|
||||||
|
DOZZLE_REMOTE_AGENT=10.250.50.54:7007,10.100.50.40:7007
|
||||||
|
|
||||||
|
# Auth — `none` is fine behind the LAN / a reverse proxy with auth.
|
||||||
|
# Switch to `simple` and set USERNAME/PASSWORD to gate the UI itself.
|
||||||
|
DOZZLE_AUTH_PROVIDER=none
|
||||||
|
DOZZLE_USERNAME=
|
||||||
|
DOZZLE_PASSWORD=
|
||||||
|
|
||||||
|
# ---- Agent-only ---------------------------------------------------------
|
||||||
|
|
||||||
|
# Host port the agent listens on (container listens on 7007 internally).
|
||||||
|
DOZZLE_AGENT_PORT=7007
|
||||||
|
|
||||||
|
# Bind address — restrict to the LAN interface if you want belt-and-braces
|
||||||
|
# beyond what the firewall already enforces. Default 0.0.0.0 exposes on all.
|
||||||
|
DOZZLE_AGENT_BIND=0.0.0.0
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# dozzle
|
||||||
|
|
||||||
|
Container log viewer. One UI on **ana-docker** aggregates logs from every Docker host via remote agents.
|
||||||
|
|
||||||
|
**Deploys to:**
|
||||||
|
- **ana-docker** (hub) — UI at `http://10.250.50.70:8088`
|
||||||
|
- **ana-ml2** (agent) — listens on `10.250.50.54:7007`
|
||||||
|
- **nh3-docker** (agent, cross-site) — listens on `10.100.50.40:7007`
|
||||||
|
|
||||||
|
One compose.yaml lives on each host. The per-host `.env` sets `COMPOSE_PROFILES=hub` or `COMPOSE_PROFILES=agent` so `docker compose up -d` brings up the right service. On the hub, add every agent to `DOZZLE_REMOTE_AGENT` as a comma-separated list (e.g. `10.250.50.54:7007,10.100.50.40:7007`).
|
||||||
|
|
||||||
|
## Auth / TLS note
|
||||||
|
|
||||||
|
Dozzle agents and hub auto-generate mTLS certificates on first run. On the trusted LAN (10.250.0.0/16) the default config is fine. If you ever expose an agent beyond the LAN, generate and pin certificates explicitly per the Dozzle docs (`dozzle generate`). The web UI itself is unauthenticated by default — flip `DOZZLE_AUTH_PROVIDER=simple` and set `DOZZLE_USERNAME`/`DOZZLE_PASSWORD` in the hub `.env` if you want a login gate.
|
||||||
|
|
||||||
|
## Deploy — hub (ana-docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker
|
||||||
|
sudo mkdir -p /opt/docker/compose/dozzle
|
||||||
|
sudo chown $USER /opt/docker/compose/dozzle
|
||||||
|
cd /opt/docker/compose/dozzle
|
||||||
|
|
||||||
|
# scp compose.yaml + .env.example from this workspace, then:
|
||||||
|
cp .env.example .env
|
||||||
|
# Ensure:
|
||||||
|
# COMPOSE_PROFILES=hub
|
||||||
|
# DOZZLE_HOSTNAME=ana-docker
|
||||||
|
# DOZZLE_REMOTE_AGENT=10.250.50.54:7007
|
||||||
|
# DOZZLE_PORT=8088
|
||||||
|
|
||||||
|
docker compose config
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy — agent (ana-ml2)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-ml2
|
||||||
|
sudo mkdir -p /opt/docker/compose/dozzle
|
||||||
|
sudo chown $USER /opt/docker/compose/dozzle
|
||||||
|
cd /opt/docker/compose/dozzle
|
||||||
|
|
||||||
|
# scp the same compose.yaml + .env.example, then:
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit to:
|
||||||
|
# COMPOSE_PROFILES=agent
|
||||||
|
# DOZZLE_HOSTNAME=ana-ml2
|
||||||
|
# DOZZLE_AGENT_PORT=7007
|
||||||
|
|
||||||
|
docker compose config
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hub health (from anywhere on LAN)
|
||||||
|
curl -s http://10.250.50.70:8088/healthz
|
||||||
|
|
||||||
|
# Agent reachable from the hub's perspective
|
||||||
|
ssh ana-docker 'nc -zv 10.250.50.54 7007'
|
||||||
|
|
||||||
|
# Open http://10.250.50.70:8088 — you should see two tabs:
|
||||||
|
# "ana-docker" (local containers) and "ana-ml2" (via agent).
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Hub shows only local containers:** agent is unreachable. Check firewall rules on ana-ml2 (port 7007 must be open from 10.250.50.70) and that the agent is actually listening (`ss -tlnp | grep 7007`).
|
||||||
|
- **Agent keeps restarting:** verify the docker.sock bind mount is read-only and the socket exists.
|
||||||
|
- **Certificate mismatch after image upgrade:** delete the `dozzle_data` and `dozzle_agent_data` volumes on both hosts and redeploy to regenerate.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Dozzle — container log viewer.
|
||||||
|
#
|
||||||
|
# Multi-host layout via compose profiles:
|
||||||
|
# COMPOSE_PROFILES=hub → runs the web UI (deploy on ana-docker)
|
||||||
|
# COMPOSE_PROFILES=agent → runs the remote agent (deploy on ana-ml2)
|
||||||
|
#
|
||||||
|
# Same compose.yaml on both servers; per-host `.env` picks the profile.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
dozzle:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle
|
||||||
|
profiles: [hub]
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${DOZZLE_PORT}:8080"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
- DOZZLE_REMOTE_AGENT=${DOZZLE_REMOTE_AGENT:-}
|
||||||
|
- DOZZLE_AUTH_PROVIDER=${DOZZLE_AUTH_PROVIDER:-none}
|
||||||
|
- DOZZLE_USERNAME=${DOZZLE_USERNAME:-}
|
||||||
|
- DOZZLE_PASSWORD=${DOZZLE_PASSWORD:-}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/dozzle", "healthcheck"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Dozzle
|
||||||
|
- homepage.icon=mdi-text-box-search
|
||||||
|
- homepage.description=Container logs (ana-docker + ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.70:${DOZZLE_PORT}
|
||||||
|
|
||||||
|
dozzle-agent:
|
||||||
|
image: amir20/dozzle:${DOZZLE_VERSION}
|
||||||
|
container_name: dozzle-agent
|
||||||
|
profiles: [agent]
|
||||||
|
restart: unless-stopped
|
||||||
|
command: agent
|
||||||
|
ports:
|
||||||
|
- "${DOZZLE_AGENT_BIND:-0.0.0.0}:${DOZZLE_AGENT_PORT}:7007"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- dozzle_agent_data:/data
|
||||||
|
environment:
|
||||||
|
- DOZZLE_HOSTNAME=${DOZZLE_HOSTNAME}
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dozzle_data:
|
||||||
|
dozzle_agent_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Infinity stack tunables. Copy this to `.env` on the server before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit .env with real values
|
||||||
|
# docker compose up -d
|
||||||
|
|
||||||
|
# Image version — pin for reproducibility (`latest` for edge)
|
||||||
|
INFINITY_VERSION=latest
|
||||||
|
|
||||||
|
# Port exposed on host
|
||||||
|
INFINITY_PORT=7997
|
||||||
|
|
||||||
|
# GPU assignment (ana-ml2 has 0 and 1; default 1 keeps 0 free for heavy LLM work)
|
||||||
|
GPU_ID=1
|
||||||
|
|
||||||
|
# Models — both served simultaneously; reference by the full repo name in requests
|
||||||
|
EMBED_MODEL=Qwen/Qwen3-Embedding-0.6B
|
||||||
|
RERANK_MODEL=Qwen/Qwen3-Reranker-0.6B
|
||||||
|
|
||||||
|
# Inference engine: torch (widest support) or optimum (ONNX, sometimes faster)
|
||||||
|
ENGINE=torch
|
||||||
|
|
||||||
|
# Batch size — 32 is a safe default; bump for throughput if VRAM allows
|
||||||
|
BATCH_SIZE=32
|
||||||
|
|
||||||
|
# Optional API key — leave blank for no auth (fine on the internal network)
|
||||||
|
API_KEY=
|
||||||
|
|
||||||
|
# HuggingFace token — only needed for gated models
|
||||||
|
HF_TOKEN=
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# infinity
|
||||||
|
|
||||||
|
OpenAI-compatible embeddings + reranker server. One container serves both embedding and reranker models simultaneously.
|
||||||
|
|
||||||
|
**Server:** ana-ml2
|
||||||
|
**Port:** 7997 (infinity default)
|
||||||
|
**GPU:** pinned to GPU 1 by default (configurable via `.env`)
|
||||||
|
|
||||||
|
## What it replaces / supersedes
|
||||||
|
|
||||||
|
- `qwen3-embedding-0.6B` entry in llama-swap (llama.cpp GGUF → infinity transformer)
|
||||||
|
- `qwen3-reranker-0.6B` entry in llama-swap
|
||||||
|
|
||||||
|
Once infinity is verified stable, retire those two entries from `stacks/llama-swap/config.yaml`.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On ana-ml2:
|
||||||
|
sudo mkdir -p /opt/docker/compose/infinity
|
||||||
|
sudo chown $USER /opt/docker/compose/infinity
|
||||||
|
cd /opt/docker/compose/infinity
|
||||||
|
|
||||||
|
# Copy compose.yaml + .env.example here (e.g. via scp from this workspace)
|
||||||
|
# Then:
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env — pick GPU, models, etc.
|
||||||
|
|
||||||
|
# Pre-download models into the shared HF cache (optional, speeds first boot)
|
||||||
|
HF_HOME=/tank/aimodels/huggingface hf download "$(grep ^EMBED_MODEL .env | cut -d= -f2)"
|
||||||
|
HF_HOME=/tank/aimodels/huggingface hf download "$(grep ^RERANK_MODEL .env | cut -d= -f2)"
|
||||||
|
|
||||||
|
# Dry-parse
|
||||||
|
docker compose config
|
||||||
|
|
||||||
|
# Launch
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health
|
||||||
|
curl -s http://localhost:7997/health
|
||||||
|
|
||||||
|
# Embedding
|
||||||
|
curl -s http://localhost:7997/embeddings \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"Qwen/Qwen3-Embedding-0.6B","input":["hello world"]}' | jq .
|
||||||
|
|
||||||
|
# Reranker
|
||||||
|
curl -s http://localhost:7997/rerank \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"Qwen/Qwen3-Reranker-0.6B","query":"what is a cat","documents":["cats are mammals","dogs bark"]}' | jq .
|
||||||
|
|
||||||
|
# Listed models
|
||||||
|
curl -s http://localhost:7997/models | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scaling knobs
|
||||||
|
|
||||||
|
- **`BATCH_SIZE`** in `.env` — bigger = higher throughput, more VRAM. 32 is safe; try 64 or 128 if you have headroom.
|
||||||
|
- **Model size** — Qwen3-Embedding/Reranker come in 0.6B / 4B / 8B. Pick based on quality-vs-latency tradeoff. On RTX 6000 Ada 46 GB, the 8B pair fits easily (~20 GB VRAM).
|
||||||
|
- **`ENGINE=optimum`** — uses ONNX runtime, sometimes faster. Requires the model to have ONNX weights available; fall back to `torch` if it errors on startup.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Infinity — OpenAI-compatible embeddings + reranker server.
|
||||||
|
#
|
||||||
|
# Serves embedding and reranker models simultaneously from one container
|
||||||
|
# on port 7997 (HTTP). Consumers: AIPA agents (search/retrieval), LibreChat
|
||||||
|
# RAG, anything that needs vector embeddings.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
#
|
||||||
|
# Pre-download models to avoid first-run delay:
|
||||||
|
# HF_HOME=/tank/aimodels/huggingface hf download Qwen/Qwen3-Embedding-0.6B
|
||||||
|
# HF_HOME=/tank/aimodels/huggingface hf download Qwen/Qwen3-Reranker-0.6B
|
||||||
|
|
||||||
|
services:
|
||||||
|
infinity:
|
||||||
|
image: michaelf34/infinity:${INFINITY_VERSION}
|
||||||
|
container_name: infinity
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${INFINITY_PORT}:7997"
|
||||||
|
volumes:
|
||||||
|
- /tank/aimodels/huggingface:/hfcache
|
||||||
|
environment:
|
||||||
|
- HF_HOME=/hfcache
|
||||||
|
- HF_HUB_CACHE=/hfcache/hub
|
||||||
|
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-}
|
||||||
|
command: >
|
||||||
|
v2
|
||||||
|
--model-id ${EMBED_MODEL}
|
||||||
|
--model-id ${RERANK_MODEL}
|
||||||
|
--engine ${ENGINE}
|
||||||
|
--device cuda
|
||||||
|
--batch-size ${BATCH_SIZE}
|
||||||
|
--host 0.0.0.0
|
||||||
|
--port 7997
|
||||||
|
--api-key ${API_KEY:-}
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
device_ids:
|
||||||
|
- "${GPU_ID}"
|
||||||
|
capabilities:
|
||||||
|
- gpu
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:7997/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 120s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=AI Systems
|
||||||
|
- homepage.name=Infinity
|
||||||
|
- homepage.icon=mdi-vector-arrange-below
|
||||||
|
- homepage.description=Embeddings + Reranker API (ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.54:7997/docs
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# llama-swap stack tunables. Copy to `.env` on ana-docker before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit if needed
|
||||||
|
# docker compose up -d
|
||||||
|
|
||||||
|
# Image tag. `cuda` is the CUDA-enabled build; pin to a specific release
|
||||||
|
# (e.g. `cuda-v0.0.6`) for reproducibility once upstream tags stabilize.
|
||||||
|
LLAMA_SWAP_VERSION=cuda
|
||||||
|
|
||||||
|
# Host port for the OpenAI-compatible API (container listens on 8080)
|
||||||
|
LLAMA_SWAP_PORT=9292
|
||||||
|
|
||||||
|
# Host paths
|
||||||
|
# --- Legacy GGUF models referenced by config.yaml as `-m /models/<file>`
|
||||||
|
MODELS_DIR=/tank/aimodels/llm
|
||||||
|
# --- Shared HuggingFace cache used by `-hf` model entries
|
||||||
|
HF_CACHE_DIR=/tank/aimodels/huggingface
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# llama-swap
|
||||||
|
|
||||||
|
GGUF model server with on-demand model swapping. Served via llama.cpp's `llama-server` under the llama-swap proxy.
|
||||||
|
|
||||||
|
**Server:** ana-ml2
|
||||||
|
**Port:** 9292 (configurable via `.env`)
|
||||||
|
**GPU:** both (unpinned — `runtime: nvidia` grants access to all devices; per-model GPU selection happens inside `config.yaml`)
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **`compose.yaml`** — canonical compose. Deployed to `/opt/docker/compose/llama-swap/compose.yaml` on ana-ml2.
|
||||||
|
- **`.env.example`** — template for the per-host `.env`. Copy to `.env` on the server and tweak.
|
||||||
|
- **`config.yaml`** — model definitions and groups. Deployed to `/opt/docker/conf/llama-swap/config.yaml` on the server.
|
||||||
|
|
||||||
|
Homepage labels are in the compose file under the `AI Systems` group, matching the convention used by `vllm-qwen3` and `infinity`.
|
||||||
|
|
||||||
|
## Deploy a fresh install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/deploy-stack.sh ana-ml2 llama-swap
|
||||||
|
|
||||||
|
ssh ana-ml2 '
|
||||||
|
cd /opt/docker/compose/llama-swap && \
|
||||||
|
cp -n .env.example .env && \
|
||||||
|
docker compose config && \
|
||||||
|
docker compose up -d && \
|
||||||
|
docker compose logs --tail=30
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model reference conventions
|
||||||
|
|
||||||
|
- **Modern entries:** use `-hf <user>/<repo>[:<quant>]` — reads from the shared HF cache, nothing to pre-stage outside `hf download`
|
||||||
|
- **Legacy entries:** use `--model /models/<dir>/<file>.gguf` — reads GGUFs from `/tank/aimodels/llm/` (pre-HF-cache era, gradually being migrated)
|
||||||
|
|
||||||
|
New models should prefer the `-hf` pattern.
|
||||||
|
|
||||||
|
## Deploy updates to config only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After editing config.yaml here:
|
||||||
|
scp config.yaml ana-ml2:/opt/docker/conf/llama-swap/config.yaml
|
||||||
|
ssh ana-ml2 'cd /opt/docker/compose/llama-swap && docker compose restart'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy updates to compose only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After editing compose.yaml or .env.example here:
|
||||||
|
scripts/deploy-stack.sh ana-ml2 llama-swap
|
||||||
|
ssh ana-ml2 'cd /opt/docker/compose/llama-swap && docker compose up -d'
|
||||||
|
```
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# llama-swap — GGUF model server with on-demand model swapping.
|
||||||
|
#
|
||||||
|
# Proxies OpenAI-compatible API requests to llama.cpp server instances
|
||||||
|
# and swaps which model is loaded into VRAM per request. Runs on
|
||||||
|
# ana-ml2 using both GPUs dynamically (no explicit device pinning —
|
||||||
|
# llama-swap picks per-model-definition).
|
||||||
|
#
|
||||||
|
# Model definitions live in /opt/docker/conf/llama-swap/config.yaml on
|
||||||
|
# the server. Canonical copy of that config is config.yaml in this
|
||||||
|
# workspace; deploy with scp + `docker compose restart` or the script
|
||||||
|
# at the bottom of README.md.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
llama-swap:
|
||||||
|
image: ghcr.io/mostlygeek/llama-swap:${LLAMA_SWAP_VERSION}
|
||||||
|
container_name: llama-swap
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
runtime: nvidia
|
||||||
|
ports:
|
||||||
|
- "${LLAMA_SWAP_PORT}:8080"
|
||||||
|
volumes:
|
||||||
|
- /opt/docker/conf/llama-swap/config.yaml:/app/config.yaml
|
||||||
|
- ${MODELS_DIR}:/models
|
||||||
|
- ${HF_CACHE_DIR}:/hfcache
|
||||||
|
environment:
|
||||||
|
- HF_HOME=/hfcache
|
||||||
|
- HF_HUB_CACHE=/hfcache/hub
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=AI Systems
|
||||||
|
- homepage.name=llama-swap
|
||||||
|
- homepage.icon=mdi-swap-horizontal
|
||||||
|
- homepage.description=GGUF model swapper (llama.cpp; ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.54:${LLAMA_SWAP_PORT}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# llama-swap configuration for PFI-ANA
|
||||||
|
# Optimized and synchronized with /models disk inventory
|
||||||
|
# Last updated: 2026-04-10
|
||||||
|
#
|
||||||
|
# KB Sources:
|
||||||
|
# - reference/nemotron-3-super-running-parameters.md
|
||||||
|
# - reference/nemotron-3-nano-running-parameters.md
|
||||||
|
# - reference/qwen3.5-running-parameters.md
|
||||||
|
# - reference/qwen3-coder-next-running-parameters.md
|
||||||
|
# - reference/gemma-4-running-parameters.md
|
||||||
|
# - reference/qwen3-embedding-running-parameters.md
|
||||||
|
# - reference/qwen3-reranker-running-parameters.md
|
||||||
|
#
|
||||||
|
# Changelog:
|
||||||
|
# 2025-07-22: Removed jina-reranker-v3 (unused, out of rotation).
|
||||||
|
# 2026-04-10: Fixed Qwen3-Embedding pooling (mean→last; causal LM uses last-token
|
||||||
|
# pooling). Fixed ctx-size 4096→8192 for embedding+reranker. Fixed
|
||||||
|
# reranker: removed --embeddings flag (not an embedding model).
|
||||||
|
# 2026-04-17: Added Qwen3.6-35B-A3B Abliterated Heretic Q8_0 via -hf syntax.
|
||||||
|
# Requires HF_HOME=/hfcache in compose (see docker-compose.yml).
|
||||||
|
# New convention: use -hf repo[:quant] instead of --model /path.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Default 1200 seconds (20 min) to wait for model to be available to load.
|
||||||
|
healthCheckTimeout: 1200
|
||||||
|
|
||||||
|
# logLevel: sets the logging value
|
||||||
|
# - optional, default: info
|
||||||
|
# - Valid log levels: debug, info, warn, error
|
||||||
|
logLevel: info
|
||||||
|
|
||||||
|
# metricsMaxInMemory: maximum number of metrics to keep in memory
|
||||||
|
# - optional, default: 1000
|
||||||
|
metricsMaxInMemory: 1000
|
||||||
|
|
||||||
|
# startPort: sets the starting port number for the automatic ${PORT} macro.
|
||||||
|
# - optional, default: 5800
|
||||||
|
# - the ${PORT} macro can be used in model.cmd and model.proxy settings
|
||||||
|
# - it is automatically incremented for every model that uses it
|
||||||
|
# startPort: 10001
|
||||||
|
|
||||||
|
models:
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# QWEN 3.5 MODELS (KB-recommended settings)
|
||||||
|
# - Thinking mode: temp 1.0, top-p 0.95, top-k 20, min-p 0.0, presence_penalty 1.5
|
||||||
|
# - Coding (precise): temp 0.6, top-p 0.95, top-k 20, min-p 0.0, presence_penalty 0.0
|
||||||
|
# - Non-thinking general: temp 0.7, top-p 0.8, top-k 20, min-p 0.0, presence_penalty 1.5
|
||||||
|
# - Context: 256K native (start 16K-32K for responsiveness)
|
||||||
|
# - Gibberish fix: add --cache-type-k bf16 --cache-type-v bf16
|
||||||
|
# - No Ollama support for Qwen3.5 GGUFs — use llama.cpp only
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"qwen3.5-35-a3b":
|
||||||
|
name: "Qwen 3.5 35B-A3B Thinking"
|
||||||
|
description: "MoE reasoning model. 3B active params, general-purpose thinking/chat."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_Qwen3.5-35B-A3B-GGUF/Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 20
|
||||||
|
--min-p 0.00
|
||||||
|
--presence-penalty 1.5
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
|
||||||
|
"qwen3.5-122b-a10b":
|
||||||
|
name: "Qwen 3.5 122B-A10B UD-Q4_K_XL"
|
||||||
|
description: "Large MoE reasoning model. 10B active params, heavy reasoning tasks."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_Qwen3.5-122B-A10B-GGUF/UD-Q4_K_XL/Qwen3.5-122B-A10B-UD-Q4_K_XL-00001-of-00003.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 20
|
||||||
|
--min-p 0.00
|
||||||
|
--presence-penalty 1.5
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
|
||||||
|
"qwen3.5-9b":
|
||||||
|
name: "Qwen 3.5 9B UD-Q4_K_XL"
|
||||||
|
description: "Dense 9B model. Lightweight general-purpose chat and reasoning."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_Qwen3.5-9B-GGUF/Qwen3.5-9B-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 20
|
||||||
|
--min-p 0.00
|
||||||
|
--presence-penalty 1.5
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Qwen 3.6 — uses -hf syntax, reads from HF_HOME=/hfcache (host pre-download)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
"qwen3.6-35-a3b-abliterated":
|
||||||
|
name: "Qwen 3.6 35B-A3B Abliterated Heretic Q8_0"
|
||||||
|
description: "Qwen3.6 MoE, 3B active. Abliterated/heretic variant of BF16 quantized to Q8_0. ~38GB."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--jinja
|
||||||
|
-hf IIEleven11/Qwen3.6-35B-A3B-Abliterated-Heretic-BF16-Q8_0-GGUF
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 20
|
||||||
|
--min-p 0.00
|
||||||
|
--presence-penalty 1.5
|
||||||
|
--repeat-penalty 1.0
|
||||||
|
--reasoning on
|
||||||
|
--reasoning-format deepseek
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# NEMOTRON MODELS (KB-recommended settings)
|
||||||
|
# - General Chat: temp 1.0, top-p 1.0, min_p 0.01
|
||||||
|
# - Tool Calling: temp 0.6, top-p 0.95, min_p 0.01
|
||||||
|
# - NoPE architecture: no YaRN needed
|
||||||
|
# - DEPRECATED --special flag for reasoning tokens
|
||||||
|
# - --special flag causes issues.
|
||||||
|
# - Start ctx 16K-32K, increase cautiously
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"nemotron-3-super-120b":
|
||||||
|
name: "NVIDIA Nemotron 3 Super 120B-A12B UD-Q4_K_XL"
|
||||||
|
description: "Flagship NVIDIA reasoning model. 12B active of 120B, MoE. 64-72GB VRAM at Q4."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_NVIDIA-Nemotron-3-Super-120B-A12B-GGUF/UD-Q4_K_XL/NVIDIA-Nemotron-3-Super-120B-A12B-UD-Q4_K_XL-00001-of-00003.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 16384
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 1.0
|
||||||
|
--min-p 0.01
|
||||||
|
--seed 3407
|
||||||
|
|
||||||
|
"nemotron-3-nano-30b":
|
||||||
|
name: "NVIDIA Nemotron 3 Nano 30B-A3B UD-Q4_K_XL"
|
||||||
|
description: "Compact Nemotron. 3B active of 30B, MoE. ~24GB at Q4. Best performance/size on 24GB GPUs."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--special
|
||||||
|
--model /models/unsloth_Nemotron-3-Nano-30B-A3B-GGUF/Nemotron-3-Nano-30B-A3B-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 1.0
|
||||||
|
--min-p 0.01
|
||||||
|
--seed 3407
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# GEMMA 4 MODELS (KB-recommended settings)
|
||||||
|
# - All variants: temp 1.0, top-p 0.95, top-k 64, repeat_penalty 1.0
|
||||||
|
# - Thinking: enable via --chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
# - Multi-turn: only keep final visible answer in history (not thought blocks)
|
||||||
|
# - Context: E2B/E4B=128K, 26B-A4B/31B=256K. Start at 32K.
|
||||||
|
# - ⚠️ Do NOT use CUDA 13.2 runtime — causes poor outputs
|
||||||
|
# - Use llama-server (not llama-cli) for thinking control
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"gemma4-26b-a4b":
|
||||||
|
name: "Gemma 4 26B-A4B"
|
||||||
|
description: "Google DeepMind Gemma 4 MoE, 4B active params, 256K context. Best speed/quality tradeoff."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 64
|
||||||
|
--repeat-penalty 1.0
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
|
||||||
|
"gemma4-31b-dense":
|
||||||
|
name: "Gemma 4 31B Dense"
|
||||||
|
description: "Google DeepMind Gemma 4 dense 31B. Maximum quality for complex reasoning, 256K context."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_gemma-4-31B-it-GGUF/gemma-4-31B-it-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 64
|
||||||
|
--repeat-penalty 1.0
|
||||||
|
--chat-template-kwargs '{"enable_thinking":true}'
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# GLM MODELS
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"glm4.7-flash":
|
||||||
|
name: "GLM 4.7 Flash UD-Q4_K_XL"
|
||||||
|
description: "THUDM GLM 4.7 Flash. Fast inference, general-purpose chat."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_GLM-4.7-Flash-GGUF/GLM-4.7-Flash-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 40000
|
||||||
|
--flash-attn on
|
||||||
|
--temp 0.6
|
||||||
|
--top-p 0.95
|
||||||
|
|
||||||
|
"glm-steam-106b":
|
||||||
|
name: "GLM Steam 106B-A12B Q4_K_M"
|
||||||
|
description: "TheDrummer GLM Steam MoE. 12B active of 106B. Creative and RP-focused."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/RP/bartowski_TheDrummer_GLM-Steam-106B-A12B-v1-GGUF/TheDrummer_GLM-Steam-106B-A12B-v1-Q4_K_M-00001-of-00002.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 40000
|
||||||
|
--flash-attn on
|
||||||
|
--temp 0.6
|
||||||
|
--top-p 0.95
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# SKYFALL MODELS
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"skyfall-r1-31b-q6k":
|
||||||
|
name: "Skyfall R1 31B v4 Q6_K_L"
|
||||||
|
description: "TheDrummer Skyfall R1 31B v4. General-purpose reasoning."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/bartowski_TheDrummer_Skyfall-31B-v4-GGUF/TheDrummer_Skyfall-31B-v4-Q6_K_L.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 40000
|
||||||
|
--flash-attn on
|
||||||
|
|
||||||
|
"skyfall-r1-31b-v4a":
|
||||||
|
name: "Skyfall R1 31B v4a Q6_K (RP)"
|
||||||
|
description: "BeaverAI Skyfall R1 v4a variant. RP/creative-focused."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/RP/BeaverAI_Skyfall-R1-31B-v4a-GGUF/Skyfall-R1-31B-v4a-Q6_K.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 40000
|
||||||
|
--flash-attn on
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# CODER MODELS
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"qwen3-coder-next":
|
||||||
|
name: "Qwen3 Coder Next UD-Q4_K_XL"
|
||||||
|
description: "Latest Qwen3 Coder. Non-reasoning model, optimized for code gen. KB: temp 1.0, top-k 40, min-p 0.01."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-UD-Q4_K_XL.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 1.0
|
||||||
|
--top-p 0.95
|
||||||
|
--top-k 40
|
||||||
|
--min-p 0.01
|
||||||
|
--repeat-penalty 1.0
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# LARGE / SPECIAL-PURPOSE MODELS
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"kimik2-q2kxl":
|
||||||
|
name: "Kimi K2 Instruct UD-Q2_K_XL"
|
||||||
|
description: "Moonshot Kimi K2. Huge MoE model (8-shard Q2). Limited GPU layers due to size."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_Kimi-K2-Instruct-0905-GGUF/UD-Q2_K_XL/Kimi-K2-Instruct-0905-UD-Q2_K_XL-00001-of-00008.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 2
|
||||||
|
--temp 0.6
|
||||||
|
--top-p 0.95
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# GRANITE MODELS (IBM)
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"granite-4-small":
|
||||||
|
name: "Granite 4.0 Small Q4_K_M"
|
||||||
|
description: "IBM Granite 4.0 Small. Deterministic utility model for structured tasks."
|
||||||
|
ttl: 0
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/unsloth_granite-4.0-h-small-GGUF/granite-4.0-h-small-Q4_K_M.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 120000
|
||||||
|
--flash-attn on
|
||||||
|
--top-p 1.0
|
||||||
|
--temp 0.0
|
||||||
|
--top-k 0
|
||||||
|
|
||||||
|
"granite-4-micro":
|
||||||
|
name: "Granite 4.0 Micro Q4_K_M"
|
||||||
|
description: "IBM Granite 4.0 Micro. Ultra-lightweight for fast structured responses."
|
||||||
|
ttl: 600
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--context-shift
|
||||||
|
--model /models/ibm-granite_granite-4.0-micro-GGUF/granite-4.0-micro-Q4_K_M.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 999
|
||||||
|
--ctx-size 32768
|
||||||
|
--flash-attn on
|
||||||
|
--temp 0.0
|
||||||
|
--top-p 1.0
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# EMBEDDING MODELS (persistent, always loaded)
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"embeddinggemma-300M":
|
||||||
|
name: "Embedding Gemma 300M"
|
||||||
|
description: "Google Embedding Gemma for vectorization."
|
||||||
|
ttl: 0
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--embedding
|
||||||
|
--pooling cls
|
||||||
|
--model /models/ggml-org_embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 0
|
||||||
|
--ctx-size 2048
|
||||||
|
--batch-size 1024
|
||||||
|
--no-mmap
|
||||||
|
--ubatch-size 1024
|
||||||
|
--cont-batching
|
||||||
|
--threads 24
|
||||||
|
|
||||||
|
"qwen3-embedding-0.6B":
|
||||||
|
name: "Qwen3 Embedding 0.6B"
|
||||||
|
description: "Qwen3 Embedding model for vectorization. 32K context, last-token pooling (decoder/causal LM)."
|
||||||
|
ttl: 0
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--embeddings
|
||||||
|
--pooling last
|
||||||
|
--model /models/Qwen_Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 0
|
||||||
|
--ctx-size 8192
|
||||||
|
--batch-size 8192
|
||||||
|
--ubatch-size 2048
|
||||||
|
--no-mmap
|
||||||
|
--cont-batching
|
||||||
|
--threads 24
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# RERANKING MODELS (persistent, always loaded)
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
"qwen3-reranker-0.6B":
|
||||||
|
name: "Qwen3 Reranker 0.6B"
|
||||||
|
description: "Qwen3 Reranker for retrieval reranking. Causal LM scoring yes/no logits at last token. Replaces BGE v2."
|
||||||
|
ttl: 0
|
||||||
|
cmd: |
|
||||||
|
/app/llama-server
|
||||||
|
--reranking
|
||||||
|
--pooling rank
|
||||||
|
--model /models/ggml-org_Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf
|
||||||
|
--port ${PORT}
|
||||||
|
--n-gpu-layers 0
|
||||||
|
--ctx-size 8192
|
||||||
|
--batch-size 8192
|
||||||
|
--ubatch-size 2048
|
||||||
|
--cont-batching
|
||||||
|
--threads 24
|
||||||
|
|
||||||
|
# NOTE: jina-reranker-v3 removed 2025-07-22 — unused, out of rotation.
|
||||||
|
# Qwen3 Reranker handles all reranking duties.
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GROUPS
|
||||||
|
# - swap: false = models in group can coexist in memory
|
||||||
|
# - exclusive: false = group can share memory with other groups
|
||||||
|
# - persistent: true = models never unload (for utility/embedding)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
groups:
|
||||||
|
"high-reasoning":
|
||||||
|
swap: false
|
||||||
|
exclusive: false
|
||||||
|
members:
|
||||||
|
- "qwen3.5-35-a3b"
|
||||||
|
- "gemma4-31b-dense"
|
||||||
|
- "nemotron-3-nano-30b"
|
||||||
|
|
||||||
|
"heavy-moe":
|
||||||
|
swap: false
|
||||||
|
exclusive: false
|
||||||
|
members:
|
||||||
|
- "qwen3.5-122b-a10b"
|
||||||
|
- "nemotron-3-super-120b"
|
||||||
|
- "kimik2-q2kxl"
|
||||||
|
- "glm-steam-106b"
|
||||||
|
|
||||||
|
"utility":
|
||||||
|
swap: false
|
||||||
|
exclusive: false
|
||||||
|
persistent: true
|
||||||
|
members:
|
||||||
|
- "embeddinggemma-300M"
|
||||||
|
- "qwen3-embedding-0.6B"
|
||||||
|
- "qwen3-reranker-0.6B"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# rest-server-ana stack tunables. Copy to `.env` on ana-docker.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit if needed
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Matches stacks/rest-server-nh3/.env.example — keep them aligned so
|
||||||
|
# restic clients see the same URL shape against either endpoint.
|
||||||
|
|
||||||
|
REST_SERVER_VERSION=latest
|
||||||
|
|
||||||
|
# Host port the container listens on (container internal is 8000)
|
||||||
|
REST_PORT=8000
|
||||||
|
|
||||||
|
# Where restic pack files live. TrueNAS NFS share is already mounted at
|
||||||
|
# /mnt/backup on ana-docker; the "repo/ana" subdir is the historical
|
||||||
|
# location used by the prior (non-private-repos) rest-server.
|
||||||
|
#
|
||||||
|
# With --private-repos, layout becomes:
|
||||||
|
# ${DATA_DIR}/ana-docker/
|
||||||
|
# ${DATA_DIR}/ana-ml2/
|
||||||
|
# ${DATA_DIR}/nh3-docker/
|
||||||
|
# ${DATA_DIR}/esh-docker-vm/
|
||||||
|
# Plus the auth file at ${DATA_DIR}/.htpasswd.
|
||||||
|
DATA_DIR=/mnt/backup/restic/repo/ana
|
||||||
|
|
||||||
|
# UID/GID the container process runs as. Must match the owner of
|
||||||
|
# DATA_DIR so NFS root_squash doesn't bite. On ana-docker this is the
|
||||||
|
# `lkraven` user (1000:1000).
|
||||||
|
REST_UID=1000
|
||||||
|
REST_GID=1000
|
||||||
|
|
||||||
|
# Timezone — affects log lines and /metrics timestamps
|
||||||
|
TZ=America/Los_Angeles
|
||||||
|
|
||||||
|
# Extra rest-server flags. Examples:
|
||||||
|
# --prometheus-no-auth — make /metrics public (needed if Beszel or
|
||||||
|
# Prometheus scrapes without creds)
|
||||||
|
# --no-verify-upload — trust the client's hash; faster writes
|
||||||
|
# Leave blank unless you have a reason.
|
||||||
|
EXTRA_OPTIONS=
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# rest-server-ana
|
||||||
|
|
||||||
|
Anaheim-site restic backup endpoint. Replaces the older `restic` stack on ana-docker with the same auth model as `rest-server-nh3` on the Synology, so every client host uses identical URL shapes against either endpoint.
|
||||||
|
|
||||||
|
**Server:** ana-docker (`10.250.50.70`)
|
||||||
|
**Port:** `http://10.250.50.70:8000`
|
||||||
|
**Data:** `/mnt/backup/restic/repo/ana/` (TrueNAS NFS mount on the host)
|
||||||
|
|
||||||
|
Paired with:
|
||||||
|
- **`rest-server-nh3`** on the Synology (`10.100.50.50:8000`, data on Btrfs).
|
||||||
|
- A cross-site rsync job (TBD, on ana-docker) that mirrors each site's data tree to the other so either NAS can fully restore either site's hosts.
|
||||||
|
|
||||||
|
## What changed from the old `restic` stack
|
||||||
|
|
||||||
|
| | old `restic` on ana-docker | this stack |
|
||||||
|
|---|---|---|
|
||||||
|
| `--private-repos` | no | **yes** |
|
||||||
|
| `--append-only` | no | **yes** |
|
||||||
|
| `--prometheus` | no | **yes** |
|
||||||
|
| healthcheck | no | yes |
|
||||||
|
| `.env`-driven | no | yes |
|
||||||
|
| restart policy | none | `unless-stopped` |
|
||||||
|
| image version | floating `latest` | `${REST_SERVER_VERSION}` |
|
||||||
|
| stack dir on server | `/opt/docker/compose/restic/` | `/opt/docker/compose/rest-server-ana/` |
|
||||||
|
|
||||||
|
Data path is unchanged (`/mnt/backup/restic/repo/ana/`) so nothing new needs to be allocated on TrueNAS.
|
||||||
|
|
||||||
|
## Pre-deploy: clean the data dir and create htpasswd
|
||||||
|
|
||||||
|
Since there's nothing in the existing path we want to keep, start fresh so the on-disk layout matches `--private-repos`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker '
|
||||||
|
# Stop the old stack so port 8000 and the data dir are free
|
||||||
|
cd /opt/docker/compose/restic
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Wipe the old non-private-repos layout
|
||||||
|
sudo rm -rf /mnt/backup/restic/repo/ana/*
|
||||||
|
sudo rm -rf /mnt/backup/restic/repo/ana/.htpasswd # if present
|
||||||
|
|
||||||
|
# Create the htpasswd file. Use the same passwords here as on the NH3
|
||||||
|
# Synology so each host has one credential that works at either endpoint.
|
||||||
|
sudo touch /mnt/backup/restic/repo/ana/.htpasswd
|
||||||
|
sudo chmod 600 /mnt/backup/restic/repo/ana/.htpasswd
|
||||||
|
'
|
||||||
|
|
||||||
|
# Generate htpasswd entries locally (one per host) and append. Using the
|
||||||
|
# `httpd:2.4-alpine` throwaway container so we do not depend on
|
||||||
|
# apache2-utils being installed on ana-docker.
|
||||||
|
for user in ana-docker ana-ml2 nh3-docker esh-docker-vm; do
|
||||||
|
read -rs -p "password for $user (must match the NH3 Synology): " pw; echo
|
||||||
|
docker run --rm httpd:2.4-alpine htpasswd -nbB "$user" "$pw" \
|
||||||
|
| ssh ana-docker 'sudo tee -a /mnt/backup/restic/repo/ana/.htpasswd >/dev/null'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run that locally and don't have Docker here, equivalent on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker "docker run --rm httpd:2.4-alpine htpasswd -nbB <user> '<pw>'" \
|
||||||
|
| ssh ana-docker 'sudo tee -a /mnt/backup/restic/repo/ana/.htpasswd >/dev/null'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Stage the new stack and push it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stage the stack into the mirror (if not already done via sync-stacks.sh)
|
||||||
|
mkdir -p stacks-mirror/ana-docker/rest-server-ana
|
||||||
|
cp stacks/rest-server-ana/compose.yaml stacks/rest-server-ana/.env.example \
|
||||||
|
stacks-mirror/ana-docker/rest-server-ana/
|
||||||
|
|
||||||
|
scripts/deploy-stack.sh ana-docker rest-server-ana
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm at the prompt. Then on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker '
|
||||||
|
cd /opt/docker/compose/rest-server-ana
|
||||||
|
cp -n .env.example .env
|
||||||
|
docker compose config
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs --tail=30
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retire the old stack
|
||||||
|
|
||||||
|
Once the new one is healthy and the first repo has initialized successfully from a client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ana-docker '
|
||||||
|
cd /opt/docker/compose/restic
|
||||||
|
docker compose down
|
||||||
|
# Optionally remove the old stack dir (keep it for a release or two
|
||||||
|
# in case you need to roll back):
|
||||||
|
# rm -rf /opt/docker/compose/restic
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 401 from the root — service up, auth enforced
|
||||||
|
curl -sS -o /dev/null -w 'unauth status=%{http_code}\n' \
|
||||||
|
http://10.250.50.70:8000/
|
||||||
|
|
||||||
|
# 200 / 404 from a real user+password — auth valid, --private-repos path OK
|
||||||
|
curl -sS -o /dev/null -w 'auth status=%{http_code}\n' \
|
||||||
|
-u ana-docker:<password> http://10.250.50.70:8000/ana-docker/
|
||||||
|
|
||||||
|
# Init a repo from a client host (one-time per host)
|
||||||
|
ssh ana-docker '
|
||||||
|
export RESTIC_REPOSITORY="rest:http://ana-docker:<rest-pw>@10.250.50.70:8000/ana-docker/"
|
||||||
|
export RESTIC_PASSWORD="<client-side-encryption-passphrase>"
|
||||||
|
restic init
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prune ceremony
|
||||||
|
|
||||||
|
Same as `rest-server-nh3` — prune is blocked by `--append-only`. Two options, pick one per endpoint:
|
||||||
|
|
||||||
|
- **Temporary flag flip:** edit compose, remove `--append-only` from `OPTIONS`, `docker compose up -d`, run `restic forget --prune` from origin hosts, put the flag back, `docker compose up -d`. Quarterly change.
|
||||||
|
- **Second endpoint on a different port:** stand up a sibling container (e.g. port `8001`) against the same data dir without `--append-only`, reachable only from a trusted host. Everyday backups still hit `:8000`.
|
||||||
|
|
||||||
|
If you go the second-endpoint route, copy this stack to `stacks/rest-server-ana-prune/` with `REST_PORT=8001` and `--append-only` removed from the compose.
|
||||||
|
|
||||||
|
## Off-site replication
|
||||||
|
|
||||||
|
Scheduled on ana-docker (to be written):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull NH3's tree down to this side
|
||||||
|
rsync -avz --delete admin@10.100.50.50:/volume1/Backup/restic/ /mnt/backup/restic-mirror-nh3/
|
||||||
|
# Push our tree to NH3
|
||||||
|
rsync -avz --delete /mnt/backup/restic/repo/ana/ admin@10.100.50.50:/volume1/Backup/restic-mirror-ana/
|
||||||
|
```
|
||||||
|
|
||||||
|
Two unidirectional syncs, each running in the direction its data flows. Prune runs only at the origin so the mirror shrinks correctly.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# rest-server (Anaheim) — restic backup target for the fleet.
|
||||||
|
#
|
||||||
|
# Deploys to ana-docker. Data dir is on the TrueNAS NFS mount
|
||||||
|
# (/mnt/backup/restic/repo/ana) so snapshots on the NAS side protect the
|
||||||
|
# backup blobs themselves.
|
||||||
|
#
|
||||||
|
# Mirrors stacks/rest-server-nh3/ in every meaningful way — same auth
|
||||||
|
# model, same on-disk layout, same operational semantics — so each client
|
||||||
|
# host uses an identical URL shape against either endpoint:
|
||||||
|
#
|
||||||
|
# rest:http://<user>:<pw>@10.100.50.50:8000/<user>/ (NH3 Synology)
|
||||||
|
# rest:http://<user>:<pw>@10.250.50.70:8000/<user>/ (this stack)
|
||||||
|
#
|
||||||
|
# Auth model:
|
||||||
|
# --private-repos : URL path must start with /<user>/ and the HTTP
|
||||||
|
# basic-auth user must match. Per-host repos are
|
||||||
|
# strictly isolated.
|
||||||
|
# --append-only : on-disk data can be added but not removed or
|
||||||
|
# rewritten; a compromised host can't wipe its own
|
||||||
|
# history. Prune requires disabling this (see README).
|
||||||
|
#
|
||||||
|
# Credentials come from /data/.htpasswd — see README for populating it.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
rest-server:
|
||||||
|
image: restic/rest-server:${REST_SERVER_VERSION}
|
||||||
|
container_name: rest-server
|
||||||
|
restart: unless-stopped
|
||||||
|
# Run as the UID that owns the NFS-backed data dir, so file I/O
|
||||||
|
# is not subject to NFS root_squash. On ana-docker this is lkraven (1000).
|
||||||
|
user: "${REST_UID:-1000}:${REST_GID:-1000}"
|
||||||
|
ports:
|
||||||
|
- "${REST_PORT}:8000"
|
||||||
|
volumes:
|
||||||
|
- ${DATA_DIR}:/data
|
||||||
|
environment:
|
||||||
|
- OPTIONS=--private-repos --append-only --prometheus ${EXTRA_OPTIONS:-}
|
||||||
|
- TZ=${TZ:-America/Los_Angeles}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8000/metrics >/dev/null 2>&1 || [ $? -eq 6 ] && exit 0 || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=PFI-ANA
|
||||||
|
- homepage.name=Restic (rest-server)
|
||||||
|
- homepage.icon=mdi-cloud-upload
|
||||||
|
- homepage.description=Anaheim restic endpoint (data on TrueNAS NFS)
|
||||||
|
- homepage.href=http://10.250.50.70:${REST_PORT}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# rest-server (NH3 Synology) tunables. Copy to `.env` on the Synology.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit if needed
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Synology-specific notes:
|
||||||
|
# - Put this stack under /volume1/docker/compose/rest-server/
|
||||||
|
# (Container Manager's default project root pattern on DSM 7.x).
|
||||||
|
# - Point DATA_DIR at a Btrfs share you own — /volume1/Backup/restic
|
||||||
|
# is the natural choice since /volume1/Backup is already the
|
||||||
|
# fleet-facing backup share (exported via NFS).
|
||||||
|
|
||||||
|
REST_SERVER_VERSION=latest
|
||||||
|
|
||||||
|
# Host port the container listens on (container internal port is 8000)
|
||||||
|
REST_PORT=8000
|
||||||
|
|
||||||
|
# Where restic pack files live on the Synology. Must be a writable
|
||||||
|
# Btrfs path. Will hold one subdir per user (--private-repos layout):
|
||||||
|
# ${DATA_DIR}/ana-docker/
|
||||||
|
# ${DATA_DIR}/ana-ml2/
|
||||||
|
# ${DATA_DIR}/nh3-docker/
|
||||||
|
# ${DATA_DIR}/esh-docker-vm/
|
||||||
|
# Plus the auth file at ${DATA_DIR}/.htpasswd.
|
||||||
|
DATA_DIR=/volume1/Backup/restic
|
||||||
|
|
||||||
|
# Timezone — affects log lines and the /metrics timestamps
|
||||||
|
TZ=America/Los_Angeles
|
||||||
|
|
||||||
|
# Any extra rest-server flags (rare). Some useful ones:
|
||||||
|
# --no-verify-upload — trust the client's hash; faster writes
|
||||||
|
# --max-size=<bytes> — cap per-repo size
|
||||||
|
# Leave blank unless you have a reason.
|
||||||
|
EXTRA_OPTIONS=
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# rest-server-nh3
|
||||||
|
|
||||||
|
NH3-site restic backup endpoint. Runs in Synology Container Manager on `10.100.50.50` and stores pack files on a Btrfs share so Synology snapshots protect against local corruption.
|
||||||
|
|
||||||
|
**Server:** Synology RS2418+ at `10.100.50.50`
|
||||||
|
**Port:** `http://10.100.50.50:8000` (configurable via `.env`)
|
||||||
|
**Data:** `/volume1/Backup/restic/` (configurable)
|
||||||
|
|
||||||
|
Paired with the existing `restic rest-server` on **ana-docker** (`http://10.250.50.70:8000`, data on TrueNAS NFS) as the Anaheim-side endpoint. Each fleet host backs up to the rest-server closest to it; an rsync job on ana-docker mirrors the two trees against each other for off-site redundancy.
|
||||||
|
|
||||||
|
## Auth model
|
||||||
|
|
||||||
|
`--private-repos` + `--append-only`, enforced via htpasswd:
|
||||||
|
|
||||||
|
- One HTTP basic-auth user **per host** (`ana-docker`, `ana-ml2`, `nh3-docker`, `esh-docker-vm`).
|
||||||
|
- Each user can only write under `/<username>/…` — a compromised host can't see or delete another host's data.
|
||||||
|
- Append-only means a compromised client can add to its own repo but can't rewrite or delete existing packs, so ransomware on a backed-up host doesn't destroy history.
|
||||||
|
- **Trade-off:** `restic forget --prune` can't run against an append-only endpoint. Prune ceremony described at the bottom of this file.
|
||||||
|
|
||||||
|
## Pre-deploy: create the data path and htpasswd
|
||||||
|
|
||||||
|
On the Synology (SSH in as an admin-capable user, or DSM *File Station*):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create the restic data share on a Btrfs volume
|
||||||
|
ssh admin@10.100.50.50 'sudo mkdir -p /volume1/Backup/restic && \
|
||||||
|
sudo chown 1000:1000 /volume1/Backup/restic && \
|
||||||
|
sudo chmod 700 /volume1/Backup/restic'
|
||||||
|
|
||||||
|
# 2. Generate htpasswd entries. The Synology doesn't ship apache2-utils,
|
||||||
|
# so use a throwaway container:
|
||||||
|
ssh admin@10.100.50.50 'cd /volume1/Backup/restic && \
|
||||||
|
sudo touch .htpasswd && sudo chown 1000:1000 .htpasswd && sudo chmod 600 .htpasswd'
|
||||||
|
|
||||||
|
for user in ana-docker ana-ml2 nh3-docker esh-docker-vm; do
|
||||||
|
read -s -p "password for $user: " pw; echo
|
||||||
|
ssh admin@10.100.50.50 \
|
||||||
|
"docker run --rm httpd:2.4-alpine htpasswd -nbB $user '$pw'" \
|
||||||
|
| ssh admin@10.100.50.50 "sudo tee -a /volume1/Backup/restic/.htpasswd >/dev/null"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Record every password in your off-host password manager (1Password / Vaultwarden etc.) — you'll paste them into Backrest and into the systemd timer configs later.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
In Synology **Container Manager**:
|
||||||
|
|
||||||
|
1. *Project* → **Create** → Name `rest-server`, Path `/volume1/docker/compose/rest-server/`.
|
||||||
|
2. Copy `compose.yaml` into the project path; copy `.env.example` → `.env` and edit if needed (default `REST_PORT=8000` and `DATA_DIR=/volume1/Backup/restic` should be fine).
|
||||||
|
3. Start the project.
|
||||||
|
|
||||||
|
CLI equivalent (if you have SSH + a shell account that can run Docker on the NAS):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@10.100.50.50
|
||||||
|
sudo mkdir -p /volume1/docker/compose/rest-server
|
||||||
|
sudo chown $USER /volume1/docker/compose/rest-server
|
||||||
|
cd /volume1/docker/compose/rest-server
|
||||||
|
|
||||||
|
# scp the files from this workspace, then:
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose config
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
From this workstation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Should return "200 OK" or redirect to /metrics; anything 5xx is a problem.
|
||||||
|
curl -u ana-docker:<password> -sv http://10.100.50.50:8000/ana-docker/ -o /dev/null
|
||||||
|
|
||||||
|
# Once restic is wired up, init the repo (one-time, per host):
|
||||||
|
RESTIC_REPOSITORY='rest:http://ana-docker:<password>@10.100.50.50:8000/ana-docker/' \
|
||||||
|
RESTIC_PASSWORD='<client-side-encryption-passphrase>' \
|
||||||
|
restic init
|
||||||
|
```
|
||||||
|
|
||||||
|
Restic URI shape for each host (paste into Backrest when adding the repo):
|
||||||
|
|
||||||
|
```
|
||||||
|
rest:http://<user>:<pass>@10.100.50.50:8000/<user>/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prune ceremony (because of --append-only)
|
||||||
|
|
||||||
|
Because `--append-only` blocks deletes, `restic forget --prune` will fail against the live endpoint. Two options, pick one and stick with it:
|
||||||
|
|
||||||
|
### Option A — temporary flag flip (simplest, requires a maintenance window)
|
||||||
|
|
||||||
|
1. On the Synology, edit the stack's `.env` and set `EXTRA_OPTIONS=--no-auth` — **only kidding, don't.** Set `EXTRA_OPTIONS= ` and comment out `--append-only` in the compose `OPTIONS=` line (or parameterize if you prefer).
|
||||||
|
2. `docker compose up -d` to restart with deletes allowed.
|
||||||
|
3. Run `restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 3 --prune` from the origin host.
|
||||||
|
4. Restore `--append-only` and `docker compose up -d`.
|
||||||
|
|
||||||
|
Treat this as a quarterly change, not a cron job. Schedule it so you're present if restic hits anything weird.
|
||||||
|
|
||||||
|
### Option B — second endpoint on a different port (automation-friendly)
|
||||||
|
|
||||||
|
Stand up a second `rest-server` container against the same `DATA_DIR` without `--append-only`, listening on e.g. `8001`, reachable only from within the Synology / over VPN. A scheduled prune job hits that endpoint; day-to-day backup traffic continues to hit `:8000` in append-only mode.
|
||||||
|
|
||||||
|
If you end up wanting this, copy this stack to `stacks/rest-server-nh3-prune/` with `REST_PORT=8001` and `--append-only` removed.
|
||||||
|
|
||||||
|
## Off-site replication
|
||||||
|
|
||||||
|
Scheduled on ana-docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example — not the final script, just illustrating the shape.
|
||||||
|
rsync -avz --delete \
|
||||||
|
admin@10.100.50.50:/volume1/Backup/restic/ \
|
||||||
|
/mnt/backup/restic-mirror-nh3/
|
||||||
|
```
|
||||||
|
|
||||||
|
`rsync` is safe because restic packs are immutable once written — nothing under `/volume1/Backup/restic/<user>/data/` gets rewritten, only added or (during prune) removed. A raw `rsync --delete` with prune running only on the origin side is enough; no filesystem-level locks required.
|
||||||
|
|
||||||
|
## What doesn't live here
|
||||||
|
|
||||||
|
- No `.env` in the committed copy — only `.env.example`.
|
||||||
|
- `.htpasswd` is never checked in, never synced via `sync-stacks.sh` (its `*.ht*` isn't in the global exclude but the data dir is outside the stack path).
|
||||||
|
- Client-side restic passwords (the encryption passphrase for each repo) are separate from the HTTP auth passwords and never stored on the Synology.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# rest-server (NH3 Synology) — restic backup target for the fleet.
|
||||||
|
#
|
||||||
|
# Deploys into Synology Container Manager on 10.100.50.50. Data lives on
|
||||||
|
# a Btrfs shared folder so it gets Synology snapshots + optional
|
||||||
|
# replication to a sibling share if you configure one later.
|
||||||
|
#
|
||||||
|
# Auth model:
|
||||||
|
# --private-repos : every URL path must start with /<username>/ and the
|
||||||
|
# HTTP basic-auth user must match. One user per host.
|
||||||
|
# Per-host repos are strictly isolated.
|
||||||
|
# --append-only : on-disk data can be ADDED but not REMOVED or REWRITTEN.
|
||||||
|
# A compromised host can't delete its own history.
|
||||||
|
# Prune requires disabling this (see README).
|
||||||
|
#
|
||||||
|
# Credentials come from /data/.htpasswd — see README for how to populate
|
||||||
|
# it. That file is mounted read-only into the container.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
|
||||||
|
services:
|
||||||
|
rest-server:
|
||||||
|
image: restic/rest-server:${REST_SERVER_VERSION}
|
||||||
|
container_name: rest-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${REST_PORT}:8000"
|
||||||
|
volumes:
|
||||||
|
- ${DATA_DIR}:/data
|
||||||
|
environment:
|
||||||
|
- OPTIONS=--private-repos --append-only --prometheus ${EXTRA_OPTIONS:-}
|
||||||
|
- TZ=${TZ:-America/Los_Angeles}
|
||||||
|
# rest-server stores repos under /data and looks for /data/.htpasswd
|
||||||
|
# automatically — no extra bind mount needed as long as the htpasswd
|
||||||
|
# file is created inside DATA_DIR before startup.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8000/metrics >/dev/null || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# vllm-qwen3 stack tunables. Copy this to `.env` on the server before deploying.
|
||||||
|
#
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit .env with real values
|
||||||
|
# docker compose up -d
|
||||||
|
|
||||||
|
# Image version — pin for reproducibility (`latest` for edge)
|
||||||
|
VLLM_VERSION=latest
|
||||||
|
|
||||||
|
# Host ports (container always listens on 8000 internally)
|
||||||
|
EMBED_PORT=8001
|
||||||
|
RERANK_PORT=8002
|
||||||
|
|
||||||
|
# GPU assignment — both services share this GPU
|
||||||
|
# (ana-ml2 has 0 and 1; default 1 keeps 0 free for heavy LLM work)
|
||||||
|
GPU_ID=1
|
||||||
|
|
||||||
|
# Models — reference by full repo name in API requests
|
||||||
|
EMBED_MODEL=Qwen/Qwen3-Embedding-0.6B
|
||||||
|
RERANK_MODEL=Qwen/Qwen3-Reranker-0.6B
|
||||||
|
|
||||||
|
# GPU memory split — fractions are of TOTAL GPU memory, not free memory.
|
||||||
|
# When two vLLM services share a GPU, each profiler needs its own slice to
|
||||||
|
# fit both the model and KV cache, so small values cause the second-to-start
|
||||||
|
# service to OOM on KV cache allocation. 0.40 + 0.40 leaves ~20% headroom
|
||||||
|
# and is comfortably above the minimum for two 0.6B Qwen3 models at 8k ctx.
|
||||||
|
EMBED_GPU_MEM_UTIL=0.40
|
||||||
|
RERANK_GPU_MEM_UTIL=0.40
|
||||||
|
|
||||||
|
# Context length caps — lower these if VRAM is tight.
|
||||||
|
# Qwen3-Embedding supports up to 32k; reranker up to 32k.
|
||||||
|
EMBED_MAX_MODEL_LEN=8192
|
||||||
|
RERANK_MAX_MODEL_LEN=8192
|
||||||
|
|
||||||
|
# Optional API key — leave blank for no auth (fine on the internal network).
|
||||||
|
# If set, both services require `Authorization: Bearer <key>`.
|
||||||
|
API_KEY=
|
||||||
|
|
||||||
|
# HuggingFace token — only needed for gated models
|
||||||
|
HF_TOKEN=
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# vllm-qwen3
|
||||||
|
|
||||||
|
Qwen3 embedding + reranker served via vLLM. Replaces the unmaintained Infinity stack.
|
||||||
|
|
||||||
|
**Server:** ana-ml2
|
||||||
|
**Ports:** `8001` (embed), `8002` (rerank) — both configurable via `.env`
|
||||||
|
**GPU:** both services share GPU 1 by default (configurable)
|
||||||
|
|
||||||
|
## Why two services
|
||||||
|
|
||||||
|
vLLM runs **one model per process**, so embedding and reranking each get their own container. Both pin to the same GPU and split VRAM via `--gpu-memory-utilization`. Both use `--runner pooling` so the OpenAI server exposes `/v1/embeddings` (for the embedder) and `/rerank`, `/score` (for the reranker, which also needs the `--hf-overrides` described below).
|
||||||
|
|
||||||
|
## Reranker caveat
|
||||||
|
|
||||||
|
Qwen/Qwen3-Reranker-0.6B is a causal-LM checkpoint. The `--hf-overrides` flag in `compose.yaml` re-maps it to `Qwen3ForSequenceClassification` so vLLM's `/rerank` and `/score` endpoints work and the model emits only `no`/`yes` class logits instead of the full 151k-token distribution.
|
||||||
|
|
||||||
|
If that override breaks after a vLLM upgrade, the pre-converted checkpoint `tomaarsen/Qwen3-Reranker-0.6B-seq-cls` is a drop-in replacement that needs no overrides — set `RERANK_MODEL=tomaarsen/Qwen3-Reranker-0.6B-seq-cls` in `.env` and remove the `--hf-overrides` line from the compose.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On ana-ml2:
|
||||||
|
sudo mkdir -p /opt/docker/compose/vllm-qwen3
|
||||||
|
sudo chown $USER /opt/docker/compose/vllm-qwen3
|
||||||
|
cd /opt/docker/compose/vllm-qwen3
|
||||||
|
|
||||||
|
# Copy compose.yaml + .env.example here (e.g. via scp from this workspace)
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env — pick GPU, ports, memory split, etc.
|
||||||
|
|
||||||
|
# Pre-download models (optional, speeds first boot)
|
||||||
|
HF_HOME=/tank/aimodels/huggingface hf download "$(grep ^EMBED_MODEL .env | cut -d= -f2)"
|
||||||
|
HF_HOME=/tank/aimodels/huggingface hf download "$(grep ^RERANK_MODEL .env | cut -d= -f2)"
|
||||||
|
|
||||||
|
# Dry-parse
|
||||||
|
docker compose config
|
||||||
|
|
||||||
|
# Launch
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
First boot compiles CUDA graphs and can take 2–3 minutes per service. The `start_period: 180s` healthcheck grace reflects that.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health
|
||||||
|
curl -s http://localhost:8001/health
|
||||||
|
curl -s http://localhost:8002/health
|
||||||
|
|
||||||
|
# Embedding (OpenAI-compatible)
|
||||||
|
curl -s http://localhost:8001/v1/embeddings \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"Qwen/Qwen3-Embedding-0.6B","input":["hello world"]}' | jq .
|
||||||
|
|
||||||
|
# Reranker
|
||||||
|
curl -s http://localhost:8002/rerank \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"Qwen/Qwen3-Reranker-0.6B","query":"what is a cat","documents":["cats are mammals","dogs bark"]}' | jq .
|
||||||
|
|
||||||
|
# Listed models
|
||||||
|
curl -s http://localhost:8001/v1/models | jq .
|
||||||
|
curl -s http://localhost:8002/v1/models | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scaling knobs
|
||||||
|
|
||||||
|
- **`EMBED_GPU_MEM_UTIL` / `RERANK_GPU_MEM_UTIL`** — fractions of **total** GPU VRAM each service reserves (not of free VRAM). Both services profile independently, so each slice must be large enough to fit that service's model + KV cache with no knowledge of the other. Setting them too low causes the second-to-start container to OOM on KV cache allocation with `Available KV cache memory: -X.XX GiB`. Default 0.40/0.40 (= 0.80 total) leaves ~20% GPU headroom and works cleanly for the 0.6B pair at 8k context; raise for 4B/8B variants or drop `max-model-len` if you need more room.
|
||||||
|
- **`EMBED_MAX_MODEL_LEN` / `RERANK_MAX_MODEL_LEN`** — lower to reduce KV-cache allocation if VRAM is tight. Qwen3 supports up to 32k natively.
|
||||||
|
- **Larger models** — Qwen3-Embedding/Reranker come in 0.6B / 4B / 8B. Swap `EMBED_MODEL` / `RERANK_MODEL` and bump the memory fractions accordingly.
|
||||||
|
- **Separate GPUs** — if contention hurts latency, split them: add a second `GPU_ID_RERANK` variable and point each service at its own device. (Requires a small compose edit; currently both share `${GPU_ID}`.)
|
||||||
|
|
||||||
|
## Migrating off Infinity
|
||||||
|
|
||||||
|
Once this stack is verified stable:
|
||||||
|
|
||||||
|
1. Stop the infinity stack (`docker compose down` under `/opt/docker/compose/infinity/`).
|
||||||
|
2. Update consumers (AIPA agents, LibreChat RAG) to point at `:8001` for embeddings and `:8002` for rerank.
|
||||||
|
3. Delete `stacks/infinity/` from this workspace.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# vLLM — Qwen3 Embedding + Reranker (one stack, two services).
|
||||||
|
#
|
||||||
|
# Replaces the unmaintained Infinity stack. vLLM runs one model per process,
|
||||||
|
# so this stack brings up two containers sharing a single GPU:
|
||||||
|
#
|
||||||
|
# vllm-embed — Qwen3-Embedding served as an OpenAI /v1/embeddings server
|
||||||
|
# vllm-rerank — Qwen3-Reranker served as a /rerank + /score server
|
||||||
|
#
|
||||||
|
# The reranker is a causal-LM checkpoint; --hf-overrides re-maps it to
|
||||||
|
# Qwen3ForSequenceClassification so vLLM's reranking endpoints work and the
|
||||||
|
# model only emits two class logits (no/yes) instead of the full 151k vocab.
|
||||||
|
#
|
||||||
|
# All tunables live in .env — edit that, not this file.
|
||||||
|
#
|
||||||
|
# Pre-download models to avoid first-run delay:
|
||||||
|
# HF_HOME=/tank/aimodels/huggingface hf download Qwen/Qwen3-Embedding-0.6B
|
||||||
|
# HF_HOME=/tank/aimodels/huggingface hf download Qwen/Qwen3-Reranker-0.6B
|
||||||
|
|
||||||
|
services:
|
||||||
|
vllm-embed:
|
||||||
|
image: vllm/vllm-openai:${VLLM_VERSION}
|
||||||
|
container_name: vllm-embed
|
||||||
|
restart: unless-stopped
|
||||||
|
ipc: host
|
||||||
|
ports:
|
||||||
|
- "${EMBED_PORT}:8000"
|
||||||
|
volumes:
|
||||||
|
- /tank/aimodels/huggingface:/hfcache
|
||||||
|
environment:
|
||||||
|
- HF_HOME=/hfcache
|
||||||
|
- HF_HUB_CACHE=/hfcache/hub
|
||||||
|
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-}
|
||||||
|
- VLLM_API_KEY=${API_KEY:-}
|
||||||
|
command:
|
||||||
|
- ${EMBED_MODEL}
|
||||||
|
- --served-model-name
|
||||||
|
- ${EMBED_MODEL}
|
||||||
|
- --runner
|
||||||
|
- pooling
|
||||||
|
- --host
|
||||||
|
- 0.0.0.0
|
||||||
|
- --port
|
||||||
|
- "8000"
|
||||||
|
- --gpu-memory-utilization
|
||||||
|
- ${EMBED_GPU_MEM_UTIL}
|
||||||
|
- --max-model-len
|
||||||
|
- ${EMBED_MAX_MODEL_LEN}
|
||||||
|
- --dtype
|
||||||
|
- auto
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
device_ids:
|
||||||
|
- "${GPU_ID}"
|
||||||
|
capabilities:
|
||||||
|
- gpu
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 180s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=AI Systems
|
||||||
|
- homepage.name=vLLM Embed (Qwen3)
|
||||||
|
- homepage.icon=mdi-vector-arrange-below
|
||||||
|
- homepage.description=Qwen3 Embedding via vLLM (ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.54:${EMBED_PORT}/docs
|
||||||
|
|
||||||
|
vllm-rerank:
|
||||||
|
image: vllm/vllm-openai:${VLLM_VERSION}
|
||||||
|
container_name: vllm-rerank
|
||||||
|
restart: unless-stopped
|
||||||
|
ipc: host
|
||||||
|
ports:
|
||||||
|
- "${RERANK_PORT}:8000"
|
||||||
|
volumes:
|
||||||
|
- /tank/aimodels/huggingface:/hfcache
|
||||||
|
environment:
|
||||||
|
- HF_HOME=/hfcache
|
||||||
|
- HF_HUB_CACHE=/hfcache/hub
|
||||||
|
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-}
|
||||||
|
- VLLM_API_KEY=${API_KEY:-}
|
||||||
|
command:
|
||||||
|
- ${RERANK_MODEL}
|
||||||
|
- --served-model-name
|
||||||
|
- ${RERANK_MODEL}
|
||||||
|
- --runner
|
||||||
|
- pooling
|
||||||
|
- --hf-overrides
|
||||||
|
- '{"architectures":["Qwen3ForSequenceClassification"],"classifier_from_token":["no","yes"],"is_original_qwen3_reranker":true}'
|
||||||
|
- --host
|
||||||
|
- 0.0.0.0
|
||||||
|
- --port
|
||||||
|
- "8000"
|
||||||
|
- --gpu-memory-utilization
|
||||||
|
- ${RERANK_GPU_MEM_UTIL}
|
||||||
|
- --max-model-len
|
||||||
|
- ${RERANK_MAX_MODEL_LEN}
|
||||||
|
- --dtype
|
||||||
|
- auto
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
device_ids:
|
||||||
|
- "${GPU_ID}"
|
||||||
|
capabilities:
|
||||||
|
- gpu
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 180s
|
||||||
|
networks:
|
||||||
|
- tnet
|
||||||
|
labels:
|
||||||
|
- homepage.group=AI Systems
|
||||||
|
- homepage.name=vLLM Rerank (Qwen3)
|
||||||
|
- homepage.icon=mdi-sort-variant
|
||||||
|
- homepage.description=Qwen3 Reranker via vLLM (ana-ml2)
|
||||||
|
- homepage.href=http://10.250.50.54:${RERANK_PORT}/docs
|
||||||
|
|
||||||
|
networks:
|
||||||
|
tnet:
|
||||||
|
name: traefik-net
|
||||||
|
external: true
|
||||||
Reference in New Issue
Block a user