My AI Agents Share a Server. Here's How I Keep Them Out of Each Other's Way.
The first time I had two coding agents deploying to the same server, I gave them the same password. It was the obvious thing to do. Then one agent's deploys started failing with an error about a build already running, an
The first time I had two coding agents deploying to the same server, I gave them the same password. It was the obvious thing to do. Then one agent's deploys started failing with an error about a build already running, and the build it was talking about belonged to the other agent. A few days later I wanted to stop one of them for the night and realized I couldn't do it without stopping both.
That's when I started thinking of agents less like scripts and more like roommates. They can share a house. They each need their own key, their own room, and some rules about how loud they're allowed to be.
This post shows how to give each AI agent its own account on a server running agentic-hosting, an open-source tool that turns a Linux server into a place agents can deploy apps and databases on their own. By the end you'll know how to create an account per agent, hand out keys that expire, swap keys without downtime, and shut off one misbehaving agent while the rest keep working.
⭐ If this is useful, a star on GitHub helps other people find it.
🤖 Give this to your agent: point it at https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt and it can sign up and set things up from that one link.
A few words first
agentic-hosting is one program, ah, that runs on your server. Agents talk to it over an HTTP API, the same way they'd call any web service.
Three terms come up a lot below:
- A tenant is an account on the server. Everything an agent creates (apps, databases, builds) belongs to one tenant.
- An API key is the password an agent sends with every request. A tenant can have several.
- The bootstrap token is a long secret you, the person running the server, set when you install it. It's what lets someone create a new tenant. Agents don't need it after that.
In the examples, $AH_URL is your server's address.
Step 1: one tenant per agent
Creating a tenant is one request with the bootstrap token in a header:
curl -s -X POST "$AH_URL/v1/tenants/register" \
-H "X-Bootstrap-Token: $AH_BOOTSTRAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"blog-agent",
"email":"[email protected]"}'
# {"tenant_id":"3f9a...","api_key":"8c41....d07e..."}
The server answers 201 with the new tenant's ID and its first API key. That key is shown once. The server only keeps a fingerprint of it, so if you lose it, it can't be read back.
Some rules worth knowing before you script this for a fleet of agents:
-
Each tenant needs its own email. Two tenants can share a name but not an email. I use plus addresses (
me+blog@,me+shop@), which the server accepts. A duplicate email gets422. -
Registration is slow on purpose. The server allows 5 registrations per hour from one IP address and 20 per hour in total. This keeps anyone from guessing the bootstrap token by brute force. If you set up ten agents in a loop, the sixth one gets
429with aRetry-Afterheader telling you how long to wait. - The bootstrap token must be at least 32 characters. The server won't start with a shorter one.
If you're just trying this on a laptop, ./bin/ah --dev --open-registration skips the bootstrap token entirely. Anyone who can reach the port can then make an account, so keep that to your own machine.
Step 2: read the key
A key looks like two long strings of letters and numbers joined by a dot: keyID.secret. The part before the dot is the key's ID. It isn't secret, and it's how you refer to the key later. The part after the dot is the actual password.
Agents send the whole thing on every request:
export AH_KEY="8c41....d07e..."
curl -s -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/tenant"
# the key's ID is everything before the dot:
echo "${AH_KEY%%.*}"
Step 3: keep the first key, give the agent a second
This is the habit that made everything else easier for me. The key that comes back from registration stays with me (I'll call it $MY_KEY). The agent gets a separate key that I create for it, with a name and an expiry:
curl -s -X POST "$AH_URL/v1/auth/keys" \
-H "Authorization: Bearer $MY_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"claude-code","expires_in":2592000}'
# {"id":"51be...","name":"claude-code",
# "api_key":"51be....9a2f...","prefix":"51be20c7",
# "expires_at":1795359600}
expires_in is in seconds, so that's 30 days. Leave it out and the key never expires. The longest you can ask for is 10 years.
Now there are two keys on the tenant and I can tell them apart. (jq is a small tool for picking fields out of JSON.)
curl -s -H "Authorization: Bearer $MY_KEY" \
"$AH_URL/v1/auth/keys" \
| jq '.[] | {id, name, last_used_at, expires_at}'
The list never shows secrets. last_used_at is a Unix timestamp that updates at most every five minutes, which is enough to see whether a key is still in use before you remove it.
A tenant can hold 20 keys at once. One catch I hit: an expired key still counts toward the 20 until someone revokes it. If your agents rotate keys often, have them clean up the expired ones.
Step 4: swap keys without downtime
Rotating a key is the same three moves every time:
- Create a new key for the agent.
- Give it to the agent and let it make a request with it.
- Revoke the old one, using its ID (the part before the dot).
curl -s -o /dev/null -w '%{http_code}\n' \
-X DELETE -H "Authorization: Bearer $MY_KEY" \
"$AH_URL/v1/auth/keys/$OLD_KEY_ID"
# 204
Revoking takes effect right away, even though the server caches logins for speed. The very next request with the old key gets 401 and {"error":"invalid api key"}. An expired key gets the same answer.
Because both keys work until you revoke the old one, the agent never has a moment where it's locked out. It's simple enough that an agent can do it on its own, a few days before its key expires.
Step 5: cut off one agent
Say an agent goes sideways at 2am. It's redeploying in a loop, or it's doing something you didn't ask for. You have two levels of off switch.
Revoke its key. Use your own key on that tenant to revoke the agent's key, as in step 4. The agent is locked out immediately. Its apps and databases keep running, so whatever it already shipped stays up while you figure out what happened. Every other agent on the server is untouched, because they're on other tenants with other keys.
Suspend the whole tenant. If you want everything that agent built stopped too:
curl -s -X DELETE -H "Authorization: Bearer $MY_KEY" \
"$AH_URL/v1/tenant"
This marks the tenant suspended, revokes every one of its keys, cancels its builds, and stops its apps and databases. Nothing is deleted. To bring it back, use the admin token (a second operator secret, set as AH_ADMIN_TOKEN when you install):
curl -s -X POST \
-H "X-Admin-Token: $AH_ADMIN_TOKEN" \
"$AH_URL/v1/tenants/$TENANT_ID/reactivate"
# {"tenant_id":"3f9a...","api_key":"...",
# "status":"active"}
$TENANT_ID is the tenant_id from step 1. You get a fresh key. The databases come back on their own. The apps need a redeploy.
One honest limit: keys on the same tenant are equals. There are no read-only keys or per-key permissions yet, so the agent's key could revoke yours. If that ever happens, the admin token can issue a new key for the tenant from its email with POST /v1/auth/recover. It's one more reason I never give the admin token to an agent.
What "its own space" actually means
Everything a tenant creates is scoped to it. Apps, databases, builds, and build images all carry the tenant's ID, and every lookup checks it. If one agent asks for another tenant's service by ID, it gets 404 and {"error":"service not found"}, as if the service doesn't exist.
It goes below the API too. Each app runs in its own Docker container, and each tenant's containers sit on their own private network, so one agent's app can't open a connection to another agent's app or database. An agent can deploy public images from Docker Hub, or images it built or snapshotted itself, but never another tenant's.
Each tenant also has its own budget. New tenants get 5 apps and 3 databases by default, and one build at a time. That last one is what bit me in the opening: two agents on one tenant were taking turns at a single build slot without knowing it. An agent can check where it stands:
curl -s -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/tenant/usage" \
| jq '{services, databases, api_keys, rate_limit}'
# {"services":{"used":2,"max":5},
# "databases":{"used":1,"max":3},
# "api_keys":{"used":2,"max":20},
# "rate_limit":100}
As the operator, you can raise or lower a tenant's limits with PATCH /v1/tenants/{id}/quotas and the admin token. Hitting a limit returns 409 with a message like quota exceeded: maximum 5 services reached.
The one thing that isn't per tenant: web addresses
If you start ah with a domain (--base-domain apps.example.com), each app gets a web address from its name: a service called web lives at https://web.apps.example.com. Those names are shared across the whole server, first come, first served. If one agent already has web, the next agent that tries gets:
409 {"error":"subdomain already taken: web —
choose a different service name"}
A handful of names like api, admin, www, and dashboard are reserved for everyone. The fix is simple: have each agent put its project in the name, like blog-web or shop-api. Without a domain, apps get an address made from their random ID, so this never comes up.
Speed limits for each agent
Each tenant gets 100 requests per second, with bursts up to 200. All the keys on a tenant share that one budget. On top of that, the whole server has a limit of 500 per second (bursts to 1,000), so many agents together can't swamp it. I went through how these work in an earlier post, so here's just the part that matters when several agents share a server.
There are two different 429s, and they mean different things:
429 {"error":"rate limit exceeded"}
-> this agent is going too fast
429 {"error":"global rate limit exceeded"}
-> the whole server is busy
Both come with a Retry-After header in seconds. The first one is the agent's own doing, and it should slow down. The second means some other agent is busy, and backing off for a few seconds is all it can do. Every logged-in response also carries X-RateLimit-Limit and X-RateLimit-Remaining, so an agent can see the first kind coming.
Retries stay inside the tenant too
Agents retry a lot, so the server supports an Idempotency-Key header: a random string the agent sends with a POST so that if it sends the same request again, the server replays the first answer instead of doing the work twice. Those keys are remembered for 10 minutes and are scoped to the tenant.
That scoping is one more reason to give each agent its own tenant. Two agents sharing a tenant could, in theory, pick the same key for the same URL. The second would get the first one's answer back, or a 409 if their requests were different. Separate tenants can never collide.
What I tell my agents
If you run more than one agent, here's the short version I give each of them:
- You have your own tenant and your own key.
Never use another agent's key.
- Put your project in every service name
(blog-web, not web). Names are shared.
- Rotate your key before it expires: create a
new one, switch, then revoke the old one.
- Revoke expired keys; they still use a slot.
- 429 "rate limit exceeded": you're too fast.
429 "global rate limit exceeded": server is
busy. Either way, wait Retry-After seconds.
- 409 is a limit, not a speed problem. Read the
error before retrying.
- Send an Idempotency-Key on any POST you
might retry.
Wrapping up
When agents share a server, the most useful thing I did was give each one its own tenant: its own key, its own apps and databases, its own budget, and its own place in line for builds. Keep the first key for yourself, give the agent a second one that expires, and you can rotate it, revoke it, or suspend the whole tenant without anyone else noticing.
- Star agentic-hosting on GitHub so more people find it.
- Give it to your agent: llms.txt to sign up and set up, or the agent skill for daily use.
- Try the quickstart, or start at the docs site.
- Open an issue if you're running a few agents on one box and want per-key permissions. That's the next thing I'd like to build, and I want to hear how you'd use it.
Cover photo: a community garden on East 92nd Street in Manhattan, where volunteers were each assigned their own plot. Suzanne Szasz, August 1973, for the EPA's Documerica project. U.S. National Archives, public domain.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.