docs: phase 4 packaging + deployment plan
This commit is contained in:
parent
b06668fcbb
commit
3ce2940094
1 changed files with 867 additions and 0 deletions
|
|
@ -0,0 +1,867 @@
|
|||
# Phase 4 — Packaging & Deployment
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Turn Phase 1–3 code into shippable artifacts: a single-file Burrito binary for the agent, a Mix release for the server, a Caddy reverse-proxy config template, systemd unit for the agent, and deployment docs (LXC for server, scp+systemd for agents).
|
||||
|
||||
**Architecture:** Two artifact pipelines.
|
||||
|
||||
1. **Agent**: Burrito wraps the OTP release in a single self-extracting binary so Proxmox hosts only need the file and a systemd unit — no Erlang install on the hosts. Because Burrito cross-compiles via Zig, we document a Docker-based build path so developers on any platform can produce Linux binaries reproducibly.
|
||||
|
||||
2. **Server**: Standard `mix release`. Runs inside an LXC container on a Proxmox host in the RZ. Caddy fronts it, terminates TLS, proxies HTTP + WebSockets to `127.0.0.1:4000`. Migrations run via a release eval command on boot.
|
||||
|
||||
No new tests — packaging is verified by actually building artifacts.
|
||||
|
||||
**Tech Stack:** `burrito` (single-binary packaging), `mix release`, Docker (optional, for cross-compile), Caddy (TLS + reverse proxy), systemd.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
agent/
|
||||
├── mix.exs modify: add burrito + releases
|
||||
├── rel/
|
||||
│ ├── proxmox-monitor-agent.service create (systemd unit)
|
||||
│ └── env.sh.eex create (runtime env)
|
||||
├── build/ (gitignored) Burrito output
|
||||
├── Dockerfile.build create (reproducible linux builds)
|
||||
└── docs/
|
||||
└── install.md create (per-host install steps)
|
||||
|
||||
server/
|
||||
├── mix.exs modify: release config
|
||||
├── rel/
|
||||
│ ├── env.sh.eex create if missing
|
||||
│ └── remote.vm.args.eex create if missing
|
||||
├── lib/server/release.ex modify: add migrate/rollback
|
||||
├── Dockerfile create (build+runtime, via phx.gen.release)
|
||||
└── docs/
|
||||
├── deploy-lxc.md create (setup LXC container)
|
||||
└── Caddyfile.example create (reverse-proxy template)
|
||||
|
||||
docs/
|
||||
└── deployment-overview.md create (who-builds-what, ports, flow)
|
||||
```
|
||||
|
||||
**Ignored in git:** `agent/build/`, `server/_build/`, anything the release tooling writes.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Agent — Burrito Dep + Mix Config
|
||||
|
||||
**Files:**
|
||||
- Modify: `agent/mix.exs`
|
||||
- Modify: `.gitignore`
|
||||
|
||||
- [ ] **Step 1: Add `:burrito` dep and release config**
|
||||
|
||||
Open `agent/mix.exs`. Replace the whole file with:
|
||||
|
||||
```elixir
|
||||
defmodule ProxmoxAgent.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "0.1.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :agent,
|
||||
version: @version,
|
||||
elixir: "~> 1.17",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
releases: releases()
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger, :crypto],
|
||||
mod: {ProxmoxAgent.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:slipstream, "~> 1.1"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:toml, "~> 0.7"},
|
||||
{:burrito, "~> 1.3"}
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
defp releases do
|
||||
[
|
||||
agent: [
|
||||
steps: [:assemble, &Burrito.wrap/1],
|
||||
burrito: [
|
||||
targets: [
|
||||
linux_amd64: [os: :linux, cpu: :x86_64],
|
||||
linux_arm64: [os: :linux, cpu: :aarch64],
|
||||
macos: [os: :darwin, cpu: :aarch64]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Rationale for the three targets: `linux_amd64` is the canonical Proxmox deploy; `linux_arm64` is there so a future Raspberry-Pi-class host works; `macos` exists only to let developers smoke-test the binary locally.
|
||||
|
||||
- [ ] **Step 2: Extend `.gitignore`**
|
||||
|
||||
Open `/Users/cabele/claudeprojects/proxmox_monitor/.gitignore` and append:
|
||||
|
||||
```
|
||||
# Burrito build output
|
||||
/agent/build/
|
||||
/agent/burrito_out/
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Fetch deps and confirm compile**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor/agent
|
||||
mix deps.get 2>&1 | tail -5
|
||||
mix compile --warnings-as-errors 2>&1 | tail -3
|
||||
```
|
||||
|
||||
Expected: burrito fetched (plus its deps `typed_struct` and similar). Compile succeeds. **Do not run `mix release` yet** — it requires Zig to be installed and will be covered in Task 7.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor
|
||||
git add agent/mix.exs agent/mix.lock .gitignore
|
||||
git commit -m "feat(agent): burrito dep + release config for linux_amd64/arm64 + macos"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Agent — systemd Unit + `env.sh.eex`
|
||||
|
||||
**Files:**
|
||||
- Create: `agent/rel/proxmox-monitor-agent.service`
|
||||
- Create: `agent/rel/env.sh.eex`
|
||||
|
||||
- [ ] **Step 1: systemd unit**
|
||||
|
||||
Create `agent/rel/proxmox-monitor-agent.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Proxmox Monitor Agent
|
||||
Documentation=https://github.com/you/proxmox_monitor
|
||||
After=network-online.target zfs.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=AGENT_CONFIG=/etc/proxmox-monitor/agent.toml
|
||||
ExecStart=/usr/local/bin/proxmox-monitor-agent start
|
||||
ExecStop=/usr/local/bin/proxmox-monitor-agent stop
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# Burrito unpacks into this directory; keep it stable across runs
|
||||
Environment=BURRITO_CACHE_DIR=/var/cache/proxmox-monitor-agent
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Rationale:
|
||||
- Runs as **root** — required by `zpool status` against degraded pools.
|
||||
- `After=zfs.target` so ZFS is ready before the agent tries to read it.
|
||||
- `BURRITO_CACHE_DIR` pinned so each restart reuses the unpacked release, avoiding repeated extraction to `/tmp`.
|
||||
- `Restart=always` with 5s backoff — short network blips shouldn't need admin attention.
|
||||
|
||||
- [ ] **Step 2: release env.sh.eex**
|
||||
|
||||
Create `agent/rel/env.sh.eex`:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# Default-logs to journald (stdout) when running under systemd
|
||||
export RELEASE_COOKIE="${RELEASE_COOKIE:-$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')}"
|
||||
```
|
||||
|
||||
Rationale: Cookie is only relevant for distribution, but the release runtime wants one set. Generating a per-boot random cookie is fine for a single-node runtime.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add agent/rel
|
||||
git commit -m "feat(agent): systemd unit + release env.sh for root+journald install"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Agent — Docker-based Cross-Compile
|
||||
|
||||
**Files:**
|
||||
- Create: `agent/Dockerfile.build`
|
||||
- Create: `agent/scripts/build-linux.sh`
|
||||
|
||||
Burrito's Zig toolchain is painful to install. A Debian-based Docker image produces reproducible Linux artifacts without touching the developer's machine.
|
||||
|
||||
- [ ] **Step 1: Build Dockerfile**
|
||||
|
||||
Create `agent/Dockerfile.build`:
|
||||
|
||||
```dockerfile
|
||||
# Reproducible Burrito build environment for the agent.
|
||||
# Produces linux_amd64 + linux_arm64 binaries into /work/agent/burrito_out.
|
||||
# Keep elixir/OTP in sync with what the project compiles against locally
|
||||
# (see `elixir --version` — currently Elixir 1.19 on OTP 28).
|
||||
FROM elixir:1.19-otp-28 AS build
|
||||
|
||||
ENV MIX_ENV=prod DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential git ca-certificates curl xz-utils unzip 7zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Zig (Burrito needs it for cross-compile)
|
||||
ARG ZIG_VERSION=0.13.0
|
||||
RUN curl -fsSL https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz \
|
||||
| tar -xJ -C /opt && ln -s /opt/zig-linux-*/zig /usr/local/bin/zig
|
||||
|
||||
WORKDIR /work/agent
|
||||
RUN mix local.hex --force && mix local.rebar --force
|
||||
|
||||
# Copy sources last for layer caching
|
||||
COPY mix.exs mix.lock ./
|
||||
RUN mix deps.get --only prod
|
||||
|
||||
COPY lib lib
|
||||
COPY config config
|
||||
|
||||
RUN mix deps.compile
|
||||
RUN mix release
|
||||
|
||||
# Default: print the produced artifacts
|
||||
CMD ["sh", "-c", "ls -la burrito_out/"]
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Image is single-stage; the goal is to produce binaries, not to be a runtime.
|
||||
- Zig version pinned — Burrito 1.x is sensitive to Zig major changes.
|
||||
|
||||
- [ ] **Step 2: Build helper script**
|
||||
|
||||
Create `agent/scripts/build-linux.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Produce Linux Burrito binaries for the agent.
|
||||
# Usage: ./scripts/build-linux.sh [output_dir]
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT="${1:-$(pwd)/dist}"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
IMG="proxmox-monitor-agent-build:latest"
|
||||
|
||||
docker build -f Dockerfile.build -t "$IMG" .
|
||||
docker run --rm -v "$OUT":/out "$IMG" sh -c 'cp -v burrito_out/* /out/'
|
||||
|
||||
echo
|
||||
echo "Binaries written to $OUT:"
|
||||
ls -la "$OUT"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Make it executable**
|
||||
|
||||
```bash
|
||||
chmod +x /Users/cabele/claudeprojects/proxmox_monitor/agent/scripts/build-linux.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor
|
||||
git add agent/Dockerfile.build agent/scripts/build-linux.sh
|
||||
git commit -m "feat(agent): docker-based cross-compile for linux binaries"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Server — `mix phx.gen.release`
|
||||
|
||||
**Files:**
|
||||
- Generated by the command: `server/lib/server/release.ex` (modify), `server/Dockerfile`, `server/rel/env.sh.eex`, `server/rel/remote.vm.args.eex`
|
||||
|
||||
- [ ] **Step 1: Run the generator**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor/server
|
||||
mix phx.gen.release
|
||||
```
|
||||
|
||||
Expected prompt: "A release was generated. Would you like to overwrite lib/server/release.ex?" — **answer N** to preserve our existing `register_host/1` helper. The generator will create `Dockerfile`, `rel/env.sh.eex`, `rel/remote.vm.args.eex`, and `bin/server` scripts if absent.
|
||||
|
||||
- [ ] **Step 2: Extend `Server.Release` with migrate/rollback**
|
||||
|
||||
Our `Server.Release` currently only defines `register_host/1`. Open `server/lib/server/release.ex` and replace the module body with:
|
||||
|
||||
```elixir
|
||||
defmodule Server.Release do
|
||||
@moduledoc "Convenience functions for IEx and release-stage admin tasks."
|
||||
|
||||
@app :server
|
||||
|
||||
@doc "Create a host and print the plaintext token once."
|
||||
def register_host(name) do
|
||||
load_app!()
|
||||
|
||||
case Server.Hosts.create_host(name) do
|
||||
{:ok, {host, token}} ->
|
||||
IO.puts("Host '#{host.name}' registered (id=#{host.id}).")
|
||||
IO.puts("TOKEN: #{token}")
|
||||
IO.puts("Store this token NOW — it will never be shown again.")
|
||||
{:ok, host, token}
|
||||
|
||||
{:error, cs} ->
|
||||
IO.puts("Failed to register host: #{inspect(cs.errors)}")
|
||||
{:error, cs}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Run pending migrations. Invoke via: bin/server eval 'Server.Release.migrate()'"
|
||||
def migrate do
|
||||
load_app!()
|
||||
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Roll back one step per repo."
|
||||
def rollback(repo, version) do
|
||||
load_app!()
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||
end
|
||||
|
||||
defp repos, do: Application.fetch_env!(@app, :ecto_repos)
|
||||
|
||||
defp load_app! do
|
||||
Application.load(@app)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Inspect generated env.sh.eex**
|
||||
|
||||
Open the generated `server/rel/env.sh.eex`. The default is fine for LXC; add a one-liner to be explicit about the cookie source (prevents accidental default):
|
||||
|
||||
```sh
|
||||
# Inside server/rel/env.sh.eex, near the top:
|
||||
if [ -z "${RELEASE_COOKIE:-}" ]; then
|
||||
# For single-node deployments we don't need a stable cookie across
|
||||
# releases; refresh it per-boot so a stale cookie file cannot be
|
||||
# used to attach a remote shell.
|
||||
export RELEASE_COOKIE="$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
fi
|
||||
```
|
||||
|
||||
If the generator already wrote a similar block, leave it alone.
|
||||
|
||||
- [ ] **Step 4: Compile + release dry-run**
|
||||
|
||||
```bash
|
||||
mix compile 2>&1 | tail -3
|
||||
MIX_ENV=prod DASHBOARD_PASSWORD_HASH='placeholder' mix release --overwrite 2>&1 | tail -10
|
||||
```
|
||||
|
||||
Expected: `Release created at _build/prod/rel/server`. The placeholder hash is only to satisfy `runtime.exs` during build; real deploys set a proper one before `start`.
|
||||
|
||||
- [ ] **Step 5: Smoke-test the release binary locally**
|
||||
|
||||
```bash
|
||||
DATABASE_PATH=/tmp/proxmox_monitor_release.db \
|
||||
PHX_SERVER=true \
|
||||
SECRET_KEY_BASE="$(mix phx.gen.secret)" \
|
||||
DASHBOARD_PASSWORD_HASH="$(mix run -e 'IO.puts(Argon2.hash_pwd_salt("devpass"))' 2>&1 | tail -1)" \
|
||||
_build/prod/rel/server/bin/server eval 'Server.Release.migrate()'
|
||||
```
|
||||
|
||||
Expected: migrations run, exits 0. **Do not start the full server** — we only verify the release builds and `eval` works.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor
|
||||
git add server/lib/server/release.ex server/Dockerfile server/rel
|
||||
git commit -m "feat(server): phoenix release with migrate/rollback helpers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Caddyfile Template
|
||||
|
||||
**Files:**
|
||||
- Create: `server/docs/Caddyfile.example`
|
||||
|
||||
- [ ] **Step 1: Write template**
|
||||
|
||||
Create `server/docs/Caddyfile.example`:
|
||||
|
||||
```caddyfile
|
||||
# /etc/caddy/Caddyfile — Proxmox Monitor reverse-proxy
|
||||
#
|
||||
# Replace monitor.example.com with your actual hostname.
|
||||
# Caddy handles Let's Encrypt automatically when the domain's A record
|
||||
# points at this host.
|
||||
|
||||
monitor.example.com {
|
||||
# Security headers
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
# The Phoenix endpoint handles both HTTP requests and WebSocket upgrades
|
||||
# on the same port; Caddy's reverse_proxy transparently upgrades /socket.
|
||||
reverse_proxy 127.0.0.1:4000 {
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
# Keep WebSocket connections open long enough for the Phoenix heartbeat
|
||||
# cycle (30s by default).
|
||||
transport http {
|
||||
read_timeout 90s
|
||||
dial_timeout 10s
|
||||
}
|
||||
}
|
||||
|
||||
# Basic access log
|
||||
log {
|
||||
output file /var/log/caddy/monitor.log {
|
||||
roll_size 10mb
|
||||
roll_keep 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor
|
||||
git add server/docs/Caddyfile.example
|
||||
git commit -m "docs(server): Caddyfile template with TLS + WSS reverse-proxy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Deployment Docs
|
||||
|
||||
**Files:**
|
||||
- Create: `server/docs/deploy-lxc.md`
|
||||
- Create: `agent/docs/install.md`
|
||||
- Create: `docs/deployment-overview.md`
|
||||
|
||||
- [ ] **Step 1: Deployment overview**
|
||||
|
||||
Create `docs/deployment-overview.md`:
|
||||
|
||||
```markdown
|
||||
# Deployment Overview
|
||||
|
||||
Two artifacts, built independently, deployed independently.
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Server (LXC in RZ) │
|
||||
agents ──WSS─>│ - Phoenix release │
|
||||
│ - SQLite │
|
||||
│ - Caddy (TLS) │
|
||||
└─────────────────────────┘
|
||||
▲
|
||||
│ ssh
|
||||
│
|
||||
┌─────────────────────────┐
|
||||
│ Operator workstation │
|
||||
│ - Builds server release│
|
||||
│ - Builds agent binary │
|
||||
└─────────────────────────┘
|
||||
│ scp
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Proxmox host (any of N) │
|
||||
│ - Burrito agent binary │
|
||||
│ - systemd unit │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## What runs where
|
||||
|
||||
| Component | Host | Port / Path |
|
||||
|-----------|------|------------------------------------------|
|
||||
| Caddy | Server LXC | 443 public, forwards → 127.0.0.1:4000 |
|
||||
| Phoenix | Server LXC | 127.0.0.1:4000 (HTTP + WS) |
|
||||
| SQLite | Server LXC | file at $DATABASE_PATH |
|
||||
| Agent | Proxmox host | no listening ports |
|
||||
|
||||
## Secrets the operator must provide
|
||||
|
||||
| Variable | Where | How to generate |
|
||||
|---------------------------|------------|-------------------------------------------------|
|
||||
| `SECRET_KEY_BASE` | Server env | `mix phx.gen.secret` |
|
||||
| `DASHBOARD_PASSWORD_HASH` | Server env | `mix run -e 'IO.puts(Argon2.hash_pwd_salt("..."))'` |
|
||||
| Agent token | Server DB | Admin UI → "Add host" reveals it once |
|
||||
|
||||
## Build flow
|
||||
|
||||
1. `cd server && MIX_ENV=prod mix release` → produces `_build/prod/rel/server/`
|
||||
2. `cd agent && ./scripts/build-linux.sh` → produces `dist/proxmox-monitor-agent_linux_amd64`
|
||||
|
||||
See `server/docs/deploy-lxc.md` and `agent/docs/install.md` for step-by-step.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: LXC server deploy**
|
||||
|
||||
Create `server/docs/deploy-lxc.md`:
|
||||
|
||||
````markdown
|
||||
# Server Deployment (LXC + Caddy)
|
||||
|
||||
Target: a Proxmox LXC container running Debian 12 in the RZ, publicly reachable
|
||||
on port 443 via Caddy. ~1 GB RAM, 2 cores, 10 GB disk covers >20 agents.
|
||||
|
||||
## 1. Create the LXC (on the hypervisor)
|
||||
|
||||
```bash
|
||||
pct create 200 \
|
||||
/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst \
|
||||
--hostname proxmox-monitor \
|
||||
--memory 1024 --cores 2 \
|
||||
--rootfs local-zfs:10 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
|
||||
--unprivileged 1 --features nesting=0 --onboot 1
|
||||
pct start 200
|
||||
pct enter 200
|
||||
```
|
||||
|
||||
## 2. Inside the LXC: base packages
|
||||
|
||||
```bash
|
||||
apt-get update && apt-get install -y \
|
||||
ca-certificates curl debian-keyring debian-archive-keyring apt-transport-https
|
||||
# Caddy's apt repo
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
|
||||
gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||
> /etc/apt/sources.list.d/caddy-stable.list
|
||||
apt-get update && apt-get install -y caddy sqlite3
|
||||
```
|
||||
|
||||
## 3. Upload the release
|
||||
|
||||
From the operator workstation:
|
||||
|
||||
```bash
|
||||
cd proxmox_monitor/server
|
||||
MIX_ENV=prod mix release --overwrite
|
||||
tar -czf server_release.tgz -C _build/prod/rel server
|
||||
scp server_release.tgz root@<LXC-IP>:/tmp/
|
||||
```
|
||||
|
||||
Back in the LXC:
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/proxmox-monitor
|
||||
tar -xzf /tmp/server_release.tgz -C /opt/proxmox-monitor
|
||||
```
|
||||
|
||||
## 4. Directories & env file
|
||||
|
||||
```bash
|
||||
install -d -m 0700 /var/lib/proxmox-monitor
|
||||
cat > /etc/default/proxmox-monitor <<EOF
|
||||
DATABASE_PATH=/var/lib/proxmox-monitor/monitor.db
|
||||
SECRET_KEY_BASE=$(/opt/proxmox-monitor/server/bin/server eval 'IO.puts(64 |> :crypto.strong_rand_bytes() |> Base.encode64())' 2>/dev/null | tail -1)
|
||||
DASHBOARD_PASSWORD_HASH='<paste from: mix run -e "IO.puts(Argon2.hash_pwd_salt(\"your-password\"))">'
|
||||
PHX_SERVER=true
|
||||
PHX_HOST=monitor.example.com
|
||||
PORT=4000
|
||||
EOF
|
||||
chmod 0600 /etc/default/proxmox-monitor
|
||||
```
|
||||
|
||||
## 5. systemd unit
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/proxmox-monitor.service
|
||||
[Unit]
|
||||
Description=Proxmox Monitor Server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
EnvironmentFile=/etc/default/proxmox-monitor
|
||||
ExecStartPre=/opt/proxmox-monitor/server/bin/server eval 'Server.Release.migrate()'
|
||||
ExecStart=/opt/proxmox-monitor/server/bin/server start
|
||||
ExecStop=/opt/proxmox-monitor/server/bin/server stop
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now proxmox-monitor
|
||||
journalctl -u proxmox-monitor -f # verify it listens on 4000
|
||||
```
|
||||
|
||||
## 6. Caddy
|
||||
|
||||
```bash
|
||||
install -m 0644 /opt/proxmox-monitor/server/lib/server-0.1.0/priv/docs/Caddyfile.example /etc/caddy/Caddyfile
|
||||
# Edit monitor.example.com to match your real DNS.
|
||||
nano /etc/caddy/Caddyfile
|
||||
systemctl reload caddy
|
||||
```
|
||||
|
||||
(If Caddy isn't the one in this LXC, copy the template to wherever Caddy lives.)
|
||||
|
||||
## 7. Create the first host
|
||||
|
||||
```bash
|
||||
/opt/proxmox-monitor/server/bin/server rpc 'Server.Release.register_host("pve-host-01")'
|
||||
```
|
||||
|
||||
Copy the printed TOKEN — you'll paste it into the agent config.
|
||||
|
||||
## 8. Upgrade flow
|
||||
|
||||
```bash
|
||||
# operator
|
||||
cd server && MIX_ENV=prod mix release --overwrite
|
||||
scp _build/prod/rel/server.tar.gz root@<LXC>:/tmp/server_release.tgz
|
||||
|
||||
# LXC
|
||||
systemctl stop proxmox-monitor
|
||||
tar -xzf /tmp/server_release.tgz -C /opt/proxmox-monitor --overwrite
|
||||
systemctl start proxmox-monitor # ExecStartPre runs migrate automatically
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 3: Agent install**
|
||||
|
||||
Create `agent/docs/install.md`:
|
||||
|
||||
````markdown
|
||||
# Agent Install (per Proxmox host)
|
||||
|
||||
## Prerequisites on the Proxmox host
|
||||
|
||||
- Proxmox VE 8.3+ (OpenZFS 2.3+ for the `-j` flags on `zpool`/`zfs`)
|
||||
- Root SSH access
|
||||
- Outbound HTTPS to the monitor server
|
||||
|
||||
No Erlang or Elixir needed — the Burrito binary ships its own runtime.
|
||||
|
||||
## 1. Build the binary (operator workstation)
|
||||
|
||||
```bash
|
||||
cd proxmox_monitor/agent
|
||||
./scripts/build-linux.sh # requires Docker
|
||||
ls dist/
|
||||
# proxmox-monitor-agent_linux_amd64
|
||||
# proxmox-monitor-agent_linux_arm64
|
||||
```
|
||||
|
||||
## 2. Register the host in the dashboard
|
||||
|
||||
From the dashboard at `https://monitor.example.com/admin/hosts`:
|
||||
|
||||
1. "Register a new host" → enter the short name (e.g. `pve-host-01`).
|
||||
2. Copy the one-time token shown.
|
||||
|
||||
## 3. Copy files to the Proxmox host
|
||||
|
||||
```bash
|
||||
HOST=pve-host-01
|
||||
scp dist/proxmox-monitor-agent_linux_amd64 \
|
||||
root@$HOST:/usr/local/bin/proxmox-monitor-agent
|
||||
ssh root@$HOST 'chmod 0755 /usr/local/bin/proxmox-monitor-agent'
|
||||
|
||||
# systemd unit (included in the repo)
|
||||
scp rel/proxmox-monitor-agent.service \
|
||||
root@$HOST:/etc/systemd/system/
|
||||
```
|
||||
|
||||
## 4. Write the config
|
||||
|
||||
On the Proxmox host:
|
||||
|
||||
```bash
|
||||
install -d -m 0700 /etc/proxmox-monitor
|
||||
cat > /etc/proxmox-monitor/agent.toml <<EOF
|
||||
server_url = "wss://monitor.example.com/socket/websocket"
|
||||
token = "<paste-token-from-dashboard>"
|
||||
host_id = "pve-host-01"
|
||||
|
||||
[intervals]
|
||||
fast_seconds = 30
|
||||
medium_seconds = 300
|
||||
slow_seconds = 1800
|
||||
EOF
|
||||
chmod 0600 /etc/proxmox-monitor/agent.toml
|
||||
```
|
||||
|
||||
## 5. Enable the service
|
||||
|
||||
```bash
|
||||
install -d -m 0700 /var/cache/proxmox-monitor-agent
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now proxmox-monitor-agent
|
||||
journalctl -u proxmox-monitor-agent -f
|
||||
```
|
||||
|
||||
Expected within ~10s:
|
||||
```
|
||||
agent: starting with host_id=pve-host-01
|
||||
reporter: connected, joining host:pve-host-01
|
||||
reporter: joined host:pve-host-01
|
||||
```
|
||||
|
||||
The host's card on the dashboard should flip to `online`.
|
||||
|
||||
## 6. Token rotation
|
||||
|
||||
If a token leaks: dashboard → Admin → "Rotate". Copy the new token, update
|
||||
`/etc/proxmox-monitor/agent.toml` on the affected host, `systemctl restart
|
||||
proxmox-monitor-agent`. Old token is invalidated immediately.
|
||||
|
||||
## 7. Upgrade flow
|
||||
|
||||
```bash
|
||||
# operator
|
||||
./scripts/build-linux.sh
|
||||
scp dist/proxmox-monitor-agent_linux_amd64 root@$HOST:/usr/local/bin/proxmox-monitor-agent.new
|
||||
|
||||
# on the host
|
||||
mv /usr/local/bin/proxmox-monitor-agent{.new,}
|
||||
systemctl restart proxmox-monitor-agent
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|------------------------------------------|-----------------------------------------------------------------|
|
||||
| `enoent` errors for `zpool`/`pvesh` | You're not on a Proxmox host, or binaries aren't in `$PATH`. |
|
||||
| `handshake_failed: :nxdomain` | DNS for the monitor hostname fails from this host. |
|
||||
| `unknown_host` rejection on join | Host name in `agent.toml` doesn't match the dashboard entry. |
|
||||
| `invalid_token` rejection | Token was rotated; paste the new one. |
|
||||
| Agent reconnects every 30s | Server's WebSocket timeout hit — check Caddy `read_timeout 90s`.|
|
||||
````
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor
|
||||
git add docs/deployment-overview.md server/docs/deploy-lxc.md agent/docs/install.md
|
||||
git commit -m "docs: deployment overview + LXC server deploy + per-host agent install"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Build Verification
|
||||
|
||||
This task runs the actual build tooling to prove the artifacts produce.
|
||||
|
||||
- [ ] **Step 1: Check Docker is available for the agent build**
|
||||
|
||||
```bash
|
||||
docker --version 2>&1 | head -1
|
||||
```
|
||||
|
||||
- If Docker is **present**: proceed to Step 2.
|
||||
- If Docker is **absent** or non-functional: **skip Step 2** but verify Burrito is wired in by compiling:
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor/agent
|
||||
mix compile --warnings-as-errors 2>&1 | tail -3
|
||||
```
|
||||
|
||||
Expected: clean compile. Real Linux binaries will be built on a host that has Docker.
|
||||
|
||||
- [ ] **Step 2 (only if Docker available): Produce Linux agent binaries**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor/agent
|
||||
./scripts/build-linux.sh
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
Expected: `dist/proxmox-monitor-agent_linux_amd64` and `..._linux_arm64`, each an ELF executable, roughly 30-60 MB.
|
||||
|
||||
- [ ] **Step 3: Build the server release**
|
||||
|
||||
```bash
|
||||
cd /Users/cabele/claudeprojects/proxmox_monitor/server
|
||||
MIX_ENV=prod DASHBOARD_PASSWORD_HASH='placeholder' mix release --overwrite 2>&1 | tail -10
|
||||
ls _build/prod/rel/server/bin/
|
||||
```
|
||||
|
||||
Expected: `_build/prod/rel/server/bin/server` exists, plus `migrate` (if generator created one).
|
||||
|
||||
- [ ] **Step 4: Release migration smoke test**
|
||||
|
||||
```bash
|
||||
TMPDB=/tmp/phase4_migrate.db
|
||||
rm -f "$TMPDB"
|
||||
DATABASE_PATH="$TMPDB" \
|
||||
PHX_SERVER=false \
|
||||
SECRET_KEY_BASE="$(cd /Users/cabele/claudeprojects/proxmox_monitor/server && mix phx.gen.secret 2>&1 | tail -1)" \
|
||||
DASHBOARD_PASSWORD_HASH='placeholder' \
|
||||
/Users/cabele/claudeprojects/proxmox_monitor/server/_build/prod/rel/server/bin/server eval 'Server.Release.migrate()'
|
||||
```
|
||||
|
||||
Expected: output shows migrations applied. `$TMPDB` now contains a `hosts` and `metrics` table.
|
||||
|
||||
```bash
|
||||
sqlite3 /tmp/phase4_migrate.db '.schema' | head -20
|
||||
rm -f /tmp/phase4_migrate.db
|
||||
```
|
||||
|
||||
Expected: CREATE TABLE statements for `hosts` and `metrics`.
|
||||
|
||||
- [ ] **Step 5: Rollup**
|
||||
|
||||
No commit — this task is pure verification.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 Exit Criteria
|
||||
|
||||
- `agent/mix.exs` defines Burrito release targets and `mix compile` stays clean.
|
||||
- `agent/Dockerfile.build` + `scripts/build-linux.sh` reproducibly produce Linux binaries (verified when Docker is available).
|
||||
- `server/_build/prod/rel/server/bin/server` exists after `mix release` and `server eval 'Server.Release.migrate()'` creates a schema in a fresh SQLite file.
|
||||
- systemd units for both agent and server live in the repo.
|
||||
- `Caddyfile.example` covers TLS + WSS reverse-proxy.
|
||||
- `docs/deployment-overview.md`, `server/docs/deploy-lxc.md`, `agent/docs/install.md` walk from zero to "agent reports metrics to dashboard" in a sequence an engineer can follow without asking questions.
|
||||
|
||||
**Deferred (not in MVP):**
|
||||
- CI pipeline to build and publish artifacts (manual `scp` for now — fine at N≤20 hosts per the concept).
|
||||
- Agent self-update (concept calls this out as YAGNI).
|
||||
- Docker image for the server (we use bare LXC + release, which has lower overhead).
|
||||
Loading…
Add table
Add a link
Reference in a new issue