Sentinel
Fleet Machines
Hosts you onboarded by running the Sentinel agent, plus socket-only monitored servers
Containers
Workloads running on your fleet machines β grouped by the machine hosting them
Services
One-click, multi-container app deployments from a curated catalog
βΈ
Fleet network
cluster groundwork
Which machines can reach each other directly on their own network, bypassing the relay. A cluster needs this; a relayed tunnel is not enough.
βΈ
Kubernetes cluster
k3s
Runs a single-node cluster — control plane and workloads on one machine — capped to a share of that machine's CPU and memory.
Catalog
Running
Fleet Connectivity
Which machines were reachable over the last 24 hours. A gap means the agent stopped reporting, not that anything was done to it.
Activity
Who did what, to which machine, and whether it was allowed. Refusals are recorded alongside successes — a run of them is usually the more interesting signal.
Addresses are shown with the country their network is registered in — for corporate or VPN traffic that is where it reaches the internet, not where the person is. IP country data by DB-IP (CC BY 4.0).
Sentinel API Reference
Node, container, and service orchestration β everything below is the same API the dashboard itself calls.
Authentication
The browser session cookie (what the dashboard itself uses) or an API key on the x-api-key header. Generate an operator key from the panel.
curl https://applotus.fly.dev/api/sentinel/nodes \ -H "x-api-key: YOUR_KEY"
Three kinds of key, not one β the fleet-management endpoints below (dispatch, node deletion, credential lookups, command results, service deploy/remove) require an operator key or a signed-in session. A node or container key gets a 403 on all of them; it can only reach the five agent endpoints (heartbeat, result, download, mesh services) for the one machine it is bound to.
| Key type | Scope |
|---|---|
| Operator | Full workspace access. Minted from the API Keys panel above; up to 20 per workspace. |
| Node | Bound to one host. Minted automatically the first time an agent connects, or upfront as an enrollment key (unbound until the first machine claims it β see the install docs below) via POST /api/tenant/keys/enrollment. |
| Container | Bound to one container. Minted automatically when a container is deployed with its own agent. |
An agent authenticating with a bare workspace ID (the pre-key --tenant flag, no --api-key) is treated as unauthenticated everywhere except its own heartbeat β that one call also mints it a real node key automatically and hands it back, so an already-deployed agent upgrades itself onto a real credential with no reinstall.
Nodes
A node is a machine running the Sentinel agent (a "host") or a container the agent provisioned (a "container" node, linked to its host via parentNodeId).
Returns every node in the workspace: status, metrics, container runtime capability, desktop availability, and (for container nodes) image, ports, persistence, and lifecycle state.
Removes a node's record only β does not touch anything on the host. Use cleanup_node below to actually reclaim what the agent left running.
Container lifecycle
One endpoint for every agent-executed action. The agent picks the task up on its next heartbeat (or instantly, if its control socket is connected) and reports back asynchronously β see Results.
curl -X POST https://applotus.fly.dev/api/sentinel/agent/dispatch \
-H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" \
-d '{"nodeId":"node-xyz","type":"provision_container","image":"docker.io/library/redis:7-alpine","cpus":1,"memoryMb":512}'
| type | Notes |
|---|---|
| provision_container | Deploys a container. Fields: image, cpus, memoryMb, diskMb, ports[], env{}, persistent, withAgent, serviceAlias. Database images (mysql/postgres/mongo/mariadb) get a bootstrap password and, where verified, a collision-safe port assigned automatically. |
| remove_container | Fields: containerId. Stops and deletes it on the host. |
| extend_container_ttl | Fields: containerId, addSeconds (timed containers only, capped at 24h added at a time). |
| set_container_persistence | Fields: containerId, persistent, ttlSeconds?. Persistent = no expiry, restarted on exit, snapshotted on a schedule. |
| hibernate_container / wake_container | Fields: containerId. Stops a persistent container to free its CPU/memory without losing it (snapshotted first), then starts it back up. |
| backup_container | Fields: containerId. Manual filesystem snapshot (persistent containers also get these on a schedule). |
| get_container_env / update_container_env | Fields: containerId / containerId, env{}. Reads or replaces the deployment env vars (recreates the container for updates). |
| get_container_logs | Fields: containerId, tail? (default 200, max 500). Tails the runtime's own stdout/stderr log. |
| install_container_desktop | Fields: containerId. Installs a virtual desktop (Xvfb + XFCE + VNC) inside a running container. Needs the container's own agent and ≥1 CPU / ≥2GB memory. |
| cleanup_node | Fields: stopAgent?. Full teardown: every container, snapshot, and network the agent put on the node, optionally stopping the agent itself. |
Results
Every dispatched task is asynchronous β there's no "wait for it to finish" response. Poll this and match on the task.id the dispatch call returned against each result's taskId. A result's exit_code is 0 on success; stdout holds a JSON-encoded payload on success, stderr holds the error message on failure.
Services
A service is a named group of containers from a curated catalog (see the Services tab), deployed and torn down together on the tenant's shared mesh network. Every component is an ordinary provision_container dispatch under the hood β nothing here needs a different agent version.
Returns the template catalog plus every running instance, with live status (healthy / degraded / down) and per-component CPU/RAM derived from the same node records the Containers tab shows.
nodeId must have a usable container runtime. Returns the new instance immediately (components are still provisioning) with each component's mesh alias and, for database components, its generated credential.
Dispatches removal for every component container and drops the instance record.
Container web proxy
Reverse-proxies HTTP(S) into a port a container listens on β no in-container agent, no port published to the host or the internet, and it's what the "Open" link on a Containers/Services card actually points at. Root-relative asset paths and same-origin redirects are rewritten to stay inside the proxy prefix automatically. Apps that build absolute URLs into their own HTML from the request they received (rather than relative ones) will not render correctly through this β that's why the Services catalog favors single-page tools like Adminer over full frameworks.
Manually enabling the Sentinel agent
The button runs these same commands for you when it has a way to reach the host directly, and already bakes in an enrollment key rather than an operator key -- see below. Use the copy-paste versions here for a machine you'd rather install on by hand.
An install command ends up in shell history, a systemd unit, a plist, a scheduled task -- wherever it lands, a workspace-wide operator key sitting there is a bigger handout than a fresh install needs. Generate a scoped enrollment key instead and use it in --api-key below:
curl -X POST https://applotus.fly.dev/api/tenant/keys/enrollment \ -H "x-api-key: YOUR_OPERATOR_KEY"
Call this with your operator key (or a signed-in session) to mint the scoped key -- it's how the enrollment key gets created, not what the new agent authenticates with. Reuses any enrollment key that hasn't been claimed yet, so calling this repeatedly doesn't accumulate unused credentials. The key binds to whichever machine's first heartbeat presents it; every heartbeat after that from a different machine is refused.
sudo bash -c 'curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o /usr/local/bin/sentinel-agent.py && chmod +x /usr/local/bin/sentinel-agent.py cat <<EOF > /etc/systemd/system/sentinel-agent.service [Unit] Description=Applotus Sentinel Node Agent After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/python3 /usr/local/bin/sentinel-agent.py --server https://applotus.fly.dev --api-key YOUR_API_KEY Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload && systemctl enable --now sentinel-agent'
Replace YOUR_API_KEY with the enrollment key from above (or an operator key from Settings → API Keys, for a quick test). Logs: journalctl -u sentinel-agent -f.
mkdir -p ~/.applotus ~/Library/LaunchAgents ~/Library/Logs
curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o ~/.applotus/sentinel-agent.py
chmod +x ~/.applotus/sentinel-agent.py
# Pick a python3 launchd can actually exec (see server.js for why this is not just /usr/bin/python3).
PY=""
for c in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
if [ -x "$c" ] && "$c" -c "import sys" >/dev/null 2>&1; then PY="$c"; break; fi
done
[ -n "$PY" ] || echo "WARNING: no working python3 found -- run 'xcode-select --install' or 'brew install python3', then re-run this. The agent will not start until then."
cat <<EOF > ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.applotus.sentinel-agent</string>
<key>ProgramArguments</key>
<array>
<string>$PY</string>
<string>$HOME/.applotus/sentinel-agent.py</string>
<string>--server</string><string>https://applotus.fly.dev</string>
<string>--api-key</string><string>YOUR_API_KEY</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>$HOME/Library/Logs/applotus-sentinel-agent.log</string>
<key>StandardErrorPath</key><string>$HOME/Library/Logs/applotus-sentinel-agent.log</string>
</dict>
</plist>
EOF
launchctl bootout gui/$(id -u)/com.applotus.sentinel-agent 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist
launchctl print gui/$(id -u)/com.applotus.sentinel-agent | head -5
bash ~/Downloads/install-macos.shPrefer the download. The script below has to survive a copy-paste into Terminal, and on a real Mac it did not — the plist's argument block is one very long line, and zsh mangled it mid-heredoc. The downloaded installer writes the same plist a line at a time, checks that python3 actually runs, verifies the agent compiled, and lints the plist before loading it.
A LaunchAgent starts at login, not at boot. For a headless Mac that must run unattended, put the same plist in /Library/LaunchDaemons owned by root instead β but then the agent has no user session, so desktop/VNC detection won't work. KeepAlive auto-restarts the process, so stopping it requires launchctl bootout gui/$(id -u)/com.applotus.sentinel-agent, not just deleting the plist.
Invoke-WebRequest -Uri "https://applotus.fly.dev/download/sentinel-agent.py" -OutFile "sentinel-agent.py" python sentinel-agent.py --server https://applotus.fly.dev --api-key YOUR_API_KEY
That runs in the foreground and stops when the window closes or the machine reboots. For a Windows box that needs to come back up on its own after every reboot β before anyone logs in β download and run install.bat as Administrator instead: it checks for Python (installing it via winget if missing), downloads the agent, and registers it as a SYSTEM-run Scheduled Task with an "at startup" trigger, the Windows equivalent of the macOS LaunchDaemon above.
To stop or remove it later: schtasks /end /tn ApplotusSentinelAgent or schtasks /delete /tn ApplotusSentinelAgent /f (from an Administrator prompt).
Making a node container-ready (podman)
A node needs a container engine before it can run containers or services. Applotus deploys rootless podman β no daemon, no root requirement on Linux. The "Install Runtime" action on a node card runs the matching command below for you automatically when it detects passwordless sudo (Linux) or admin rights (Windows); use these yourself when it can't.
| OS | Install command |
|---|---|
| Debian / Ubuntu | sudo apt-get update -y && sudo apt-get install -y podman |
| Fedora / RHEL / CentOS / Rocky / Alma | sudo dnf install -y podman |
| Arch / Manjaro | sudo pacman -Sy podman |
| Alpine | sudo apk add podman |
| openSUSE | sudo zypper install -y podman |
| macOS | brew install podman && podman machine init && podman machine start |
| Windows | winget install -e --id RedHat.Podman |
macOS runs containers inside a small Linux VM that podman machine creates β that init step is the only slow, multi-GB one. Windows gets native Windows images with the default backend, or Linux images if you set podman's backend to WSL2 first.
Rootless Linux prerequisites β if the install above succeeds but containers still fail to start, the host is usually missing user namespaces. Fix with:
sudo sysctl -w user.max_user_namespaces=28633 sudo sysctl -w kernel.unprivileged_userns_clone=1 sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$USER" podman system migrate
Making a node remote-desktop ready
A node with no VNC server has no desktop to stream, so "Remote Desktop" on its card falls back to terminal-only. The "Enable Desktop" action installs the right piece automatically when it detects passwordless sudo (Linux) or admin rights (Windows/macOS); the equivalent manual commands are below for hosts where that can't run unattended.
sudo apt-get install -y x11vnc # Debian/Ubuntu sudo dnf install -y x11vnc # Fedora/RHEL sudo pacman -Sy x11vnc # Arch sudo zypper install -y x11vnc # openSUSE sudo apk add x11vnc # Alpine
sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -configure -access -on -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw YOUR_VNC_PASSWORD -restart -agent
No package to install β this just turns on the OS's own VNC-compatible screen sharing daemon on port 5900. YOUR_VNC_PASSWORD is capped at 8 characters (VNC's legacy DES key limit).
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://www.tightvnc.com/download/2.8.84/tightvnc-2.8.84-gpl-setup-64bit.msi', 'tightvnc-setup.msi'); Start-Process msiexec.exe -ArgumentList '/i tightvnc-setup.msi /quiet ADDLOCAL=Server' -Wait"
Installs and registers the tvnserver Windows service bound to loopback (127.0.0.1:5900) β traffic only ever reaches it through the encrypted agent tunnel, never a directly opened port. The one-click "Enable Desktop" action additionally locks down loopback-only + disables its own auth prompts, since the agent tunnel is the only intended path in.
Negotiating RFB session with agent...
Remote File Explorer Gzip / Zip Stream
Target Node
👁 Preview
Click Preview on a file to see it here — images, audio, video, and text/code all render inline.
🔑 Connection details
Reachable under this hostname from your other containers on the same tenant network.
Run this from another container on the same fleet machine — for example the VPS image’s terminal. The tenant bridge is local to each machine, so it will not resolve from your laptop, nor from a container on a different machine.
⚠️ Containers unavailable
VNC Authentication Required
Enter the remote desktop session password.
Maximum 8 characters for RFB authentication.
Remote Desktop (RDP) Port 3389
Direct high-performance native RDP session
Works directly with Windows Remote Desktop (mstsc), macOS Microsoft Remote Desktop, or Remmina.
xfreerdp /v:HOST:3389 /u:USERNAME
If this node is behind NAT or on a separate remote network, direct RDP port 3389 cannot be reached. Applotus can install an in-browser VNC server via the running agent to stream this desktop directly in your browser over the secure agent WebSocket tunnel!
Enable Remote Desktop 1-Click Setup
Target Node
sudo apt-get update && sudo apt-get install -y x11vnc
Install Container Runtime 1-Click Setup
Target Node
sudo apt-get update && sudo apt-get install -y podman
Deploy Container
Target Node
--privileged, no host network/PID namespace). Exposed ports are bound to 127.0.0.1 on the host and streamed to your browser over the agent's secure tunnel β never exposed on the LAN or public internet.
Containers on this same machine reach this one at this name — e.g. mysql://user:pw@mysql:3306/db. The tenant network is a local bridge per machine, so containers elsewhere need the service mesh. Lowercase letters, digits, - and _.
Treated as secrets: they are passed to the container and never shown again — only the variable names are kept. Names must be letters, digits and underscore. Values cannot contain newlines. For a database image, the bootstrap password set by Applotus wins over anything you put here.
Sentinel Assist
—
Container Lifetime
β
Extensions stack onto whatever time is left, up to 7 days out. Works on a container that has already hit zero, as long as the node hasn't swept it yet.
- No expiry β it is never auto-removed
- Runtime restart policy set to always, so it survives a host reboot
- Restarted within ~15s if the process exits
- Filesystem snapshotted every 15 min, last 3 kept, and rebuilt from the newest snapshot if the container is lost
- Can be hibernated (stopped, freeing its CPU/memory) and woken back up on demand
Container Environment Variables
Inspect and update deployment variables
Updating variables will cleanly recreate the container with new settings while preserving ports, images, and network links.
Agent Logs
Target Node
Loading…
Deploy Applotus Node Agent
Zero-dependency Python agent for Linux, macOS, and Windows
Run directly in foreground terminal for quick discovery:
curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o sentinel-agent.py && python3 sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default
Ctrl+C anytime to stop.
Run as detached background process using nohup:
curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o sentinel-agent.py && nohup python3 sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default > sentinel-agent.log 2>&1 &
tail -f sentinel-agent.log. Kill anytime via pkill -f sentinel-agent.py.
Install and enable automatic systemd background service:
sudo bash -c 'curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o /usr/local/bin/sentinel-agent.py && chmod +x /usr/local/bin/sentinel-agent.py
cat <<EOF > /etc/systemd/system/sentinel-agent.service
[Unit]
Description=Applotus Sentinel Node Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /usr/local/bin/sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now sentinel-agent
systemctl status sentinel-agent --no-pager'
journalctl -u sentinel-agent -f
Install a launchd LaunchAgent that starts at login and restarts on crash:
⬇ Download installermkdir -p ~/.applotus ~/Library/LaunchAgents ~/Library/Logs
curl -fsSL https://applotus.fly.dev/download/sentinel-agent.py -o ~/.applotus/sentinel-agent.py
chmod +x ~/.applotus/sentinel-agent.py
# Pick a python3 launchd can actually exec (see server.js for why this is not just /usr/bin/python3).
PY=""
for c in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
if [ -x "$c" ] && "$c" -c "import sys" >/dev/null 2>&1; then PY="$c"; break; fi
done
[ -n "$PY" ] || echo "WARNING: no working python3 found -- run 'xcode-select --install' or 'brew install python3', then re-run this. The agent will not start until then."
cat <<EOF > ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.applotus.sentinel-agent</string>
<key>ProgramArguments</key>
<array>
<string>$PY</string>
<string>$HOME/.applotus/sentinel-agent.py</string>
<string>--server</string><string>https://applotus.fly.dev</string>
<string>--api-key</string><string>YOUR_API_KEY</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>$HOME/Library/Logs/applotus-sentinel-agent.log</string>
<key>StandardErrorPath</key><string>$HOME/Library/Logs/applotus-sentinel-agent.log</string>
</dict>
</plist>
EOF
launchctl bootout gui/$(id -u)/com.applotus.sentinel-agent 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist
launchctl print gui/$(id -u)/com.applotus.sentinel-agent | head -5
π How to Stop / Unload Agent on macOS:
# Stop & unload the agent (works on all macOS versions): launchctl bootout gui/$(id -u)/com.applotus.sentinel-agent 2>/dev/null || launchctl unload -w ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist # (Optional) To permanently remove: rm -f ~/Library/LaunchAgents/com.applotus.sentinel-agent.plist
tail -f ~/Library/Logs/applotus-sentinel-agent.log
/Library/LaunchDaemons owned by root instead — but note the agent then has no user session, so desktop and VNC detection will not work.
KeepAlive in launchd automatically restarts the agent process if it is terminated by the server. You must run the unload command above on your Mac first to stop it permanently.
/usr/bin/python3, which needs the Command Line Tools (xcode-select --install). Point ProgramArguments at a Homebrew python if you prefer one.
Run natively on Windows via PowerShell:
Invoke-WebRequest -Uri "https://applotus.fly.dev/download/sentinel-agent.py" -OutFile "sentinel-agent.py"; if (Get-Command python.exe -ErrorAction SilentlyContinue) { python sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default } else { py sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default }
β‘ Python not recognized? Click for 1-click winget install Details βΎ
winget install Python.Python.3.11 --override "/quiet PrependPath=1" --accept-package-agreements --accept-source-agreements
Optional: Run in background (hidden window) Details βΎ
Start-Process python -ArgumentList "sentinel-agent.py --server https://applotus.fly.dev --tenant tenant_default" -WindowStyle Hidden
AI Orchestrator Configuration
Manage central model and API key for multi-agent delegation