Manage Podman Quadlets across multiple Linux servers through SSH connections.
Disclaimer This has been developed with help of Claude and Gemini. I'm an old timer developer with most experience in c# then c++, not much frontend development except for ugly UI with .Net or QT. All the above led me to choose htmx and python. I felt that htmx left most of the power to the backend and also less learning for me to assure a minimum learning to perform proper QA.
I first started my development in my homelab Forgejo repository but as I want to be as transparent as possible, I've transfered everything in Github (all issues and Git commit history).
- Manage servers over SSH without installing remote agents
- Control quadlets on multiple Linux servers from a single interface
- Edit files, navigate directories, and view systemd status simultaneously
- Stream live logs and monitor CPU, memory, and container health
- Sync external file changes automatically
- Assign viewer (read-only) or editor (full access) roles
- Store SSH private keys with AES-256-GCM encryption
- Start, stop, and restart Podman
.podquadlets directly - Create quadlets using built-in templates
[Insert screenshot of settings view]
- Python 3.12+
- Remote Linux servers with systemd and Podman installed
- SSH access to managed servers (key-based authentication)
git clone <repository-url>
cd QuadletManager
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtQUADLET_MASTER_KEY=$(openssl rand -hex 32) docker compose up -dCopy quadletmanager.container to ~/.config/containers/systemd/ (rootless) or /etc/containers/systemd/ (rootful), then:
systemctl --user daemon-reload
systemctl --user start quadletmanagerThe master key is used to encrypt and decrypt SSH private keys stored in the database. Set it via environment variable or config.yaml:
export QUADLET_MASTER_KEY=$(openssl rand -hex 32)If unset, a dev key is auto-generated and persisted to master.key (mode 0600) next to quadlets.db, so restarts reuse the same key. Treat master.key like the database itself: back it up alongside quadlets.db and keep it out of version control, since anyone with master.key can decrypt the stored SSH private keys.
master_key: "your-64-char-hex-master-key"
session_timeout: 3600
poll_frequency: 10
dev_auto_login: false| Variable | Description |
|---|---|
QUADLET_MASTER_KEY |
AES-256 master key (64 hex characters). Falls back to an auto-generated dev key persisted in master.key if unset |
QUADLET_CONFIG_PATH |
Path to config YAML file (default: config.yaml) |
QUADLET_DB_PATH |
Path to SQLite database file (default: quadlets.db) |
LOG_LEVEL |
Logging level: DEBUG, INFO, WARNING, ERROR |
source venv/bin/activate
export QUADLET_MASTER_KEY=$(openssl rand -hex 32)
uvicorn main:app --host 0.0.0.0 --port 8000On first startup, the database is seeded with two default users:
| Username | Password | Role |
|---|---|---|
admin |
admin |
Editor |
viewer |
viewer |
Viewer |
Change these passwords before deploying to production.
On each target server, create a dedicated user and grant passwordless sudo for the specific commands QuadletManager needs.
Important
Managing the Local Host (Loopback Connection): If you are running QuadletManager inside a container (using Podman/Docker) and want to manage the host machine it is running on:
- Do not use
localhostor127.0.0.1as the server IP, as that refers to the container itself. - For Podman, use
host.containers.internalas the IP/hostname in the settings panel. - For Docker, use
host.docker.internal(you may need to addextra_hosts: ["host.docker.internal:host-gateway"]to yourdocker-compose.ymlor container configuration flags on Linux). - Make sure the host's SSH service is running, and your public SSH key is added to the host's
authorized_keysfile.
sudo useradd -m -s /bin/bash quadlet-agentAdd your QuadletManager public SSH key to /home/quadlet-agent/.ssh/authorized_keys.
For rootless (user) scope quadlets, no sudo is required. For global scope quadlets, create /etc/sudoers.d/quadlet-manager:
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u *
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/tee /etc/containers/systemd/*
quadlet-agent ALL=(ALL) NOPASSWD: /usr/bin/podman stats *| Scope | Path on Remote Server | Sudo Required |
|---|---|---|
| User (rootless) | ~/.config/containers/systemd/ |
No |
| Global (rootful) | /etc/containers/systemd/ |
Yes |
See docs/TESTING.md for the full testing guide.
pip install -r requirements-test.txt
PYTHONPATH=. pytest tests/ --ignore=tests/e2e/playwright install chromium
PYTHONPATH=. pytest tests/e2e/PYTHONPATH=. pytest tests/ --ignore=tests/e2e/ --cov=. --cov-report=term-missingflowchart TD
Browser["Browser (HTMX/Monaco/Chart.js)"] --> FastAPI["FastAPI Server"]
FastAPI --> Sync["Sync Engine"]
FastAPI --> Stats["Stats Engine"]
FastAPI --> SSH["SSH Manager (Connection Pool)"]
Sync --> Remote["Remote Linux Servers (systemd + podman)"]
Stats --> Remote
SSH --> Remote
- Backend: FastAPI (Python) with asyncssh for SSH connection pooling
- Frontend: HTMX for server-driven UI updates, Monaco Editor for file editing, Chart.js for resource visualization
- Database: SQLite with AES-256-GCM encrypted SSH key storage
- Real-time: Server-Sent Events for stats/sync updates, WebSocket for log streaming
Full architecture documentation: docs/ARCHITECTURE.md
| Method | Path | Description |
|---|---|---|
| POST | /login |
Submit credentials |
| GET | /logout |
Clear session |
| GET | / |
Main dashboard |
| GET | /api/servers |
Server list |
| GET | /api/quadlets/{server_id} |
File tree for a server |
| GET | /api/file/{server_id} |
File content |
| POST | /api/save |
Save file content |
| POST | /api/create |
Create new quadlet |
| GET | /api/systemctl/status/{server_id} |
Get unit status |
| POST | /api/systemctl/{server_id} |
Execute systemctl action (start/stop/restart) |
| POST | /api/pod-action/{server_id} |
Execute pod action |
| GET | /api/health/history/{server_id} |
Container health history |
| GET | /api/events |
SSE stream for real-time updates |
| WS | /ws/logs/{server_id}/{unit_name} |
Log streaming |
Full API documentation: docs/ARCHITECTURE.md
- docs/ARCHITECTURE.md -- system architecture, component details, API reference
- docs/SETUP.MD -- remote server setup guide
- docs/TESTING.md -- test structure and writing guide
- docs/REQUIREMENTS.MD -- original requirements specification


