A reinstall is one authenticated call, so it fits neatly into a pipeline: rebuild a server on a schedule, on a tag, or from a button in your CI. Below is a GitHub Actions job that reinstalls one bare-metal server, applies a saved disk layout and post-install script, then waits for the machine to come up. If you haven't made a key yet, start with Getting started with the Amóni API.
Before the pipeline: the allow-list
Every API key only works from the source addresses you give it, and that's the one part of CI that takes some thought. A GitHub-hosted runner doesn't have a fixed egress address, so there's nothing to put on an allow-list. That leaves two honest options: run the job on a self-hosted runner whose egress IP you know, or route the calls through a proxy or bastion with a static address. Put that single address on the key. Now the key is narrowly scoped and pinned to a machine you control.
The key needs read and servers.write and nothing else. Keep it in a repository or environment secret called AMONI_API_KEY, and never commit it.
Find the ids once
A reinstall is addressed by numbers: the server id, an OS profile id, plus a saved disk layout and post-install script if you use them. Look them up once and write them into the workflow. You'll find the server id in its portal URL and in GET /v1/servers. Everything else comes from the server's profiles:
curl -s https://api.amoni.app/v1/servers/955/profiles \
-H "Authorization: Bearer $AMONI_API_KEY"
{
"success": true,
"data": {
"profiles": [ { "id": 12, "name": "Ubuntu 24.04", "os_slug": "ubuntu-24.04" } ],
"disk_layouts": [ { "id": 1, "name": "web-raid1-lvm", "scope": "client" } ],
"scripts": [ { "id": 1, "name": "harden-ssh", "scope": "client" } ]
}
}
Saved disk layouts and scripts belong to you, and you create them once on the Install Profiles page. A reinstall can only use a custom layout or script once it's active, so create it and let it activate before it goes into CI.
The workflow
The job below reinstalls the server and then polls until the install reports complete. It runs on a self-hosted runner, which keeps its egress on the key's allow-list.
name: Reinstall server
on:
workflow_dispatch: # a button in the Actions tab
env:
AMONI: https://api.amoni.app/v1
SERVER_ID: "955"
jobs:
reinstall:
runs-on: self-hosted # fixed egress, on the key's allow-list
steps:
- name: Start the reinstall
env:
KEY: ${{ secrets.AMONI_API_KEY }}
run: |
curl -sf -X POST "$AMONI/servers/$SERVER_ID/reinstall" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"profile_id": 12,
"hostname": "web1.example.com",
"disk_layout_id": 1,
"script_ids": [1]
}' | tee reinstall.json
jq -e '.success' reinstall.json
- name: Wait for it to come up
env:
KEY: ${{ secrets.AMONI_API_KEY }}
run: |
for i in $(seq 1 60); do
sleep 30
body=$(curl -sf "$AMONI/servers/$SERVER_ID/provision-status" \
-H "Authorization: Bearer $KEY")
echo "$body" | jq -r '.data | "\(.status) \(.step // "")"'
if [ "$(echo "$body" | jq -r '.data.complete')" = "true" ]; then
echo "Install complete."; exit 0
fi
done
echo "Timed out waiting for the install."; exit 1
The reinstall call returns right away:
{ "success": true, "data": {
"message": "Reinstall started. This can take several minutes.",
"notice": null,
"applied": { "os": "Ubuntu 24.04" }
} }
That response means the install has started, not that it's done. Progress lives on GET /v1/servers/{id}/provision-status, which reports complete, a status and a step. There's no percentage while it runs, so poll on complete rather than on a number.
Getting the root password
Leave out the password and the installer generates one for you. Once the install is done, read it with GET /v1/servers/{id}/root-password. When there's nothing to hand back it answers 404 with code root_password_unavailable, so read that as “use your SSH key” rather than an error. Better still, pass ssh_key_ids in the reinstall. The machine then comes up with your key already on it, and there's no password to fetch at all. You can manage those keys with the API or in the portal.
Let a webhook tell you instead
Polling is fine for one server in one job. Once you're reinstalling a fleet, subscribe a webhook and let the operation.succeeded event for server.reinstall tell you as each server lands, instead of every job holding a runner open on a sleep loop.
Questions we get
- Why will my GitHub-hosted runner not work? Its egress address changes, so it can't go on the key's allow-list. Use a self-hosted runner or a proxy with a static IP.
- Do I have to give a disk layout and script? No. Send only
profile_idand the reinstall uses the image's default partitioning, with no post-install step. - How many scripts can I run? Up to five per reinstall, run in the order you list them. Each one runs once, as root, after the OS is in place.
- Can I reinstall a cloud instance this way? Not as a reinstall; for a cloud instance that's a rebuild. Cloud instances use
POST /v1/vms/{account}/{id}/rebuildand cloud-init. - Something was refused mid-install. The
noticefield in the response says so in plain terms, for example that your key couldn't be applied. Read it instead of assuming the request went through exactly as sent.
Still stuck?
Open a support ticket with the server id and the reinstall.json your job saved. With those, we can trace the operation and what the installer did with it.