Docs / Control Center

Deploying Control Center

Control Center runs as a microVM with a compose stack inside it — the first real workload most people deploy. Standing it up, exposing it, and enrolling nodes into it.

Control Center is a microVM running a compose stack. If you have read MicroVMs, you already know how to run one — this is the first one worth running.

That is not an accident of packaging. It means the web interface is an ordinary guest on a node you control: it snapshots, replicates and backs up like anything else, and it can be destroyed and rebuilt without touching the hosts it manages.

Nothing here is required. A node runs its guests, takes its snapshots and serves its API whether or not Control Center exists. This is a window, not a controller — see Why Hoster.

What you need first

  • A node with a working microVM template — see MicroVMs.
  • The deployment bundle from HosterControlCenter/deployments/microvm: a compose.yaml, an example.env, and a systemd unit.
  • Access to a container registry holding the four Control Center images.

About the registry

The compose file takes the registry as a variable, CC_REGISTRY, so it is not tied to any particular one. Today the published images live at harbor.gateway-it.com/library; point CC_REGISTRY wherever yours are.

The four images are hoster-backend, hoster-frontend, hoster-websockify and hoster-caddy, and they share a single CC_IMAGE_TAG.

Never deploy latest. Build, tag with an immutable timestamp, push that exact tag, and pin it. latest moving under a running deployment is how you end up unable to say what is deployed, and unable to roll back to what was.

./HosterControlCenter/containers.sh build
tag=$(sudo docker images hoster-backend --format '{{.Tag}}' \
  | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r | head -1)
for image in backend frontend websockify caddy; do
  sudo docker tag  "hoster-$image:$tag" "$CC_REGISTRY/hoster-$image:$tag"
  sudo docker push "$CC_REGISTRY/hoster-$image:$tag"
done
printf 'CC_IMAGE_TAG=%s\n' "$tag"

1 · Deploy the microVM

hoster microvm deploy --name controlcenter --compose-file ./compose.yaml

The bundle’s systemd unit uses /data as its working directory, which is exactly the host-side directory a microVM mounts at /data — so the compose file, the .env, the database and the TLS material all live on the host side of that mount, where you can see them and back them up.

2 · Configure it

Copy the example, replace every placeholder, and lock the file down before starting. It holds an admin password and two secrets:

cp example.env .env
chmod 600 .env
docker compose config --quiet
docker compose pull
docker compose up -d
Value What it is
CC_REGISTRY Where the four images are pulled from
CC_IMAGE_TAG The one immutable tag every service is pinned to
CC_EXTERNAL_IP The address the parent node’s DNAT rules publish
CC_TLS_DOMAIN Name on the generated certificate
TUNNEL_EXTRA_CC_ADDRESSES Where enrolled hosts reach the tunnel, address:port
HOSTER_CC_ADMIN_USER / ..._PASSWORD First administrator
JWT_SECRET Signs sessions — at least 32 random characters
TRUSTED_PROXY_SECRET Shared with the proxy — at least 32 random characters

Then install the unit so the stack comes back after the microVM reboots:

sudo install -o root -g root -m 0644 hoster-control-center.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now hoster-control-center.service

3 · Publish it

The stack listens on two ports inside the microVM: 443 for HTTPS, and 8443 for the mutually authenticated tunnel that enrolled hosts dial out to. Neither is reachable until the node forwards to them, which is what the microVM-aware firewall rules are for:

hoster firewall bridge add-dnat hoster-internal \
  --target-microvm controlcenter \
  --external-address 192.0.2.10 \
  --external-port 1443 \
  --internal-port 443 \
  --protocol tcp

hoster firewall bridge add-dnat hoster-internal \
  --target-microvm controlcenter \
  --external-address 192.0.2.10 \
  --external-port 18443 \
  --internal-port 8443 \
  --protocol tcp

--target-microvm names the guest rather than an address, so the rule survives the guest getting a different one. The same rule can be written on the guest itself with hoster firewall microvm add-dnat controlcenter … — see Firewall.

Whatever external port you choose for 8443 has to match TUNNEL_EXTRA_CC_ADDRESSES in the .env — that is the address enrolled hosts will be told to dial.

Screenshot

Take a screenshot of the Control Center login page as first reached over HTTPS, browser certificate warning included, since that is what a first-time deployer actually sees.

The certificate is self-signed

It is generated on first start and persisted alongside the deployment. Browsers will warn until you trust it or replace it with your own. That is cosmetic for the browser and load-bearing for the tunnel: enrolled hosts pin its fingerprint, so replacing the tunnel material after nodes have joined breaks their trust relationship.

4 · Enrol your nodes

This is the step that makes the architecture concrete. Nodes are not added by Control Center reaching into them — each host dials out and holds the connection open.

Generate a join token in the interface, then on the node:

hoster control-center join --token <base64-token>

That configures WireGuard — under /opt/wireguard unless --wireguard-path says otherwise — and establishes the tunnel. Nothing has to reach into the node’s network, which is what makes this work across sites and behind NAT.

To register a host by hand instead, ask it to describe itself:

hoster control-center show-info

That prints the hostname, API bind address and port, protocol, and the admin credentials from /opt/hoster/conf/agent_config.json — the values Control Center needs to add the host manually.

Screenshot

Take a screenshot of the Control Center hosts page with three or four nodes enrolled and online, which is the payoff shot for this whole page.

Upgrading

Upgrades are a one-line change, which is the reward for pinning tags:

# push a new immutable tag, then in .env change only CC_IMAGE_TAG
docker compose pull
docker compose up -d
curl -k https://<address>/api/v1/health

Rollback is the same three commands with the previous tag. Check the health endpoint after every restart or rollback, and check the tunnel is accepting connections — an interface that loads while the tunnel is down looks healthy and manages nothing.

What to back up

Four things, and losing any of them costs more than the microVM did:

Why
The database everything Control Center knows
Tunnel CA and identity its loss disrupts every enrolled host, which pinned it
TLS material the certificate browsers and the tunnel were trusting
.env the JWT secret; losing it invalidates active sessions

Back all four up before changing tags, not after.

Because Control Center is an ordinary microVM, the platform’s own tooling covers this — snapshot it, replicate it to another host, or give it a DR target, exactly as with any other guest. See Snapshots, replication and the scheduler.

Next

  • MicroVMs — the guest type this runs on.
  • The interface — a tour of what each page shows once it is running.
  • Why Hoster — what the interface is for, and why everything in it is also on the CLI.

Something unclear on this page?

Ask about this specific page and we will come back to you. Your question arrives with a link to it, so you do not have to describe where you were.