> ## Content Index
> Fetch the complete content index at: https://blog.diengdoh.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# KeyHelp: The Free Plesk Alternative I Moved To (and Its API Traps)
- URL: https://blog.diengdoh.com/keyhelp-free-plesk-alternative/
- Published: 2026-09-10T10:00:00.000Z
- Updated: 2026-10-11T10:16:27.000Z
- Description: Plesk license expired, panel dead. I moved to KeyHelp: non-interactive install, one client per company, and the API gotchas that cost me hours.
- Author: Lukas
- Tags: Linux, Ubuntu

The Plesk license on a box I run for a small business expired, and with it most of the panel. Sites kept serving, but anything that needed the panel to *do* something — new domains, mailboxes, certificates — was dead. I wrote about what still works in that state in [Plesk with an expired license](/plesk-expired-license-what-still-works/). This post is about what I moved to instead.

**TL;DR:** KeyHelp is a free hosting panel from Keyweb. It installs non-interactively on a clean Ubuntu 24.04 and brings the full classic stack: Apache + PHP-FPM in several versions, MariaDB, Postfix/Dovecot with Rspamd, BIND, ProFTPD, webmail, fail2ban, an iptables firewall and backups. It's good. But the REST API is off by default, half the client fields are read-only, and a new client gets a 1 MB quota that will make your first mailbox fail. Details below.

## Install

Fresh Ubuntu 24.04 LTS, nothing else on it:

```bash
wget https://install.keyhelp.de/install_keyhelp.sh
bash install_keyhelp.sh --non-interactive

```

That's it. It runs for a while, pulls in everything, and at the end drops a file with the admin credentials into `/root` (named `keyhelp_login_data_<date>_<time>`). Read it, store the password in your password manager, delete the file.

**Update (October 2026):** a lot of wiki and forum posts still point at a `get_keyhelp.php` download URL. When I checked it in October 2026 it returned 404\. Use the `install.keyhelp.de/install_keyhelp.sh` URL above.

Two more install notes:

- Keyweb's docs list Ubuntu up to 24.04 as supported. The installer itself does support 26.04 as well, but for a production box I'd stick with what's documented.
- Set the hostname, the A record and the reverse DNS **before** you install. The panel and Postfix pick up the FQDN, and fixing a mail server's identity afterwards is no fun.

If you forget the admin password or just don't want to type it, there's a nice escape hatch:

```bash
ssh root@203.0.113.10 "keyhelp login"

```

That prints a one-time login link. Click, you're in. I use it more than the actual password.

## What you get

Out of the box, without touching a single package:

| Area     | Component                                                  |
| -------- | ---------------------------------------------------------- |
| Web      | Apache with PHP-FPM, multiple PHP versions side by side    |
| Database | MariaDB, phpMyAdmin / Adminer                              |
| Mail     | Postfix, Dovecot, Rspamd, webmail (Roundcube / SnappyMail) |
| DNS      | BIND (optional, per domain)                                |
| FTP      | ProFTPD                                                    |
| Security | fail2ban, iptables firewall with INPUT policy DROP         |
| Ops      | backup tooling, AWStats, disk quota                        |

The multi-PHP part was the main reason I picked it over a bare Ubuntu + certbot setup. I had one legacy site that needs PHP 7.4 and newer apps on 8.3/8.4, and that's a checkbox per domain here instead of an afternoon of PPAs — more on that in [running multiple PHP versions on one server](/multiple-php-versions-one-server/).

## Structure: one client per company

The box serves several separate businesses. In KeyHelp terms each one becomes its own **client**, and a client is a real boundary:

- its own Linux user (files under its own home, PHP-FPM pools run as that user)
- its own domains, databases, mailboxes
- its own disk quota

So if one company's WordPress gets popped, it's sitting in a different Linux user than the neighbours. That's the main thing I wanted from a panel anyway — I can do the rest by hand, but per-tenant isolation done consistently is tedious without one.

## Gotcha: don't create a Google Workspace domain as a mail domain

One of the companies runs its mail on Google Workspace. Its MX points at Google, and that has to stay that way.

If you add that domain in KeyHelp as an **email domain**, the local Postfix considers itself responsible for it. Every mail generated *on this box* to that domain — contact forms, WordPress notifications, cron output — gets delivered locally into mailboxes nobody reads instead of going out to Google. Classic split-brain mail.

So: create it as a web domain only, mail off. Postfix itself is cleanly configured from the start — check it if you're paranoid:

```bash
postconf mydestination
cat /etc/mailname

```

Both should only contain `localhost` and the server's own hostname, not your clients' domains.

## Gotcha: the API is invisible until you turn it on

I wanted to script client and mailbox creation instead of clicking. KeyHelp has a REST API at `/api/v2`. Except:

- It's **off by default**. The menu entry lives under **Configuration → API**, and it's not shown at all until the API is enabled. I went looking for a menu item that simply didn't exist yet.
- The API key is stored **hashed** in the database. Copy it when it's shown. You can't read it back later, and inserting your own key into the DB by hand is blocked — don't try, it's the wrong approach anyway.
- An empty IP field on the key means no IP restriction. Fill it in if your scripts run from a fixed host.

Calling the API from the server itself is slightly awkward because the panel's hostname resolves to the public IP. `--resolve` pins it to localhost while keeping the correct SNI/Host:

```bash
curl -s --resolve panel.example.com:443:127.0.0.1 \
  -H "X-API-Key: $KH_API_KEY" \
  https://panel.example.com/api/v2/clients

```

## Gotcha: PUT returns 200 and does nothing

This one is nasty. A client object has `resource_limits` and `permissions`. In the API they're **read-only**. If you `PUT /clients/{id}` with new values in those fields, you get a cheerful **200 OK** — and nothing changes. No error, no warning.

The only way to change limits is through a **hosting plan**: create a plan with the limits you want, then assign it to the client.

```bash
# 1. create a plan (fields per the API docs) – returns the new plan id
curl -s --resolve panel.example.com:443:127.0.0.1 \
  -H "X-API-Key: $KH_API_KEY" -H "Content-Type: application/json" \
  -X POST -d @plan.json \
  https://panel.example.com/api/v2/hosting-plans

# 2. attach the plan to the client
curl -s --resolve panel.example.com:443:127.0.0.1 \
  -H "X-API-Key: $KH_API_KEY" -H "Content-Type: application/json" \
  -X PUT -d '{"id_hosting_plan": 2}' \
  https://panel.example.com/api/v2/clients/3

```

Always re-`GET` the object after a write. A 200 from this API is not proof of anything.

## Gotcha: the 1 MB default quota

Which brings me to why I needed plans in the first place. A client created without a plan gets a disk quota of **1 MB**. Then you try to create a mailbox with, say, 5 GB, and get:

```
max_size property is bigger than assigned disk space

```

Took me a minute to realise the problem wasn't the mailbox, it was the client. I now have a "Standard 50G" plan and every new client gets it at creation. Do that first and you'll never see this error.

## Gotcha: keyhelp-toolbox hangs over SSH

`keyhelp-toolbox` sounds like the CLI you'd want for automation. It's an interactive menu. Run it as a one-liner over SSH (`ssh root@host keyhelp-toolbox ...`) and it just sits there waiting for input that never comes. Use it in a real interactive session, or use the API.

Similarly, the panel's internal cron job scripts aren't meant to be run standalone — the SSL maintenance job crashed when I tried calling it directly. There's a supported way to trigger it, which I cover in the [Postfix SNI post](/postfix-sni-hash-map-lookup-problem/).

## Gotcha: "Add domain" eats your input

UI bug, but it bit me repeatedly. In **Add domain**, after you pick the owner and hit Continue, the form sometimes reloads a few seconds later. Anything you typed into the General tab in that window — subdomain, name, parent domain — is silently gone, and you save a domain with wrong or empty fields.

My routine now: Continue, wait 5–6 seconds, *then* fill in the fields, and check them once more before Save.

## What I'm running behind it

Not everything is a PHP site. Some apps are Docker stacks, and those live on the same box bound to `127.0.0.1`, with KeyHelp's Apache in front as reverse proxy via per-domain directives — see [Apache as reverse proxy for Docker apps](/apache-reverse-proxy-docker-apps/). Fair warning: Docker and the KeyHelp firewall don't get along out of the box. That's its own post: [KeyHelp's firewall breaks Docker](/keyhelp-firewall-breaks-docker/).

## Verdict

For "several small businesses, websites + mail, one server, one admin", KeyHelp does what Plesk did for me, for free, without a license that can expire and take the panel with it. The UI is fine, the stack underneath is standard Debian/Ubuntu packages you can still debug by hand, and the per-client isolation is real.

The API is the weak spot. It works, but read-only fields that accept writes silently and a hidden menu entry are the kind of thing that eats an evening. Use plans from day one, verify every write with a GET, and keep `keyhelp login` handy.

Before you migrate anything to a new box, though: [lower your DNS TTL](/lower-dns-ttl-before-migration/) first. I learned that one the slow way.

---

### Related posts

- [Running PHP 7.4, 8.3 and 8.4 Side by Side on One Server](https://blog.diengdoh.com/multiple-php-versions-one-server/)
- [systemd Timers Instead of cron](https://blog.diengdoh.com/systemd-timers-instead-of-cron/)
- [bpftrace One-Liners I Actually Use on Production Linux](https://blog.diengdoh.com/bpftrace-linux-observability/)