Add servers without the dashboard
Installing the agent normally starts in the dashboard: Add server, copy the command, run it. When servers come and go on their own — Terraform applies, autoscaling groups, golden images — use one of two mechanisms instead.
| Create, then install | Enrollment key | |
|---|---|---|
| Who knows about the host first | Your automation, which calls the API | Nobody: the host registers itself when it boots |
| Key on the host | The per-server agent key only | An enrollment key |
| Best for | Terraform, Ansible, CI | Images, cloud-init, autoscaling groups |
Mechanism 1: create the server, then run its install command
Section titled “Mechanism 1: create the server, then run its install command”POST /v1/servers creates the server record and returns the agent’s key
and the exact install command. It needs a key with the servers:write
scope (the Automation preset).
curl -fsS -X POST https://api.monitorable.io/v1/servers \ -H "Authorization: Bearer $MONITORABLE_API_KEY" \ -H "Idempotency-Key: web-01-2026-11" \ -H "Content-Type: application/json" \ -d '{"name": "web-01"}'The 201 response carries the server, the agent_api_key (shown once) and
the install_command to run as root on the host. If the plan’s server limit
is reached the call answers 402
plan_limit_reached.
Send an Idempotency-Key (up to 64 characters) so a retried call does not
create a second server: repeating it with the same body returns the same
server with 200, without an agent_api_key or an install_command (both
fields are omitted from the JSON) and with a recovery hint. If the first response was lost, mint a fresh key with POST /v1/servers/{id}/agent-key:rotate. See Rate limits and
retries.
Pair it with DELETE /v1/servers/{id} when the host goes away. With
Terraform, the monitorable_server resource does
both: it creates the server, keeps the agent key and install command, and
deletes the server when you destroy it. The provider sends the
Idempotency-Key for you. Run the install command on the host with a
remote-exec provisioner:
resource "monitorable_server" "web" { name = "web-01"}
resource "null_resource" "web_agent" { depends_on = [monitorable_server.web]
triggers = { server_id = monitorable_server.web.id }
connection { type = "ssh" host = "203.0.113.10" user = "ubuntu" }
# install_command embeds the agent's API key and is sensitive. It is null # for an imported server, so this applies only to servers Terraform created. provisioner "remote-exec" { inline = [monitorable_server.web.install_command] }}The Terraform provider guide covers installing the provider, the scopes the key needs, alert templates, maintenance mutes, import, and keeping the state safe.
Mechanism 2: an enrollment key
Section titled “Mechanism 2: an enrollment key”An enrollment key can register servers and nothing else. Create one under
Team → API keys with the Enrollment preset, and put it in the image
or the user data. Each host then calls Monitorable when it first boots,
receives its own agent key, writes it to /etc/monitorable/agent.env
exactly as the dashboard’s command does, and starts the agent.
One host
Section titled “One host”Run the installer as root with the key:
curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sudo sh -s -- --enroll-key=<enrollment-key>Each run creates a new server. That is deliberate: hostnames are not unique in every fleet and a machine id is identical on cloned images, so Monitorable never guesses which host this is. Re-running the command on a host that already runs the agent adds another server and leaves the old one stale — to update an enrolled host, re-run the key-free command from Updating and uninstalling.
cloud-init
Section titled “cloud-init”Put the instance id into --server-ref and each instance re-enrolls as
itself instead of adding a server. Use this as the instance’s user data; it
works on any cloud that runs cloud-init, as long as each instance has its own
instance id (an image cloned with a fixed NoCloud instance-id would make
hosts share one):
## template: jinja#cloud-config# cloud-init fills in the instance id belowruncmd: - "curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --enroll-key=<enrollment-key> --server-ref={{ v1.instance_id }}"The first line must be exactly ## template: jinja: it is what makes
cloud-init replace {{ v1.instance_id }} before running the command.
cloud-init runs commands as root, so there is no sudo.
--server-ref: the host’s stable identity
Section titled “--server-ref: the host’s stable identity”--server-ref is the identifier your automation already has for the host: an
instance id, a Terraform resource id, an inventory name. Monitorable stores
it with the server.
- When the installer runs with a
--server-refthe organization has not seen, it creates the server. - When the organization already has a server with that reference, the installer re-enrolls it: same server, new agent key.
- Without
--server-ref, every run creates a new server. There is no default, by design.
The installer accepts a reference of up to 128 characters made of letters,
digits, ., _, :, / and -; use that form. (The API itself only
refuses whitespace and control characters and more than 128 characters.)
A reference is unique per organization: the same value in two organizations
is two servers. Use something that is unique across everything the
organization monitors and that stays the same for the life of the host:
| Where the host runs | --server-ref value |
|---|---|
| Any cloud with cloud-init | {{ v1.instance_id }} (jinja user data, above) |
| AWS | the instance-id from the IMDSv2 metadata service |
| Hetzner | http://169.254.169.254/hetzner/v1/metadata/instance-id |
| DigitalOcean | http://169.254.169.254/metadata/v1/id |
| Proxmox | the VMID, prefixed with the cluster name so it stays unique (pve1-101) |
| Terraform | self.id of the resource |
| Ansible | inventory_hostname |
On AWS, read the id with IMDSv2 and pass it in:
TOKEN=$(curl -fsS -X PUT http://169.254.169.254/latest/api/token \ -H "X-aws-ec2-metadata-token-ttl-seconds: 60")REF=$(curl -fsS -H "X-aws-ec2-metadata-token: $TOKEN" \ http://169.254.169.254/latest/meta-data/instance-id)curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --enroll-key=<enrollment-key> --server-ref="$REF"On Hetzner and DigitalOcean, read the id from the metadata URL in the table
and pass it as --server-ref. On Hetzner:
REF=$(curl -fsS http://169.254.169.254/hetzner/v1/metadata/instance-id)curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --enroll-key=<enrollment-key> --server-ref="$REF"On DigitalOcean:
REF=$(curl -fsS http://169.254.169.254/metadata/v1/id)curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --enroll-key=<enrollment-key> --server-ref="$REF"Ansible
Section titled “Ansible”- name: Enroll the host in Monitorable ansible.builtin.shell: >- set -o pipefail && curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --server-ref={{ inventory_hostname }} args: executable: /bin/bash creates: /etc/monitorable/agent.env environment: MONITORABLE_ENROLL_KEY: "{{ monitorable_enroll_key }}" become: true no_log: truecreates skips the task on hosts that are already installed. Keep
monitorable_enroll_key in Ansible Vault.
Installer options
Section titled “Installer options”| Option | What it does |
|---|---|
--enroll-key=<key> |
The enrollment key. Required to enroll. |
--server-ref=<ref> |
The host’s stable identity (above). Optional; without it every run adds a server. |
--name=<name> |
The server’s name in the dashboard. Defaults to the host’s hostname. The installer accepts up to 100 characters: letters, digits, spaces, ., _ and -. |
MONITORABLE_ENROLL_KEY |
Environment variable that replaces --enroll-key, so the key is not in the process list or sudo’s log. |
With the environment variable, open a root shell (sudo -s), export the
key there, then run the installer:
export MONITORABLE_ENROLL_KEY=<enrollment-key>curl --proto '=https' --tlsv1.2 -fsSL https://get.monitorable.net/install.sh | sh -s -- --server-ref="$REF"What re-enrolling does
Section titled “What re-enrolling does”When --server-ref matches a server the organization already has, the
server keeps its name, alert template, mute state and history — enrolling
never renames. The agent key is replaced and the previous agent key stops
working, so the old installation (if it is still running) stops reporting.
If two hosts enroll with the same --server-ref at the same moment, they end
up with one server, and only the host that finished last holds the working
key.
Every enrollment, a first one or a re-enrollment, is recorded in the audit
log (Team → API keys) as enroll.enroll with the server’s id. The server’s provenance (Enrolled via <key name>) is
written when the server is created and does not change on a re-enrollment.
Plan limit
Section titled “Plan limit”Enrolling a new server counts against the plan’s server limit. When the limit is reached the installer prints
❌ Plan limit reached: <detail from the API>Remove a server or upgrade the plan, then re-run.and exits with status 1, having installed nothing. Re-enrolling a known
--server-ref adds no server, so it still works when the organization is at
its limit. See pricing for the plans.
Rate limit
Section titled “Rate limit”Enrollment is limited to 10 per minute per key, in a fixed one-minute
window. A host that is over the limit waits for the Retry-After the API
sends and retries; the installer keeps retrying for about four minutes. Hosts
that are waiting all come back when the window resets, so one key enrolls
roughly 40–50 hosts that boot at the same moment. A host that runs out of
retries exits with status 1 and “re-run in a minute”, having installed
nothing, and cloud-init does not run the command again. Stagger larger
simultaneous boots, or give groups of hosts their own enrollment key.
Calling the enroll endpoint yourself
Section titled “Calling the enroll endpoint yourself”The installer calls POST /v1/enroll with {"name": "…", "server_ref": "…"}
(server_ref optional). The response carries server_id, name,
agent_api_key (shown once), ingest_endpoint and rotated: 201 for a
new server, 200 with rotated: true for a re-enrollment, whose name is
the existing server’s. name is required and validated on every call, a
re-enrollment included (1–100 characters after trimming), and server_ref
is trimmed, at most 128 characters, with no whitespace or control
characters; a bad value answers 400
invalid_argument. The key needs the
servers:enroll scope and the admin role. The endpoint ignores
Idempotency-Key — server_ref is what prevents duplicates. Its errors are
listed on Errors.
Keeping the key safe
Section titled “Keeping the key safe”An enrollment key can only register servers — it cannot read, rename or delete anything. The installer neither writes it to disk nor prints it. Revoke it from Team → API keys when the image or user data that carries it is retired; servers already enrolled keep reporting with their own agent keys.