As a technical architect, I didn't want a rented cloud VPS running a generic shared-hosting panel. I wanted to turn a Raspberry Pi 5 on my desk, behind a static IP, into a real hybrid personal server — and HostPanel was the initiative to build it.
A year in, the project looks structurally different from what I described the first time I wrote this up. The core gateway is on v3.1.0 with a self-update path that pulls straight from GitHub. The packages I started with were bundled into the repo; there are seventeen of them now, and none are bundled — the panel fetches a live catalog and installs on demand. That one change rewrote a good chunk of how the system actually works, so this post is less "here's the philosophy" and more "here's what the philosophy produced a year later."
The hybrid personal server problem — still the reason this exists
Traditional control panels are built around one paradigm: a disposable VM hosting hundreds of isolated tenant sites. A hybrid personal server needs a different architecture entirely. It lives on bare metal in my own workspace and has to run two zones side by side:
- Private infrastructure — internal databases, S3-compatible object storage, a WireGuard gateway. These stay reachable only over an encrypted tunnel or the LAN.
- Public workloads — production apps, ACME certificates, an authoritative PowerDNS nameserver serving traffic straight to the internet over the static IP.
Generic control panels fail at this because they act like installer scripts that seize the OS: config scattered across /etc, logs dumped in /var/log, unvetted PPAs injected into apt, multi-hour source compiles pegging all four ARM cores. On a Pi running both zones, that drift isn't tolerable — a bad package update can't be allowed to touch the private network or force a re-flash of the SD card.
What actually shipped: a marketplace, not a bundle
The original plan was to hand-curate fifteen packages and ship them inside the core repo. That lasted about as long as it took to notice that "add a package" meant "cut a new core release" — every package change, however small, forced a full portald redeploy just to get a new manifest onto disk.
So the three-repo split grew a fourth mechanism: a catalog. hostpanel-packages now publishes a catalog.json on its master branch — name, version, category, and an archive_url pointing at a release tarball in the same repo:
{
"name": "backup",
"version": "0.1.1",
"category": "System & Access",
"archive_url": "https://raw.githubusercontent.com/Developer-Geekay/hostpanel-packages/master/releases/backup.tar.gz",
"raw_base_url": "https://raw.githubusercontent.com/Developer-Geekay/hostpanel-packages/master/packages/backup"
}
Installing a package now means: read the catalog entry, pull the tarball, extract it under /opt/hostpanel/packages/<name>, mint it a token, register its systemd unit. Adding package eighteen is a commit to hostpanel-packages, not a portald release.
Fetching that catalog isn't a single trusting curl, either. portald tries the GitHub Contents API first — which also works against a private repo once a personal access token is configured — falls back to raw.githubusercontent.com, cache-busts both with a timestamp query parameter, and writes whatever it gets back to /opt/hostpanel/etc/catalog.json so a Pi that can't reach GitHub right now still has yesterday's list instead of nothing. The token support exists because I wanted the option to fork the packages repo private without losing the marketplace mechanism; it resolves from an environment variable, then a root-owned file, then the database, in that order, and it never appears in a log line.
Seventeen packages sit in the catalog as of this post: the original web servers and runtimes (Apache, Nginx, PHP, Node.js), MySQL and MongoDB as their own installable services rather than a Postgres/MariaDB/Mongo grab-bag, WireGuard, and two that didn't exist in the first version — backup and security. Backup takes scheduled and on-demand snapshots of the core database and every installed package's config and data; restore is deliberately manual and off-panel, because a one-click restore button on an infrastructure panel is also a one-click way to overwrite your only good copy. security is a UFW front end that auto-allows 80/443/53 on install and gives each firewall rule an allow/deny toggle instead of a raw rule editor.
The rules didn't change — they got written down properly
The filesystem-isolation and audit-logging rules from the original PROJECT_RULES.md survived intact, but they stopped being a bullet list somewhere along the way. They're a decision table in docs/ARCHITECTURE.md now, each entry paired with its reason:
| Decision | Choice | Because |
|---|---|---|
| Feature isolation | own process, port, Linux user, sudoers | a compromise is bounded to one domain |
| Operations | bash, one root-owned script per feature | the privileged surface is auditable in one file, by eye |
| Transport | HTTP on 127.0.0.1 + per-service bearer token | a loopback port is reachable by every local process |
| State | portald is the only database writer | feature services cannot read tokens or password hashes |
| Discovery | manifest.json on disk | inert data; portald never imports feature code |
That last row is the one that mattered most in practice. An earlier prototype discovered packages by calling entry_point.load() — importing a package's Python straight into the running core process. A bad package didn't fail on its own; it took the whole panel down with it. The current gateway never imports a line of package code. It reads a manifest, allocates a loopback port, and starts a separate process. The blast radius of a broken package is one systemd unit, not the gateway serving everything else.
What's next
Part 2 goes back inside portald itself with the corrections a year of running this thing produced: why every package gets a static bearer token instead of a signed request, why streaming routes live on their own URL prefix instead of a query flag, and how the catalog fetch above actually gets wired into install, uninstall, and reload.
Repository: Developer-Geekay/hostpanel · Developer-Geekay/hostpanel-packages · Developer-Geekay/hostpanel-binaries