Your servers dial out. The panel never dials in.
Every xofactory agent connects outbound and nothing else. Why that constraint is worth the trouble it causes.
dockxo, cronxo and adminxo each install a small agent on your server. All three agents share one rule: they open a connection to the panel, and the panel never opens a connection to them. There is no inbound port, no SSH key stored on our side, no firewall rule to add.
This started as a security decision and turned out to be a usability one.
What the agent does
On start, the agent reads a token from its config, opens a WebSocket (dockxo, adminxo) or a long-poll HTTPS loop (cronxo) to the panel, and authenticates with the token. From then on, the panel sends commands down that connection and the agent sends results and events back up. If the connection drops, the agent reconnects with backoff. Commands that arrive while it is away are queued on the panel and delivered once with an acknowledgement, so a flaky link does not run a deploy twice.
The dockxo agent speaks to the local Docker socket. The adminxo agent is a single Python file that uses only the standard library, including its own small WebSocket client, so it runs on a fresh Ubuntu with nothing installed. The cronxo agent is a static Go binary that also supervises the jobs it starts.
Why outbound only
You do not have to trust our network. If the panel is compromised, an attacker gets whatever the agent is willing to do for an authenticated panel. They do not get a shell by default, and they cannot reach a server whose agent is stopped. With SSH-key-based panels, a leaked key is a root shell on every server.
It works behind NAT and strict firewalls. A box in an office, a Raspberry Pi at home, a VM with a provider firewall that only allows 443 out: all of them work without a port forward. Several dockxo users deploy to machines that have no public IP at all.
The token is the whole secret. Each server has its own token. Rotate it from the panel and the old one stops working the next time the agent reconnects. Delete the server and the token is gone.
What it costs
The browser terminal in adminxo is the awkward case. A terminal wants a bidirectional stream between your browser and a PTY on the server. With inbound access that is one hop. Outbound only, the agent opens a second WebSocket for the session, the panel bridges your browser’s WebSocket to it, and every keystroke makes two hops. It adds a few milliseconds and some code, and it is still worth it.
The other cost is that a dead agent means a dark server. The panel shows the last time it heard from each agent and alerts you on Telegram when one goes quiet for longer than you allow. For cronxo, a job whose agent has stopped answering can be closed from the dashboard anyway, so the run does not sit in “running” forever.
The same rule, three times
We wrote the outbound-only agent three times in three languages before noticing it was the same design. That is the kind of thing that eventually becomes a shared library, but for now the duplication is fine: the agents are small, and each one is tuned to its job.