TazPod: Proxmox LXC Runtime (CT106) Detail
Level 3 (Detail) — the tazpod-proxmox container: provisioning IaC, installed stack, and the operator surfaces it hosts.
Concept
CT106 (tazpod-proxmox, 192.168.1.206) is the Proxmox LXC incarnation of TazPod: the same operator tooling as the Docker flavour, but as an unprivileged container on the tazlab node with the pet volume local-lvm:vm-999-disk-2 mounted at /workspace. The AI agents run here — this container is the operator environment.
The DeepSeek Harness (dsh) Web UI it used to host was decommissioned on 2026-09-25 (see below). Paperclip ran here from 2026-09-25 to 2026-09-26, moved to CT108 and was removed on 2026-10-19 with its decommission. Multica ran here from 2026-09-27 to 2026-10-20 (multica-daemon.service, layers layer6-multica/layer7-multica-team, the Git-declared team) and was decommissioned on 2026-10-20. What CT106 hosts now is the operator environment and its tooling.
Provisioning (tazpod/runtimes/lxc)
| Piece | Notes |
|---|---|
terraform/ | CT definition: unprivileged, 2 cores, memory_mb/swap_mb/rootfs_size_gb variables, nesting=1 (keyctl is added by the raw phase), mp0 = pet volume → /workspace; Ubuntu 24.04 template |
terraform-pet/ | CT 999 (pet-storage) which owns the disk volume consumed as mp0 |
ansible/ | Role tazpod (layers below) |
create.sh | Orchestrator, see phase list |
Live resources (verified 2026-09-25): 2 cores, 8192 MB RAM / 1024 MB swap, 50 GB rootfs, features=keyctl=1,nesting=1, static 192.168.1.206/24 — note the RAM/disk are above the Terraform file’s defaults, so the container has been resized since provisioning.
create.sh phases: 0 pet ensure → 1 terraform → 2 raw LXC config (lxc.idmap uid-1000 passthrough, TUN device, cap drops, mp1 Samba bind) → 2b authorize admin keys (pct exec: desktop key installed for root and tazpod) → 3 wait SSH → 3b build the tazpod binary → 4 Ansible baseline → 5 transfer Tailscale OAuth secrets from gopass → 6 Tailscale up → 7 verify.
Where the run happens: create.sh must execute on the trusted orchestration host tazlab (192.168.1.200), not a workstation — the tazpod-provision key is pinned restrict,from="192.168.1.200" on this container (TAZ-12). See Trusted Orchestration Host for the decision, prerequisites and the TAZ-19 acceptance checks.
Ansible layers: layer0-system (packages, locale, user + admin key, sudoers, sshd), layer1-shell (Tailscale, Go, nvm/Node, starship, zoxide, hugo), layer2-aws, layer3-k8s, layer4-ai (pi, gemini-cli, opencode, bun, pi-autoresearch, omp, antigravity), layer5-paperclip (the Paperclip unit — disabled since the 2026-09-26 move — plus the pet-backed pi state directory), layer6-multica (the Multica daemon: binary, unit, env, token), layer7-multica-team (the Git-declared agent roster, reconciled onto the control plane), tazpod-deploy (dotfiles, KUBECONFIG, systemd units, workspace symlinks, legacy cleanup).
Idempotency: verified with ansible-core in a local venv against the live container — a clean second pass reports changed=0 failed=0. Rules learned the hard way:
- never use
/workspace/...as a controller path (that is the path inside the container): sources must be derived fromtazpod_workspace({{ playbook_dir }}/../../../..), which is valid both from a checkout and from inside TazPod; creates:must match the real binary name (herdr, nothdr; Tailscale installs to/usr/bin, not/usr/sbin);.sshmust stay a real directory — Ansible correctly refuses to convert a directory holding live keys into a symlink;authorized_keyis no longer in ansible-core ≥ 2.21 (moved to theansible.posixcollection): corefile+lineinfileare used instead, with no extra dependencies.
Re-run assessment (2026-09-25): a check-mode run against the live container (-i 'tazpod,' -c local --check --diff) reports ok=71 changed=2 failed=0 skipped=18. The only tasks that would change anything are the TazPod binary copy (same artifact already in the workspace) and the KUBECONFIG copy (diff: a trailing newline). Nothing upgrades packages, reboots, or rewrites /etc/resolv.conf.
The pi state directory is pet-backed (~/.pi)
pi keeps its state under ~/.pi: one session transcript per agent run in ~/.pi/paperclips/<timestamp>-<agentId>.jsonl (the most readable record of what an agent actually did) and the synced agent skills under ~/.pi/agent. pi runs as tazpod, whose home is on the container rootfs, so a create.sh rebuild would destroy both. They are therefore mirrored onto the pet volume — the same durability the Paperclip run logs already get from paperclip_home. Introduced by commit d7dd4ff (tazzo/tazpod master, “lxc(paperclip): keep the pi session transcripts on the pet volume”, refs TAZ-11).
| Path | Role |
|---|---|
/workspace (/dev/mapper/pve-vm--999--disk--2, ext4) | the pet volume (mp0, host local-lvm:vm-999-disk-2) |
/workspace/.paperclip/home/.pi | canonical pet-backed pi state directory (0700, owner tazpod) |
/workspace/.tazpod/.pi | older pet location, still populated on the current host and used as the seed |
/home/tazpod/.pi | account path, a symlink into the pet volume — today /workspace/.tazpod/.pi, after the next rebuild /workspace/.paperclip/home/.pi |
The mechanism is Ansible, not a manual step: runtimes/lxc/ansible/roles/tazpod/tasks/paperclip-pi-home.yml, imported at the end of layer5-paperclip.yml (tag pi-home) and run by create.sh. On a rebuild layer4-ai first installs the pi extensions into a real ~/.pi; the Paperclip task then:
- creates
/workspace/.paperclip/home/.pi(0700,tazpod); - seeds it from the older pet location
/workspace/.tazpod/.piwithcp -a -n(no clobber), so the transcripts already collected there are not orphaned; - copies any remaining rootfs
~/.picontent into it and removes the real directory; - replaces
~/.piwith a symlink to the pet copy.
The whole block is guarded on “not already a symlink”, so on the steady state — and on today’s host, where ~/.pi already points into the pet volume — it is a no-op and a --check re-run stays clean. tazpod-deploy.yml no longer symlinks .pi (its list entry and symlink loop item were removed), because that task raced layer4-ai’s install into a real ~/.pi.
Caveat — the interactive .bashrc is still a second writer. The dotfiles deployed to CT106 by tazpod-deploy.yml (tazpod/dotfiles/.bashrc, live at /home/tazpod/.bashrc) keep .pi in their AI-tool symlink loop and force it to /workspace/.tazpod/.pi whenever readlink ~/.pi differs from that target:
for _tool in .pi .omp .gemini .claude .antigravity .antigravitycli; do
_target="/workspace/.tazpod/$_tool"; _link="$HOME/$_tool"
if [ ! -L "$_link" ] || [ "$(readlink "$_link")" != "$_target" ]; then
rm -rf "$_link" && ln -sf "$_target" "$_link"
fi
done
.profile sources .bashrc, so the first interactive/login shell after the Ansible apply repoints ~/.pi back at the legacy path, and /workspace/.paperclip/home/.pi then stops receiving transcripts. The durability guarantee still holds — both locations are on the same pet volume — but the canonical target is not stable until .pi is removed from (or aligned with) that loop. Routed to the Platform Engineer, the tazpod single writer.
Verified live on 2026-09-26 (before the pending apply):
readlink -f /home/tazpod/.pi # → /workspace/.tazpod/.pi
findmnt -no SOURCE,TARGET,FSTYPE /workspace # → /dev/mapper/pve-vm--999--disk--2 /workspace ext4
ls -d /workspace/.paperclip/home # → No such file or directory (created on the next rebuild)
Removed: dsh Web UI (decommissioned 2026-09-25)
The DeepSeek Harness Web UI was retired because it is no longer used. What was removed:
| Removed | Detail |
|---|---|
| Ansible layer | layer5-dsh.yml, templates/dsh.service.j2, the restart dsh handler, the dsh_version/dsh_web_port defaults, the layer import, and the dsh checks in create.sh |
| systemd unit | /etc/systemd/system/dsh.service — stopped, disabled, deleted, daemon-reload |
| Package | @deepseek-ai/dsh@0.1.5-rc.1 from the tazpod user’s nvm prefix (npm rm -g, 520 packages removed) |
| Log | /var/log/dsh-web.log |
| Tooling | SKILLS/tools/dsh-web-access/ (tunnel helper, TOOL.md, research note) and its row in SKILLS/tools/SKILL.md |
~/.dsh/ (41 MB — profiles, settings.yaml, sessions, backups, the skills symlink) was deleted afterwards on explicit operator instruction. The container is clean: no unit, no package, no binary, nothing listening on 3081, and no ~/.dsh under /home/tazpod or /root.
Historical context worth keeping: dsh pinned privileged RPCs to the loopback origin, so remote access was always an SSH port forward (ssh -N -L 127.0.0.1:3081:…) — never a reverse proxy. That is why the container’s only exposed surface was loopback, and why the nginx reverse proxy from the earlier era was removed in 2026-09-10.
Legacy removed (2026-09-10, residue cleared 2026-09-14)
- nginx reverse proxy on
:3080(Host-header spoofing,crypto.randomUUIDpolyfill, local TLS certs) — stopped, disabled, vhost removed, package purged, client polyfill plugin deleted. Residue cleared 2026-09-14: the nginx-era/etc/dsh-tls/key material was deleted from CT106 and the now-unusedinfra/dsh-tls/*secrets were removed from the store (TD-065). - LAN
isLoopbackvendor patch in the dsh client bundle (… || pageLocation.hostname === "192.168.1.206") — reverted 2026-09-14 by reinstalling the clean package; verified in the source and in the bundle actually served to the browser. Two helper scripts from the same era were handled:~/.dsh/scripts/patch-remote-settings.shdeleted,~/.dsh/scripts/web-login-url.shrewritten to print the loopback tunnel URL instead of the dead:3080origin. tazpod-sync.service(S3 sync daemon from the pre-gopass era) — was restart-looping against a nonexistent binary (10,117 restarts); unit removed.
Verification
# the decommissioned surface must be absent
systemctl status dsh # → Unit dsh.service could not be found
command -v dsh # → nothing
ss -tln | grep 3081 # → nothing listens
See Also
- Parent hub: tazpod
- Provisioning host: Trusted Orchestration Host (TAZ-19) —
tazlabowns/root/.ssh/tazpod-provision, socreate.shruns there - Related: lxc-storage-infra (CT 107, different container), Hindsight
- CRISP:
paperclip-lxc-deployment— the application replacing the removed UI on this container