Documentation
Three parts: install the app, connect your first server, and work out what happened when something goes wrong.
Installing the app
The app runs on your own machine — Windows, macOS or Linux. It is not a service and not a cloud: your clients' servers live in a list on your disk, and keys and passwords live in your operating system's credential store.
- Download the installer from the download page.
- Run it. Installers are not yet signed with a developer certificate, so Windows shows a SmartScreen warning and macOS shows Gatekeeper. The download page explains what to click.
- Open the app. Adding a server and checking whether it is ready both work straight away, with no account.
- Provision the stack. No account is needed for that: the installer bundle is public, no plan limits the number of servers or sites, and everything free works anonymously.
- An account is only needed for the features that physically cannot work without our infrastructure — today that is external uptime monitoring and domain reputation checks. Sign up here or inside the app, then confirm your address using the link we send: until it is confirmed the paid features stay closed, because we would have nowhere to tell you when a server is in trouble.
Connecting a server
What the server needs
- A Linux VPS running systemd — tested on Debian and Ubuntu.
- SSH access as
root, or as a user with passwordlesssudo. Provisioning runs unattended and would simply hang on a password prompt. - One free port — provisioning picks which. Busy 80 and 443 are not a problem: we never take them, we stand beside your client's existing web server. The port comes from a list that Cloudflare proxying accepts, and it goes into the tracking snippet.
- About 2 GB of free disk and 1 GB of memory. The installer sets up Docker itself if it is missing.
- A domain or subdomain for the analytics panel. With 80 and 443 free, an A record pointing at this server is enough. With them busy, the certificate is issued over DNS-01: you will need either API access to your DNS (Cloudflare, Route 53, DigitalOcean) or a single CNAME pointing at our fallback responder — the app tells you which one.
Step by step
- Add the server. Host, port, user and how to sign in: a private key (ed25519, RSA or ECDSA in OpenSSH format, passphrase supported) or a password. PuTTY
.ppkkeys are not supported — convert one in PuTTYgen via Conversions → Export OpenSSH key. - Test the connection. This is a separate button on purpose: the app writes nothing to disk until you have confirmed that access works. On the first connection it remembers the server's host key and checks it from then on.
- Run the readiness check (dry run). It collects facts — system version, free space, busy ports, whether Docker is there, whether
sudoworks without a password — and leaves not a single file behind. That is exactly why the script is piped to standard input rather than written to a temporary file. - Read the report. Blockers have to be fixed:
sudoasking for a password, a Docker daemon that is installed but dead, not enough disk. Busy 80 and 443 are not on that list — provisioning goes to another port. Warnings are your call. - Provision the stack. Give it a domain and, if you want, allow the chosen port through the firewall — that rule is the one change we make outside our own directory and Docker, and rollback removes it. The app generates the secrets (Postgres password, the analytics session key and the admin password), stores them in your OS credential store, and brings up three containers on the server: Umami, Postgres and Caddy. Progress arrives step by step, live.
Start with a site that is not critical. We are putting our software on somebody else's production. Isolation is complete — our own Docker network, our own volumes, the existing web server untouched — but the first run is wiser where a mistake costs less.
If the connection drops mid-install, the install still finishes on the server: SSH without a pseudo-terminal sends the process no signals. The app finds out how it ended when you return to the server screen. And if the install fails, the rollback returns the server to how it was — including removing Docker if we were the ones who installed it.
Sites and analytics
- Press “Look for sites” if the server already runs a web server of its own: the app reads its configuration and lists the domains it serves. Reads only — not a byte written to those configs, no restart, and the install does not depend on what is found. The list stays with you: none of it reaches us.
- Add a site to the server: domain and name. No plan limits how many sites you may have, and the domain stays with you: it reaches us only when you connect the site to a paid feature.
- Copy the tracking snippet into the site's
<head>. The data goes to your client's server, not to us. - The per-site dashboard and the overview across servers open in the app. The analytics panel is not exposed: the app reaches it through an SSH tunnel, and the local port is not published either.
- Open the site's “Data” tab to add a read-only SQL query against its own database — on the server itself or in a Docker container by name; MySQL, MariaDB, PostgreSQL or SQLite. It runs through the same tunnel and turns into a card: a number, a short line of text or a small table, sized and ordered by hand. The connection needs a user that can only
SELECT, and the screen writes theGRANTfor you; a superuser is refused outright. The answer stays on this screen — it is never saved to disk and never sent anywhere, not even to us. - On the Notifications screen, set up email and, if you want, Telegram. The agent sends them from your client's server — that is the whole point: it reports trouble while the app is closed.
Moving from another monitoring service. Export your monitors from UptimeRobot or HetrixTools as CSV and load the file on the Uptime screen. It is read on your machine: you see what it parsed, tick what to carry over, and only that reaches us. Rows we could not take are listed with the reason. Ping and port checks we do not run at all; a heartbeat we do run, but it is set up on the Jobs screen rather than imported — it needs a secret address of its own inside your cron, and one created silently would declare itself missing the same night. More monitors than the plan allows is the normal case: all of them are kept, the plan's worth run, the rest wait paused. The first import switches Remote on for 90 days, once per account.
The agent watches disk, memory and CPU, plus domain and certificate expiry. It only raises what will not fix itself: a disk running out, a certificate about to expire, a domain about to lapse.
External checks and Job Guard
These run from our infrastructure, not from your client's server, and that is the whole reason they are paid: a monitor standing on the watched machine dies together with it. The unit the plan counts is the check, not the site — a site can have several: the home page, /api/health, the login page. There is no limit on sites or servers on any plan, and there never will be.
Every check has a profile, and the words are the same ones the agent uses on the server itself, so you do not have to keep two vocabularies in your head:
- Page — the address answers with a code below 400.
- Text — a string you name is present in the response body. Case matters: search for it exactly as it appears on the page.
- JSON — the response parses as JSON and the field you name is not empty. The value is not compared: a counter that changes by itself would raise an alarm every hour.
- Redirect — the address redirects, with the code you expect. Where it leads is not checked from outside: Gatus conditions have no access to response headers. The agent's local check does verify the target, and the screen says so plainly — a check that promises more than it does is worse than no check.
- DNS — the domain resolves to the addresses you pinned. This is the one profile the agent cannot have: from inside the VPS the domain resolves to itself for anyone. The alarm comes after three rounds in a row, because a record change does not reach every resolver at once.
A check never carries a header, and that is a refusal rather than a gap. The next step after "I need a path and a string" is "I need an Authorization header" — and accepting it would make us a place where your clients' API tokens are stored. The agent's local checks do take headers: they live on your client's server, in a file with mode 640, and the token there is his own.
Job Guard watches what never answers at all — a nightly backup, a sync, an export. The task reports to a secret address of its own; you set the period and a grace window, and we write to you when the report does not arrive. A schedule is a period plus a window, not a cron expression: cron is meaningless without the time zone of the machine that runs it, and we do not know even which machine that is. Two different troubles are told apart, because the first step differs: "reported a failure" sends you to the task's log, "no signal" sends you to cron. A task that has never reported once does raise the alarm — that is exactly the case it is there to catch. Whoever knows the address can report "all good": for monitoring that is acceptable, but it should be said out loud.
Maintenance window. Before you update a stack, open a window: it silences the email, not the check. The incident is still recorded and the screen still shows the truth. Trouble that outlives the window reaches you by email once the work is over; trouble that ended inside it does not, because there is nothing left to report.
Domain mail
A site's domain gets a "Mail" tab. It turns on Cloudflare Email Routing for that domain and lets you add addresses — support@, say — that forward to an inbox you already use. It is free and needs no account with us: the app calls Cloudflare's API directly, with a key that stays on this computer, and mail never passes through our infrastructure — it goes from Cloudflare straight to the inbox you chose.
The domain needs to be delegated to Cloudflare's name servers, with Cloudflare serving its DNS in full — a partial (CNAME-only) setup cannot host Email Routing. Mail belongs to the domain, not to the site or the server: removing either from the app does not touch it, and shop.example.com and example.com share the same mail because they share the same domain.
Creating the key
In the app, open the Cloudflare section in settings and press "Create key in Cloudflare". It opens Cloudflare's own token page with the right permissions already checked — pick the account and the client's domains, press "Create", then paste the key back into the app. The key is stored only in this computer's credential store; it is never sent to us and it is never shown again after you paste it. Use a separate Cloudflare account per client where you can — a key on a shared account sees every inbox connected to it, not only the domain you are working on.
Turning mail on for a domain
Open the site's Mail tab. The app reads the domain's DNS and answers with one of three things:
- Can turn it on. It shows what Cloudflare will add; you confirm and the domain is ready in a minute or two.
- One step in Cloudflare first. The usual case when the site already sends mail — order confirmations, say. The app prepares the one line that merges into the existing SPF record without breaking it; you paste it in Cloudflare and press "Check".
- Mail already works here. Google Workspace, Yandex 360, a mail server of the client's own. The app changes nothing and explains why: turning routing on would take mail away from those inboxes.
Creating an address is one form: a name and where to forward it. If that inbox has never received mail through this Cloudflare account before, Cloudflare sends it a confirmation link — the address is marked "waiting for confirmation" until it is clicked, then it starts working on its own.
Moving mail to another service later
The app does not manage that move — Email Routing forwards mail rather than hosting it, so there is no inbox to export. To switch providers by hand, start in Cloudflare under Email → Email Routing → Settings: Email Routing locks the MX, SPF and DKIM records it added, and the DNS page will not let you change them until you disable Email Routing for the domain or unlock its records there. Then remove whatever Email Routing records are left, point MX at the new provider, and replace our include in the SPF record with the one it asks for. Check mail delivery to and from the domain again once the new records have propagated.
Lost this computer? The key lives only in its credential store. Revoke it from the Cloudflare dashboard (My Profile → API Tokens) — the app has no separate way to disable it, and support will never ask you for this key: nobody needs it but Cloudflare and the app on your own machine.
Updates, backups, removal
- Stack updates. Before anything changes, a database dump and a copy of the configs are taken. If the update goes wrong, both come back.
- Backups. "Download backup" packs a Postgres dump and the stack configuration into an archive and downloads it. The archive contains passwords — store it accordingly.
- Removal. Two different actions. "Forget" drops the server from the app. "Remove from the VPS" destroys the stack together with every bit of your client's analytics, and that is irreversible.
- App updates. They arrive on their own and are checked against a signature. An unsigned update is refused.
Working as a team
Several people at an agency can each run their own copy of the app against the same client server — with no shared account and no syncing through our infrastructure. Access is bounded by the thing you already use for it: SSH.
What it takes
- SSH access to the server for each person:
root, or a user with passwordlesssudo— the same thing provisioning needs. Anyone with that access can already read the stack's passwords on the server; the app grants no new rights, it only shows what that person can already reach. Revoking access means revoking SSH, which your agency already knows how to do. - One app version across the team. An older copy cannot read the project list and settings from the server and overwrites them with its own, stale ones. The app notices the trace an older copy leaves and warns about it on the main screen.
- One vpsFocus account per agency — but only if you use external uptime monitoring, the one feature that needs an account at all. Installing, analytics, sites and domain mail all work without one, for any number of people.
How it works
A developer adds the server to their own copy of the app the normal way. If the stack is already up, the app notices on its own and pulls the passwords from the server — nothing needs reinstalling, and nothing on the server changes. From there they see exactly what the person who provisioned it sees: analytics, the overview, checks, logs, SEO.
Shared state lives on the server and is the same for everyone: the project list, key addresses, notification settings — mail, Telegram, the maintenance window — and the stack's passwords. One person's change is visible to the rest the next time they open the screen.
Personal state stays on each computer and never spreads across the team: your own activity log, the site database connections on the "Data" tab, the Cloudflare key for domain mail, the note of when you last pulled a backup.
Whoever saves last wins — but not everywhere the same way. Lists the screen reloads from the agent fresh each time — measurement pages, log paths, key addresses — behave exactly like that: edits are rare, and the last one saved wins. The project list and notification settings are more careful: a change is applied on top of what is actually on the server the moment you save, not on top of what was open on your screen an hour ago. If a colleague changed a value you never touched, your save leaves it alone, and the screen tells you which values it left as they were.
Someone leaves the team
Revoking SSH is enough to stop someone connecting to the server — through the app or any other way. But part of what lives in the agent's configuration is shared across the whole agency rather than tied to one server: the outgoing mail password, the Telegram bot token, the Google key, the backup storage keys. Whoever leaves carries those in memory, so when someone leaves the team:
- revoke their SSH access to the servers;
- if they had their own sign-in to the vpsFocus account, change the account password and sign it out everywhere;
- change the outgoing mail password, the bot token, the Google key and the backup storage keys — an ordinary settings save on any server in the team spreads the new values to the rest on its own.
Restoring a copy onto another VPS
Moving to a new server is an ordinary copy restore, as described above, plus moving external checks to the new server where the app offers to do it: the old and the new server are different servers to us, even when it is the same client.
Cloning a VPS at your provider is not the same as moving to one: a disk snapshot carries over the marker the app uses to recognize the server too. If two servers end up carrying the same marker, the app spots it from the SSH host key fingerprint and asks: is this the same server reinstalled, or a separate copy? The one case this cannot catch is a cloud image that does not generate a fresh SSH key when it is cloned.
Common problems
The server already runs your client's web server
That is the normal case, not an obstacle. We do not touch their nginx, Apache or Caddy, we do not reconfigure it, and we do not take 80 or 443: the stack goes up on a free port and lives beside it. That port goes into the tracking snippet and updates never change it — it is written into every one of your client's pages.
"sudo asks for a password"
Provisioning runs unattended and would hang on the prompt. Give the user passwordless sudo, or connect as root.
The certificate is not issued
First look at how yours is being issued.
- Over an A record (80 and 443 free): almost always the same cause — the record does not point at this server yet, or has not propagated. Check that the domain resolves to the server's address and try again.
- Over DNS-01 with a provider key (ports busy): check that the token is allowed to edit records in that exact zone. The credentials live on your client's server in
.envwith mode 600 and never reach us. - Over our fallback responder: make sure the
_acme-challengeCNAME for your domain points at the name the app gave you. Until it propagates the challenge cannot pass.
None of this breaks provisioning: the stack is already running by that step, the app says so — "the stack is up, waiting for the certificate" — and Caddy keeps trying on its own. Let's Encrypt limits how many attempts you get per week, so there is no need to restart the install dozens of times in a row.
"The server's key has changed"
The host key differs from the one remembered on the first connection. That happens when a server is rebuilt or reinstalled — and it is also exactly what an intercepted connection looks like. Do not continue until you know which one it is.
Windows warns when installing
Installers are not signed with a developer certificate yet. The download page explains how to get past the warning.
"Confirm your email address"
Until the address is confirmed, everything that needs our infrastructure is closed: registering servers with us, connecting sites to monitoring, the paid features. Installing is not among them — the installer bundle is public, and the stack goes up without an account at all. The letter can be sent again from your account page here or from the account screen in the app.
The analytics panel does not answer
After a server reboot the containers need a moment to come up. If that is not it, open the server screen: it shows whether the stack is running and which version it is.