Modd Hosting connects WHMCS to a small Go controller that provisions and manages website application containers on a single Linux host.
WHMCS remains responsible for customers, billing, products, and administrator actions. The controller owns the host-local work:
- Docker container lifecycle;
- ZFS datasets and site storage;
- blue/green deployments with health checks;
- per-service Caddy routing;
- suspension, termination, and permanent deletion;
- service state stored as TOML; and
- optional Google Chat lifecycle notifications.
The project does not depend on WHM, cPanel, Apache, .htaccess, Unix hosting
accounts, or /home-based paths.
go-service/ Go controller, OpenAPI contract, and host packaging
whmcs-plugin/ WHMCS provisioning and administrator addon modules
.github/ Tagged-release workflow
The full design and lifecycle rules are in whmcs-container-hosting-mvp-plan.md.
WHMCS ── HTTPS + bearer token ──> Caddy ── HTTP loopback ──> controller
│
┌──────────────────┼──────────────┐
▼ ▼ ▼
Docker ZFS service TOML
│ │
└──── Unix sockets ──> Caddy <────┘
Each WHMCS service becomes whmcs-<service-id>. Provisioning creates a ZFS
dataset, the required directory skeleton, a blue container, and an individual
Caddy service file. Upgrades deploy to the inactive blue/green slot, wait for
its Unix-socket health check, switch Caddy, drain existing traffic, and remove
the old container.
Lifecycle states are:
active: application traffic routes to the live container;suspended: containers are stopped and Caddy serves the suspension page;terminated: containers and routing are removed, but data remains;deleted: containers and the ZFS dataset are removed, leaving a tombstone.
Deletion is only allowed after termination. WHMCS termination removes runtime resources while retaining customer data; permanent purging remains an administrator action in Addons > Modd Hosting.
- Linux with Docker Engine and ZFS;
useradd,groupadd,userdel, andgroupdelfor per-service identities;- a Caddy container or trusted Caddy proxy;
- the configured Docker network;
- locally available application images; and
- WHMCS with PHP 8.1 or newer and cURL.
The controller runs as root because it manages ZFS, Docker, Caddy files, and
service identities and storage. Each whmcs-<id> service uses a matching host
user and group with UID/GID 10000 + id; its dataset, socket directories, and
container process use that identity. Do not add systemd filesystem namespace
options such as ProtectSystem, ProtectHome, or PrivateTmp:
controller-created ZFS mounts and Unix socket directories must be visible to
Docker and the host.
Download the Linux x86-64 controller from a tagged GitHub release, or build it:
(cd go-service && ./build.sh test && VERSION=dev ./build.sh)Install the binary and supplied files (replace go-service/controller with
the downloaded release binary when using a release):
sudo install -m 0755 go-service/controller /usr/local/bin/modd-hosting-controller
sudo install -d -m 0750 /etc/modd-hosting /var/lib/modd-hosting/caddy/services
sudo install -d -m 0750 /var/lib/modd-hosting/services /var/lib/modd-hosting/tombstones
sudo install -d -m 0750 /srv/modd-suspended /run/whmcs
sudo install -m 0640 go-service/config.example.toml /etc/modd-hosting/controller.toml
sudo install -m 0644 go-service/packaging/suspended/index.html /srv/modd-suspended/index.html
openssl rand -hex 32 | sudo tee /etc/modd-hosting/api-token >/dev/null
sudo chmod 0600 /etc/modd-hosting/api-tokenReview the configuration before starting the service. Install
go-service/packaging/modd-hosting-controller.service as a systemd unit.
Caddy must:
- import
/etc/caddy/services/*.caddy; - mount the controller's
caddy.service_config_dirat/etc/caddy/services; - mount the host socket tree referenced by
deployment.socketat the same path inside the container; - mount
caddy.suspension_rootread-only at the same path; and - expose the controller to WHMCS over HTTPS with a trusted certificate.
The controller itself serves HTTP. Keep its listener on loopback when Caddy runs on the host. If Caddy runs in a container, use host networking or bind the controller to a firewall-restricted address reachable only by Caddy.
The controller reads TOML from /etc/modd-hosting/controller.toml by default:
modd-hosting-controller -config /path/to/controller.tomlStart with go-service/config.example.toml.
Durations use Go notation such as 500ms, 10s, or 2m.
| Setting | Purpose |
|---|---|
server.listen |
HTTP address used by the controller. Prefer loopback. |
server.request_timeout |
Maximum API read and write duration. |
server.shutdown_timeout |
Time allowed for graceful shutdown. |
auth.bearer_token_file |
File containing the shared WHMCS bearer token. |
The token file must contain one non-empty secret. All API endpoints except
GET /v1/health require it. The same value is stored as the WHMCS server
password.
| Setting | Purpose |
|---|---|
zfs.dataset_prefix |
Parent dataset; a service becomes <prefix>/whmcs-123. |
zfs.mount_prefix |
Absolute mount root; a service becomes <root>/whmcs-123. |
state.services_dir |
Live per-service TOML records. |
state.tombstones_dir |
Records retained after permanent deletion. |
Back up the services and tombstones directories; site content follows the ZFS pool's snapshot and replication policy.
| Setting | Purpose |
|---|---|
caddy.service_config_dir |
Host directory for generated service Caddyfiles and hostname-to-socket map fragments. |
caddy.suspension_root |
Directory containing the shared index.html. |
caddy.active_template |
Active service Caddyfile template, repeated per domain; supports {domain}, {service_id}, {slot}, and {socket_name}. |
caddy.validate_command |
Argument array run after a service-file change. |
caddy.reload_command |
Argument array run after successful validation. |
Commands are executed directly, without a shell. Keep each executable and
argument as a separate TOML array item. The example commands validate and
reload a Caddy container named caddy. Change the container name or config
path if your Caddy deployment differs.
The master Caddyfile is operator-owned; the controller only creates files in
service_config_dir.
| Setting | Purpose |
|---|---|
docker.network |
Existing Docker network joined by every service container. |
docker.image_repository |
Allowed image repository, without a tag. |
docker.pull_timeout |
Optional image-pull deadline; defaults to 30m. |
docker.binds |
Fixed bind mounts applied to every service container. |
docker.environment |
Additional fixed KEY=value environment entries. |
Available versions are local tags matching
<docker.image_repository>:<version>. Pull the required tag before selecting
it in WHMCS:
docker pull your-registry/whmcs-runtime:v21.6.24Bind entries accept these placeholders:
| Placeholder | Value |
|---|---|
{mountpoint} |
Service ZFS mountpoint. |
{service_id} |
Stable ID such as whmcs-123. |
{slot} |
blue or green. |
{socket_path} |
/run/nginx through v26.1.14; /run/moddengine for newer or non-numeric versions. |
The same placeholders expand in docker.environment entries. The controller
also supplies ME_SITE, ME_INSTANCE, and the slot-specific socket bind
automatically. Images through v26.1.14 must create /run/nginx/nginx.sock;
newer and non-numeric versions must create /run/moddengine/http.sock. Use
{socket_name} in caddy.active_template so Caddy follows the image. See
go-service/docs/container-runtime.md
before changing mounts.
| Setting | Purpose |
|---|---|
deployment.health_path |
HTTP path checked through the new Unix socket. |
deployment.health_attempts |
Maximum checks before deployment failure. |
deployment.health_initial_delay |
Delay before the first check. |
deployment.health_backoff_increment |
Increasing delay between checks. |
deployment.traffic_drain |
Wait before removing the old deployment. |
deployment.socket |
Host Unix-socket path template; supports {service_id} and {slot}. Its filename is selected for the image version. |
domains.staging_suffix |
Suffix appended to staging labels supplied by WHMCS. |
dns.endpoint |
WHMCS DNS API base ending in dns.php; omit the section to disable updates. |
dns.key_file |
File containing a WHMCS DNS API key with dns_write access. |
For example, staging label preview with suffix staging.com becomes
preview.staging.com. A deployment is routed only after the health endpoint
returns HTTP 2xx. Failed provision and upgrade artifacts remain in place for
diagnosis.
After a container becomes healthy, the controller verifies the domain A record
and www CNAME before enabling its Caddy route. Drifted records are repaired,
including removal of conflicting AAAA and www A/AAAA records, then routing
waits five seconds for DNS propagation. Transient failures also wait five
seconds before routing and retain the existing background retries; 403 and
mutation 404 responses are recorded as permanent and do not retry. When the
main domain changes, records on the old domain that still point to the service
are removed after the new route is active.
Administrators can queue the current domain and IPv4 again with
POST /v1/services/{id}/dns/reconnect.
| Setting | Purpose |
|---|---|
google_chat.webhook_url_file |
File containing a Google Chat webhook URL. |
Set webhook_url_file to an empty string to disable notifications. An
unreadable configured webhook disables notifications with a warning but does
not stop the controller.
JSON logs are written to stderr and captured by systemd's journal.
Install the release ZIP into the WHMCS root, or copy the module directories:
cp -R whmcs-plugin/modules/servers/moddhosting /path/to/whmcs/modules/servers/
cp -R whmcs-plugin/modules/addons/moddhosting /path/to/whmcs/modules/addons/In WHMCS:
- Add a server using module Modd Container Hosting.
- Set its hostname to the controller's HTTPS Caddy hostname.
- Set the public Caddy HTTPS port, normally
443. - Store the bearer token as the server password.
- Create a product using that server.
- Give the product and product group stable slugs when available. Missing
slug/name components are sent as
Unknown. Configure option names ascode|Display Labelusing lowercase snake-case codes and machine-readable values. - On each service, enter an available controller image tag in Image Version.
- Leave Staging Hostname blank to disable staging, or enter a label of up to 32 characters; the controller's staging suffix is appended.
- Activate Modd Hosting under addon modules and restrict it to trusted administrator roles.
- Set a unique Docker Hub Webhook Token, then copy the webhook URL from the addon's Docker images page into the Docker Hub repository settings.
The provisioning module creates, suspends, resumes, terminates, and displays
services. The addon provides controller status, permanent purging, upgrades,
and bulk upgrades.
The Docker images page also pulls the latest stable v* tag or an exact
PR/dev tag on every configured controller.
Run controller tests:
cd go-service
./build.sh testRun PHP static analysis:
cd whmcs-plugin
composer install
composer analyseThe API contract is go-service/openapi.yaml.
Commit the release changes, create a new semantic-version tag, then push
main and that tag (replace vN.N.N with the release version):
git add -A
git commit -m "Release vN.N.N"
git tag vN.N.N
git push origin main vN.N.NTagged pushes matching v* build and publish the controller and WHMCS ZIP as
GitHub release assets.