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
- Python 3.12+
- Remote Linux servers with systemd and Podman installed
- SSH access to managed servers (key-based authentication)
Download the source archive for the version you want from the Releases page, or clone and check out a release tag:
git clone https://github.com/karoltheguy/QuadletManager.git
cd QuadletManager
git checkout v0.1.0
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtOmit the git checkout to track main, which is the development branch.
QUADLET_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 quadletmanagerImages are published to ghcr.io/karoltheguy/quadletmanager for linux/amd64:
podman pull ghcr.io/karoltheguy/quadletmanager:0.1| Tag | Points at |
|---|---|
latest |
The newest release, whatever its version number |
0.1 |
The newest patch in the 0.1.x series |
0.1.0 |
That exact release |
main |
Changes merged but not yet released. Most likely to break |
Pin to 0.1 or an exact version for production. Releases before 1.0.0 may
make breaking changes in a minor bump, so latest can move across one without
warning.
docker-compose.yml and quadletmanager.container build or reference a local
image. Point image: or Image= at ghcr.io/karoltheguy/quadletmanager:0.1
to run a published build instead.
The 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, install the allowlist that ships with the repo, substituting your agent account for %AGENT%:
sed 's/%AGENT%/quadlet-agent/' deploy/quadlet-manager.sudoers \
| sudo tee /etc/sudoers.d/quadlet-manager >/dev/null
sudo chmod 440 /etc/sudoers.d/quadlet-manager
sudo visudo -cf /etc/sudoers.d/quadlet-managerThe rules themselves are listed and explained in docs/SETUP.MD.
Known gap. These rules are not currently sufficient. The app runs about twice as many commands under
sudothan they permit, so several global-scope features fail on a server set up this way. See issue #289 and docs/SUDO_PERMISSIONS.md, which records every command the app actually runs under sudo.
| 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
Contributions are welcome. See CONTRIBUTING.md for setup, how to run the four test suites the way CI does, and the pull request process.
- CONTRIBUTING.md -- development setup and pull request process
- 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



